<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kyle M. Douglass (Posts about optics)</title><link>https://kylemdouglass.com/</link><description></description><atom:link href="https://kylemdouglass.com/categories/cat_optics.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 &lt;a href="mailto:kyle.m.douglass@gmail.com"&gt;Kyle M. Douglass&lt;/a&gt; 
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/"&gt;
&lt;img alt="Creative Commons License BY-NC-SA"
style="border-width:0; margin-bottom:12px;"
src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"&gt;&lt;/a&gt;</copyright><lastBuildDate>Fri, 10 Apr 2026 07:41:20 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Ray-Surface Intersections with the Newton-Raphson Algorithm</title><link>https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;A few weeks ago I restarted work on &lt;a href="https://kmdouglass.github.io/cherry/"&gt;Cherry&lt;/a&gt;, my sequential ray tracer, by porting the GUI from Javascript/React to pure WASM with &lt;a href="https://github.com/emilk/egui"&gt;egui&lt;/a&gt;. I am very happy with the results. It's much easier to add features with egui, and I have no regrets about giving up the DOM in the web application. I was never very good at web developement, and I always felt that React has too much unseen magic happening behind the scenes.&lt;/p&gt;
&lt;p&gt;Having gotten the frontend work out of the way, I turned my attention back to adding features to Cherry. One of the applications that interests me in the lab is scan lenses, i.e. lenses that translate an angular deviation of a laser beam into a lateral displacement. These lenses are designed so that their scanning plane is as flat as possible over a large field of view. They often must work across multiple wavelengths and at large field angles.&lt;/p&gt;
&lt;p&gt;As a starting point, I put the &lt;a href="https://optiland.readthedocs.io/en/latest/gallery/specialized_lenses/f_theta_lens.html"&gt;f-theta scan lens example from Optiland&lt;/a&gt;, which itself comes from &lt;a href="https://www.routledge.com/Lens-Design/Laikin/p/book/9780849382789"&gt;Milton Laikin's Lens Design, 4th ed.&lt;/a&gt;, into Cherry and began varying the incident field angle. It did not take long before problems in the ray tracing routine appeared.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton-raphson-ray-trace-error.png"&gt;&lt;/p&gt;
&lt;p&gt;At a field angle of exactly 5.6 degrees, the marginal ray from the Fraunhofer C line ( \( \lambda = 0.6563 \, \mu m\) ) reflects backwards off the first lens surface and intersects the origin. It propagates correctly for field angles of 5.5 degrees and 5.7 degrees, which to me suggests that there is a numerical accident that happens at exactly this value. Furthermore, the spot diagram shows ray-surface intersections across all wavelengths in the image plane disappearing and reappearing randomly as the field angle increases, with the overall number of ray trace errors increasing with field angle. Small angles do not seem to have the problem, and indeed I had not yet tried examples with highly curved surfaces such as this f-theta lens.&lt;/p&gt;
&lt;p&gt;As it turns out, the cause of this problem was a silly bug that came from code I wrote three years ago. At the time, I didn't truly and fully understand the Newton-Raphson (NR) root finding algorithm for finding ray-surface intersections. I wanted surface normal vectors to always be unit vectors by convention, and this subtlety ended up degrading and in some cases, ruining the algorithm's ability to find the intersection point, especially at large angles of incidence.&lt;/p&gt;
&lt;p&gt;This post is a recap about my journey in debugging the issue and better understanding the NR algorithm. I hope you learn as much as I did from it.&lt;/p&gt;
&lt;h2&gt;Debugging Ray-Surface Intersections&lt;/h2&gt;
&lt;h3&gt;Running Traces through Algorithms&lt;/h3&gt;
&lt;p&gt;In practice there are a lot of ray-surface intersections to compute; tracing 1000 rays through 8 surfaces, for example, yields 8000 intersections. This means the algorithm loops 8000 individual times. When only a subset of these fail, it pays to have good debugging tooling in place to identify the state of the algorithm during a failure.&lt;/p&gt;
&lt;p&gt;For this I turned to the excellent &lt;a href="https://github.com/tokio-rs/tracing"&gt;tracing&lt;/a&gt; crate, which has become something of a &lt;em&gt;de facto&lt;/em&gt; standard for logging in Rust. The primary abstractions in tracing are events and spans. Events are the most straightforward to understand because they are the same thing as log messages in other languages.&lt;/p&gt;
&lt;p&gt;tracing's documentation provides a good, high-level explanation of spans&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unlike a log line that represents a moment in time, a span represents a period of time with a beginning and an end. When a program begins executing in a context or performing a unit of work, it enters that context’s span, and when it stops executing in that context, it exits the span.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The value in using spans is that you can attach data to them, forming a context. Every event that is emitted during a span is associated with this data, regardless of where the event was emitted. In pseudocode, my ray tracing algorithm roughly works like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surface_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surfaces&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ray_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;intersect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When an intersection failure occurs, I'd like to know the &lt;code&gt;ray_id&lt;/code&gt;, but I'd also like to know the state of variables inside the &lt;code&gt;intersection&lt;/code&gt; method. Using spans, I do not have to thread &lt;code&gt;ray_id&lt;/code&gt; inside the &lt;code&gt;intersect&lt;/code&gt; method to attach it to log messages. Instead, I create a span at the top of the inner-most loop that contains ray_id in its context. Then, any events inside the &lt;code&gt;intersection&lt;/code&gt; method will be associated to that context. I can then filter by, say, &lt;code&gt;ray_id=42&lt;/code&gt; to see all events that happened for that ray inside &lt;code&gt;intersect()&lt;/code&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surface_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surfaces&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ray_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trace_span&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"trace_ray"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ray_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surface_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entered&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;begins&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;                                                                                    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;intersect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;                                                     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;                                                                                    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;&lt;span class="w"&gt;                                                      &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-----------------------------------------------------------------------&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ends&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;A Failing Test Case&lt;/h3&gt;
&lt;p&gt;I recreated the same lens in an integration test with a single wavelength at \( \lambda = 0.5876 \) and an off-axis tangential ray fan consisting of 9 rays and incident at 20 degrees. I simplified the test case to reduce the total number of errors, which in turn allowed me to better isolate problems. There were two notable types of errors. In the first, I saw NaNs appear in some of the values manipulated by the Newton-Raphson algorithm:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="go"&gt;  2026-03-26T07:49:32.552835Z ERROR cherry_rs::views::ray_trace_3d::rays: Ray intersection did not converge, ctr: 999, s: NaN, residual: NaN&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:97&lt;/span&gt;

&lt;span class="go"&gt;  2026-03-26T07:49:32.552896Z ERROR cherry_rs::views::ray_trace_3d::trace: Ray terminated due to intersection failure, ray_id: 8, surface_id: 2, reason: Ray intersection did not converge&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/trace.rs:57&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In the second type of error, the ray-surface intersection simply did not converge.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="go"&gt;  2026-03-26T07:49:32.553113Z ERROR cherry_rs::views::ray_trace_3d::rays: Ray intersection did not converge, ctr: 999, s: 0.339233626291856, residual: -2.220446049250313e-16&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:97&lt;/span&gt;

&lt;span class="go"&gt;  2026-03-26T07:49:32.553139Z ERROR cherry_rs::views::ray_trace_3d::trace: Ray terminated due to intersection failure, ray_id: 3, surface_id: 3, reason: Ray intersection did not converge&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/trace.rs:57&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first type of error was easy to fix. The NaN occurs because the first guess at the intersection point lies further from the axis than the surface's radius of curvature. To correct for this, I now check for NaNs and bisect the guess backwards until I am inside the domain of the surface.&lt;/p&gt;
&lt;p&gt;The second error type was the tricky one, and I'll spend the rest of this post discussing it.&lt;/p&gt;
&lt;h2&gt;The Newton-Raphson Algorithm&lt;/h2&gt;
&lt;p&gt;At this point while debugging, I began to feel like I needed a refresher in the NR algorithm. It has been about three years since I first implemented it and quite frankly I have forgotten a lot of the details. So I'm going to circle back to the basics to better prepare myself to fix this thing.&lt;/p&gt;
&lt;h3&gt;Root Finding&lt;/h3&gt;
&lt;p&gt;The Newton-Raphson algorithm is a well-known numerical routine for finding the roots (a.k.a. zeros) of a function. I think it's best illustrated by way of example. I found one at &lt;a href="https://atozmath.com/example/CONM/Bisection.aspx?q=nr&amp;amp;q1=E1"&gt;https://atozmath.com/example/CONM/Bisection.aspx?q=nr&amp;amp;q1=E1&lt;/a&gt; that involves finding the single zero of the function \( f(x) = x^3 - x - 1 \). The function is plotted below:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_example_function.png"&gt;&lt;/p&gt;
&lt;p&gt;The algorithm is derived as follows: assume we want to find the root of a function \( f(x) \). Choose a starting point \( x_0 \) close to the root and find the slope of the line tangent to the curve of the function at this point. Extend this line to \( x_1 \), the x-intercept where \( y = 0 \). The expression for the slope of \( f (x) \) at \( x_0 \) is&lt;/p&gt;
&lt;p&gt;$$ f'(x_0) = \frac{\Delta y}{ \Delta x} = \frac{f(x_0) - f(x_1)}{x_0 - x_1} = \frac{f(x_0) - 0}{x_0 - x_1}. $$&lt;/p&gt;
&lt;p&gt;Below you can see what this construction looks like using \( x_0 = 1.5 \).&lt;/p&gt;
&lt;figure id="nr-construction"&gt;
  &lt;img src="https://kylemdouglass.com/images/newton_raphson_construction.png"&gt;
&lt;/figure&gt;

&lt;p&gt;Solving this expression for \( x_ 1 \) gives&lt;/p&gt;
&lt;p&gt;$$ x_1 = x_0 - \frac{f(x_0)}{f'(x_0)}. $$&lt;/p&gt;
&lt;p&gt;Repeat the process using \( x_1 \) as the new starting point:&lt;/p&gt;
&lt;p&gt;$$ x_2 = x_1 - \frac{f(x_1)}{f'(x_1)}. $$&lt;/p&gt;
&lt;p&gt;The more you repeat the process, the closer you get to the root.&lt;/p&gt;
&lt;h4&gt;Before I Go On, Some Vocabulary&lt;/h4&gt;
&lt;p&gt;The function \( f(x) \) is often called the &lt;strong&gt;residual&lt;/strong&gt; in the NR literature because it can be thought of as a distance-based error from the the value \( f(x) = 0 \).&lt;/p&gt;
&lt;p&gt;As far as I can tell there's no standard term for \( f'(x) \). I'll refer to it as the &lt;strong&gt;denominator&lt;/strong&gt; for simplicity. Once I reach the part of this post on surface representations, I will also refer to it as the surface normal because it is related to the normal vector to the lens surface.&lt;/p&gt;
&lt;h4&gt;Termination Criteria&lt;/h4&gt;
&lt;p&gt;There are two common stopping criteria for NR. In the first, you stop iterating whenever the difference between successive steps \( x_i \) and \( x_{i+1} \) is less than some tolerance. In the second, you stop when \( | f(x_n) | \) is less than some tolerance. You can also combine the two so that you stop when either is satisfied. This helps terminate the algorithm when it is converging so slowly that \( \Delta x_i \) is large even but the residual is small.&lt;/p&gt;
&lt;p&gt;Here are the first six iterations of the algorithm for finding the root of \( f(x) = x^3 - x - 1 \) when starting at \( x_0 = 1.5 \).&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_convergence.png"&gt;&lt;/p&gt;
&lt;p&gt;After 6 steps, the algorithm has identified the root \( x = 1.324718 \) with a precision better than \( 10^{-6} \).&lt;/p&gt;
&lt;h3&gt;Oscillations&lt;/h3&gt;
&lt;p&gt;Now of course I deliberately diverted your attention away from the important point that you need to choose the starting point such that it is already close to the root. Here's what happens when I choose a starting point close the local maximum at -0.5:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_divergence.png"&gt;&lt;/p&gt;
&lt;p&gt;The algorithm struggles to converge because the initial tangent line is nearly horizontal. This results in the next guess being very far off target and ultimately the algorithm oscillates irregularly around the starting point. However, at step 12, it happens to land just to the right of the local minimum at \( x = 0.7425 \), which sends the next guess far to the right of all local extrema.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_near_local_minimum.png"&gt;&lt;/p&gt;
&lt;p&gt;From this point, the algorithm can simply descend downhill, where by step 19 it has found the root with a tolerance better than \( 10^{-6} \).&lt;/p&gt;
&lt;h3&gt;Convergence Guarantees&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://archive.nptel.ac.in/content/storage2/courses/122104019/numerical-analysis/Rathish-kumar/ratish-1/f3node7.html"&gt;If the starting point is close to the root, the Newton-Raphson algorithm has quadratic convergence.&lt;/a&gt; This means that the error \( \epsilon_{i+1} \) in step \( i + 1 \) is proportional to the square of the error at the previous step, \( \epsilon_i^2 \). But if the starting point is not sufficiently close, then the algorithm can display quite erratic behavior and the assumptions that led to the conclusion about quadratic convergence are no longer valid.&lt;/p&gt;
&lt;h3&gt;The Importance of the Magnitude of \( f'(x) \)&lt;/h3&gt;
&lt;p&gt;The preceding discussion demonstrates that the choice of starting point is of great importance. Is there some way to identify a good or bad starting point?&lt;/p&gt;
&lt;p&gt;The above figure shows that when the slope of the tangent line is small, the next guess is relatively far away from the current position. Oscillations are more likely to occur when this happens, especially when local extrema are between the trial position and the root. &lt;/p&gt;
&lt;p&gt;Conversely, when the slope of the tangent line is large, the next guess is relatively close to the current position. This is what happened with an initial guess of 1.5 &lt;a href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#nr-construction"&gt;as seen here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We can see this behavior by rewriting the equation for the next guess \( i + 1 \) as:&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
  x_{i+1} &amp;amp;=&amp;amp; x_i - \frac{f(x)}{f'(x)} \\
  x_{i+1} - x_i &amp;amp;=&amp;amp; - \frac{f(x)}{f'(x)} \\
  \Delta x_i &amp;amp;=&amp;amp; - \frac{f(x)}{f'(x)}
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;So two quantities determine the magnitude of the step. Large step sizes occur when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the residual function \( f(x) \) is large, and&lt;/li&gt;
&lt;li&gt;the magnitude of \( f'(x) \) is small.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The magnitude of \( f'(x) \) is therefore an indicator of the likelihood of convergence problems. In the extreme case of \( f'(x) = 0 \), the NR algorithm will never converge because of a division by zero in the above equation.&lt;/p&gt;
&lt;h2&gt;Ray-Surface Intersections&lt;/h2&gt;
&lt;p&gt;The central problem in ray tracers is to find the 3D intersection of a ray with a surface. The problem has analytical solutions when the surface is planar or spherical, though care must be taken to avoid numerical artifacts such as &lt;a href="https://en.wikipedia.org/wiki/Catastrophic_cancellation"&gt;catastrophic cancellation&lt;/a&gt; in their solutions&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;When a surface is not flat or spherical, however, we turn to numerical routines such as NR. One early paper describing the approach was from &lt;a href="https://doi.org/10.1364/JOSA.52.000672"&gt;Spencer and Murty in 1962&lt;/a&gt;. Spencer and Murty were particularly interested in tracing rays through systems containing general surface shapes like conic section surfaces, aspheres, cylinders, and toroids. They were also interested in an algorithm that would easily accommodate new surface types.&lt;/p&gt;
&lt;h3&gt;Ray Parameterization&lt;/h3&gt;
&lt;p&gt;Regardless of whether you use an analytical or numerical solution, you usually approach the problem by first expressing ray propagation in parametric form. I illustrate this construction below:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_parametric_ray.png"&gt;&lt;/p&gt;
&lt;p&gt;A ray is defined by two, 3D vectors \( \vec{p} \) and \( \hat{d} \). The position vector \( \vec{p} \) points to &lt;em&gt;any&lt;/em&gt; point on the ray. \( \hat{d} \) is a vector of unit magnitude whose elements are the direction cosines of the ray. The parameter \( s \) denotes the distance along the ray from the point \( \vec{p} \) so that the set of all points on the ray is expressed as&lt;/p&gt;
&lt;p&gt;$$ \vec{r}(s) = \vec{p} + s \hat{d}. $$&lt;/p&gt;
&lt;p&gt;When \( s = 0 \), we are at the point \( \vec{p} \) on the ray. Increasing \( s \) moves us in the direction of the ray; decreasing it moves in the opposite direction.&lt;/p&gt;
&lt;h3&gt;Surface Representations&lt;/h3&gt;
&lt;p&gt;An &lt;strong&gt;implicit representation&lt;/strong&gt; of a surface in 3D is&lt;/p&gt;
&lt;p&gt;$$ F ( x, y, z) = 0. $$&lt;/p&gt;
&lt;p&gt;Seen this way, a surface is the zero level set of a 3D scalar function.&lt;/p&gt;
&lt;p&gt;A more useful representation for optical design is to place a single vertex or point of the surface at the origin and let the \( z \) axis represent the optical axis. Let the so-called &lt;a href="https://en.wikipedia.org/wiki/Sagitta_(optics)"&gt;surface sag&lt;/a&gt;, or \( sag(x, y) \), represent the distance from the \( z = 0 \) plane to the surface for all points \( x, y \) within the aperture of the surface&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_surface_sag.png"&gt;&lt;/p&gt;
&lt;p&gt;We can now rewrite \( F \) as&lt;/p&gt;
&lt;p&gt;$$ F(x, y, z) = z - \text{sag}(x, y) = 0 $$.&lt;/p&gt;
&lt;h4&gt;Saggita for Rotationally Symmetric Conic Section Surfaces&lt;/h4&gt;
&lt;p&gt;The most common surface types used in optical design are&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;flat surfaces, and&lt;/li&gt;
&lt;li&gt;rotationally symmetric conic section surfaces, also known as quadrics of rotation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The surface sag of a flat surface is zero everywhere in the local coordinate system of the surface, which by my definition is the \( z=0 \) plane.&lt;/p&gt;
&lt;p&gt;A conic section surface is a surface whose intersection with a plane is a conic section curve, i.e. a circle, parabola, hyperbola, or ellipse. The surface sag of a rotationally symmetric conic section surface with a vertex at the origin and oriented along the \( z \) direction is&lt;/p&gt;
&lt;p&gt;$$ \text{sag}(r) = \frac{r^2 C}{1 + \sqrt{1 - (1 + K) C^2 r^2}} $$&lt;/p&gt;
&lt;p&gt;where \( r = \sqrt{x^2 + y^2} \) is the radial distance from the origin and \( C \) is the curvature of the surface&lt;sup id="fnref:4"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:4"&gt;4&lt;/a&gt;&lt;/sup&gt;. It is expressed in terms of curvature and not radius of curvature \( R \) to avoid numeric difficulties with flat surfaces where \( R = \pm \infty \).&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://en.wikipedia.org/wiki/Conic_constant"&gt;conic constant&lt;/a&gt; \( K \) determines the conic's type. The types are defined by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hyperbola : \( K &amp;lt; -1 \)&lt;/li&gt;
&lt;li&gt;Parabola : \( K = -1 \)&lt;/li&gt;
&lt;li&gt;Ellipse : \( K &amp;gt; -1 \)&lt;/li&gt;
&lt;li&gt;Circle (special case of an ellipse): \( K = 0 \)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The implicit surface representation for a conic section surface is&lt;/p&gt;
&lt;p&gt;$$ F (x, y, z) = z - \frac{r^2 C}{1 + \sqrt{1 - (1 + K) C^2 r^2}} = 0 $$.&lt;/p&gt;
&lt;h4&gt;Partial Derivatives of Rotationally Symmetric Conic Section Surfaces&lt;/h4&gt;
&lt;p&gt;The last bit of information that I need to calculate ray intersections with conic section surfaces are their partial derivatives. I got these by hand by computing them in polar coordinates and converting them back to Cartesian coordinates using the chain rule. The results are&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
\frac{\partial F}{ \partial x} &amp;amp;=&amp;amp; \frac{-x C}{\sqrt{1 - (1 + K) C^2 (x^2 + y^2)}} \\
\frac{\partial F}{ \partial y} &amp;amp;=&amp;amp; \frac{-y C}{\sqrt{1 - (1 + K) C^2 (x^2 + y^2)}} \\
\frac{\partial F}{ \partial z} &amp;amp;=&amp;amp; 1
\end{eqnarray}$$&lt;/p&gt;
&lt;h3&gt;Newton-Raphson for Ray-Surface Intersections&lt;/h3&gt;
&lt;p&gt;The NR algorithm for computing ray intersections with general surfaces is&lt;/p&gt;
&lt;p&gt;$$s_{i+1} = s_i - \frac{F(x,y,z)}{\nabla F (x, y, z) \cdot \hat{d}}.$$&lt;/p&gt;
&lt;p&gt;I think most notable is that the denominator has been replaced with the &lt;em&gt;directional derivative&lt;/em&gt; of the surface's equation along the direction of the ray's propagation. Another thing worth noting is that \(x\), \(y\), and \(z\) are constrained to lie on the ray by writing \(x = p_x + sl \) and so on for the other two quantities&lt;sup id="fnref:5"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:5"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;At this point I'm at last able to understand where problems in the Newton-Raphson algorithm for ray tracing arise. Remember that oscillations and non-convergence often occur when the derivative of the residual is small or there are local extrema between the starting point and the actual root. In ray tracing, the derivative is expressed as \( \nabla F (x, y, z) \cdot \hat{d} \). This can become small when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A ray is traveling nearly parallel to the surface at a point \( x, y \).&lt;/li&gt;
&lt;li&gt;The gradient of \( F \) is small.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Geometrical Interpretation of Newton-Raphson Failures&lt;/h4&gt;
&lt;p&gt;I think the small gradient of \( F \) is more easily understood geometrically. To see this, consider that the \( \nabla F \) is parallel to the surface normal vector at all points on the surface. I can write this as a product of the magnitude of the normal vector and a unit vector pointing in its direction:&lt;/p&gt;
&lt;p&gt;$$ \nabla F = |\eta| \hat{\eta}. $$&lt;/p&gt;
&lt;p&gt;Now the denominator in the NR update equation is the dot product of the above expression with the direction of the ray, or \( |\eta| \hat{d} \cdot \hat{\eta} \). But both \( \hat{d} \) and \( \hat{\eta} \) are unit vectors, so I can replace their dot product with the cosine of the angle \( \alpha \) between them:&lt;/p&gt;
&lt;p&gt;$$ \nabla F \cdot \hat{d} = |\eta| \cos \alpha. $$&lt;/p&gt;
&lt;p&gt;So the directional derivative becomes small for large angles of incidence and small normal vectors.&lt;/p&gt;
&lt;h2&gt;Normal Vectors and Surface Representations&lt;/h2&gt;
&lt;p&gt;There are two parts of the gradient that can make the denominator in the Newton-Raphson update equation small:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The magnitude of the normal vector \( | \eta |\)&lt;/li&gt;
&lt;li&gt;The angle between the ray direction cosine vector and the unit normal vector \(\hat{d} \cdot \hat{\eta} = \cos \alpha \)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To get a sense of the magnitude of the normal vector, consider the plot of the gradient of \( F \) as a function of radial distance from the vertex of the curved surface of a \(f = 50 \, mm\), 1" diameter, spherical, convexplano lens. The radius of curvature of this surface is 25.8 mm.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_normal_convexplano.png"&gt;&lt;/p&gt;
&lt;p&gt;Compare this to the same plot but for the first surface of the scan lens from the beginning of this post, whose radius of curvature is -2.2136 mm.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_normal_scan_lens.png"&gt;&lt;/p&gt;
&lt;p&gt;In neither case is the gradient very small, and we are in some sense rescued by the fact that \( \frac{\partial F}{\partial x} \) is 1 everywhere.&lt;/p&gt;
&lt;p&gt;But wait. Shouldn't the normal vector of a spherical surface be a vector of constant magnitude and perpendicular to the surface everywhere? I expected this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_normal_sphere_symmetric.png"&gt;&lt;/p&gt;
&lt;p&gt;But got this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_normal_sphere_saggita.png"&gt;&lt;/p&gt;
&lt;p&gt;So the magnitude of the normal vector varies with distance from the z-axis&lt;sup id="fnref:6"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:6"&gt;6&lt;/a&gt;&lt;/sup&gt;. And though it's hard to see in these plots, the "normal vectors" are not normal to the surface except at \( x = y = 0 \)!&lt;/p&gt;
&lt;p&gt;I was really disturbed by this at first. As it turns out, this is due to representing the surface by its sag, which is effectively a height field above the xy plane. If instead I had used the sphere's symmetric implicit form \(F_s = x^2 + y^2 + (z - R)^2 - R^2 = 0 \) then I would have obtained a normal vector whose magnitude was constant everywhere on the sphere. This is because&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
\frac{\partial F_s}{ \partial x} &amp;amp;=&amp;amp; 2x \\
\frac{\partial F_s}{ \partial y} &amp;amp;=&amp;amp; 2y \\
\frac{\partial F_s}{ \partial z} &amp;amp;=&amp;amp; 2(z - R).
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;In other words, representing the sphere as a height field has the effect of breaking spherical symmetry with respect to its normal vector&lt;sup id="fnref:7"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fn:7"&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;All of this aside, the magnitude of the normal vector doesn't really become that large in the scan lens example, so the cause of the Newton-Raphson failure is likely coming from near-grazing incidence rays where \(\cos \alpha \approx 0 \).&lt;/p&gt;
&lt;h2&gt;Back to Debugging&lt;/h2&gt;
&lt;p&gt;At this point I wanted to confirm that the problematic rays were at near-grazing incidences, so I turned back to the code. Here is a trace of the first five NR iterations of one particular ray that fails to converge at the first surface of the lens:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="go"&gt; 2026-04-09T07:25:02.768031Z TRACE cherry_rs::views::ray_trace_3d::rays: intersect_init, pos_x: 3.0616169978683836e-17, pos_y: 0.5, pos_z: -5.0, dir_l: 2.094269368838496e-17, dir_m: 0.3420201433256687, dir_n: 0.9396926207859084, s_init: 5.320888862379561&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:69&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::rays::intersect&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::trace::trace_ray with ray_id: 8, surface_id: 2&lt;/span&gt;

&lt;span class="go"&gt;  2026-04-09T07:25:02.768093Z TRACE cherry_rs::views::ray_trace_3d::rays: newton-raphson iteration data, ctr: 0, s: 4.775443550511577, s_1: 0.0, p_x: 8.633304277606096e-17, p_y: 1.4099255856655057, p_z: -2.5, sag: -0.5071021819862234, residual: -1.9928978180137766, denom: 0.94226&lt;/span&gt;
&lt;span class="go"&gt;88642314074&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:125&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::rays::intersect&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::trace::trace_ray with ray_id: 8, surface_id: 2&lt;/span&gt;

&lt;span class="go"&gt;  2026-04-09T07:25:02.768118Z TRACE cherry_rs::views::ray_trace_3d::rays: newton-raphson iteration data, ctr: 1, s: 2.8626356840250526, s_1: 4.775443550511577, p_x: 1.306268214832213e-16, p_y: 2.1332978875896096, p_z: -0.5125509346046124, sag: -1.6227826993006131, residual: 1.110&lt;/span&gt;
&lt;span class="go"&gt;2317646960007, denom: 0.5804199073769454&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:125&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::rays::intersect&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::trace::trace_ray with ray_id: 8, surface_id: 2&lt;/span&gt;

&lt;span class="go"&gt;  2026-04-09T07:25:02.768148Z TRACE cherry_rs::views::ray_trace_3d::rays: newton-raphson iteration data, ctr: 2, s: 4.7418998688939, s_1: 2.8626356840250526, p_x: 9.056747225066086e-17, p_y: 1.479079066939422, p_z: -2.3100023717232365, sag: -0.5666786072973584, residual: -1.74332&lt;/span&gt;
&lt;span class="go"&gt;3764425878, denom: 0.9276629536509492&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:125&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::rays::intersect&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::trace::trace_ray with ray_id: 8, surface_id: 2&lt;/span&gt;

&lt;span class="go"&gt;  2026-04-09T07:25:02.768181Z TRACE cherry_rs::views::ray_trace_3d::rays: newton-raphson iteration data, ctr: 3, s: 2.997895475725084, s_1: 4.7418998688939, p_x: 1.299243264339216e-16, p_y: 2.1218252727950615, p_z: -0.5440716846947353, sag: -1.5828207424715346, residual: 1.038749&lt;/span&gt;
&lt;span class="go"&gt;0577767993, denom: 0.5956114914879407&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:125&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::rays::intersect&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::trace::trace_ray with ray_id: 8, surface_id: 2&lt;/span&gt;

&lt;span class="go"&gt;  2026-04-09T07:25:02.768222Z TRACE cherry_rs::views::ray_trace_3d::rays: newton-raphson iteration data, ctr: 4, s: 4.714415826981489, s_1: 2.997895475725084, p_x: 9.340017663658938e-17, p_y: 1.525340640282867, p_z: -2.182899743573678, sag: -0.6094301551576737, residual: -1.57346&lt;/span&gt;
&lt;span class="go"&gt;95884160044, denom: 0.9166623554823017&lt;/span&gt;
&lt;span class="go"&gt;    at cherry-rs/src/views/ray_trace_3d/rays.rs:125&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::rays::intersect&lt;/span&gt;
&lt;span class="go"&gt;    in cherry_rs::views::ray_trace_3d::trace::trace_ray with ray_id: 8, surface_id: 2&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In table form:&lt;/p&gt;
&lt;table border="1"&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;ctr&lt;/th&gt;
      &lt;th&gt;s (after step)&lt;/th&gt;
      &lt;th&gt;residual&lt;/th&gt;
      &lt;th&gt;denominator&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;4.775&lt;/td&gt;
      &lt;td&gt;-1.993&lt;/td&gt;
      &lt;td&gt;0.942&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2.863&lt;/td&gt;
      &lt;td&gt;1.110&lt;/td&gt;
      &lt;td&gt;0.580&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;4.742&lt;/td&gt;
      &lt;td&gt;-1.743&lt;/td&gt;
      &lt;td&gt;0.928&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;2.998&lt;/td&gt;
      &lt;td&gt;1.039&lt;/td&gt;
      &lt;td&gt;0.596&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;4.714&lt;/td&gt;
      &lt;td&gt;-1.573&lt;/td&gt;
      &lt;td&gt;0.917&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The denominator is not anywhere near small enough to indicate that the problem is caused by near-grazing incidence angles, so there must be something else going on.&lt;/p&gt;
&lt;p&gt;The first thing to note is that the residual \(z - \text{sag} (x, y) \) is oscillating in sign which indicates that the algorithm is hopping back and forth between different sides of the surface. The root estimate &lt;code&gt;s&lt;/code&gt; appears to slowly be converging to some value but hasn't yet done so. In fact, &lt;strong&gt;it took 100,000 iterations to converge to within 0.0004 of the root, which was somewhere around &lt;code&gt;s=4.14&lt;/code&gt;&lt;/strong&gt;. This rate of convergence is much too slow. What could be causing it?&lt;/p&gt;
&lt;p&gt;I plotted the residual function and it didn't seem too bad:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_residual_ray_id_8.png"&gt;&lt;/p&gt;
&lt;p&gt;I then plotted the NR steps for this particular ray and found that I could not reproduce the oscillations. In fact, NR converged quite rapidly.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://kylemdouglass.com/images/newton_raphson_residual_convergence_ray_id_8.png"&gt;&lt;/p&gt;
&lt;p&gt;So my two different implementations did not agree. After about 2 hours of digging I found the problem: I was normalizing the normal vector to 1 in the Rust code rather than retaining its magnitude. &lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// 💣💣💣&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vec3&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dfdx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dfdy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dfdz&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// 💣💣💣&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This has a subtle effect on the value of the NR denominator such that the step size isn't quite what it should be.&lt;/p&gt;
&lt;p&gt;I can't begin to explain to you how subtle this bug was. I nearly face-palmed by head off when I found it. It was in code that I wrote &lt;em&gt;nearly three years ago.&lt;/em&gt; Smart people can do really dumb things with computers.&lt;/p&gt;
&lt;h2&gt;Discussion&lt;/h2&gt;
&lt;p&gt;I am actually quite happy to have had to solve this bug even if there was no deeper, numerical reason behind it. It forced me to do a deep dive into the Newton-Raphson algorithm and I feel much more knowledgable as a result. Still, it's frustrating because I clearly was experimenting in the early days, and I wonder whether some other careless coding choices still await to be discovered.&lt;/p&gt;
&lt;p&gt;I only briefly mentioned it, but in this journey I also implemented a fallback to a bisection method when the initial NR guess fails due to a negative discriminant in the sag function of a conic section surface. I think this is a win because rays that would have initially failed can be recovered by the fallback routine. But what's more, both of these changes led to some impressive improvements in the benchmark tests: the convexplano lens example runs 43% faster because of faster NR convergence and fewer early ray terminations.&lt;/p&gt;
&lt;p&gt;Out of curiosity I looked into what &lt;a href="https://github.com/optiland/optiland"&gt;Optiland&lt;/a&gt; does to compute Ray-Surface intersections. I believe that it analytically computes intersections for flat and spherical surfaces and falls back to NR when things get more complicated. I use NR for everything, and to be honest, I'm happy with this approach so far. The ray-surface interection function in my ray tracer is a single long function, but it reads linearly and is very clear about what it does. If I were to add if/else branches to check for surface types (or, more properly, employ polymorphism to make the intersection logic a Surface-level method), then the logic would diffuse throughout the codebase. &lt;a href="http://number-none.com/blow/john_carmack_on_inlined_code.html"&gt;An essay by John Carmack on inlining code&lt;/a&gt; that I read a couple years ago had a profound effect on me when it comes to mission-critical, high performance sections of code. Ray-surface intersection logic is one such example where "good" software engineering practices are counter-productive, and just inlining the whole damn thing makes a lot of sense.&lt;/p&gt;
&lt;p&gt;The real lesson here is that it always pays to really understand what your algorithms are doing, and having the proper tooling in place for debugging pays off enormously.&lt;/p&gt;
&lt;p&gt;Happy ray tracing.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Spans remind me a lot of &lt;a href="https://sentry.io"&gt;Sentry&lt;/a&gt;, which I used to perform tracing on distributed code bases when I worked for a photogrammetry company doing image processing on the Cloud. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;See for example Chapter 7 of &lt;a href="https://www.realtimerendering.com/raytracinggems/rtg/index.html"&gt;Ray Tracing Gems&lt;/a&gt;. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;When I first learned this term, I thought the name came from the idea that the surface "sags" away from the \(z=0\) plane. As it turns out, it's short for &lt;em&gt;sagitta&lt;/em&gt;, the Latin word for arrow. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Surface curvature is related to the radius of curvature \( R \) as \(C = 1 / R \). &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:4" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;\(l^2 + m^2 + n^2 = 1 \) are the direction cosines of the ray. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:5" title="Jump back to footnote 5 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;I don't show this, but for a lens with positive curvature, the normal vectors point to the left in these plots for the symmetric implicit representation. In other words, it always points outwards from the sphere's center in the symmetric implicit representation, but in the +z direction in the saggital representation. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:6" title="Jump back to footnote 6 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;There is a name for this height field representation in the theory of surfaces; it's called the &lt;a href="https://en.wikipedia.org/wiki/Monge_patch"&gt;Monge patch&lt;/a&gt;. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/#fnref:7" title="Jump back to footnote 7 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>numerical methods</category><category>ray tracing</category><guid>https://kylemdouglass.com/posts/ray-surface-intersections-with-the-newton-raphson-algorithm/</guid><pubDate>Thu, 09 Apr 2026 18:26:02 GMT</pubDate></item><item><title>Completing the Square and the Normal Form of Quadrics</title><link>https://kylemdouglass.com/posts/completing-the-square-and-normal-form-quadrics/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;I am working on rendering cross section views of optical systems for my ray tracer. The problem is one of finding the intersection curve between a plane (the cutting plane) and a &lt;a href="https://en.wikipedia.org/wiki/Quadric"&gt;quadric surface&lt;/a&gt; which represents an interface between two media with different refractive indexes. Quadric surfaces are important primitives for modeling optical interfaces because they represent common surface types in optics, such as spheroids and paraboloids. A pair of quadrics, or a quadric and a plane, models a common lens.&lt;/p&gt;
&lt;p&gt;In 3D, the implicit surface equation for a quadric is&lt;/p&gt;
&lt;p&gt;$$
A x^2 + B y^2 + C z^2 + D x y + E y z + F x z + G x + H y + I z + J = 0
$$&lt;/p&gt;
&lt;p&gt;Any quadric can be reduced to a so-called &lt;a href="https://en.wikipedia.org/wiki/Quadric#Euclidean_space"&gt;normal form&lt;/a&gt; that identifies its class, i.e. ellipsoid, hyperbolic paraboloid, etc. Except for paraboloids, none of the normal form equations contain linear terms in \( x \), \( y \), or \( z \).&lt;/p&gt;
&lt;p&gt;A quadric of revolution occurs when two or more of the parameters of the the quadric's normal form are equal, such as \( x^2 / R^2 + y^2 / R^2 + z^2 / R^2 = 1 \), which is the equation for a spheroid with radius parameter \( R \)&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/completing-the-square-and-normal-form-quadrics/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;. Quadrics of revolution are the surface types most-often encountered in optics&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/completing-the-square-and-normal-form-quadrics/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The surface sag of a quadric surface is a very important quantity for ray tracing. The sag of a quadric is usually given in terms of the &lt;a href="https://en.wikipedia.org/wiki/Conic_constant"&gt;conic constant&lt;/a&gt;, \( K \). One obtains the sag by solving the following quadric equation for \( z \):&lt;/p&gt;
&lt;p&gt;$$
x^2 + y^2 - 2 R z + ( K + 1 ) z^2 = 0
$$&lt;/p&gt;
&lt;p&gt;Here, \( R \) is the radius of curvature of the surface at its apex, \( x = y = z = 0 \).&lt;/p&gt;
&lt;p&gt;At this point I asked myself how I could rewrite the above expression in its normal form, and for a while I was unable to do it. After a bit of searching on the internet, I eventually realized that the solution involves &lt;a href="https://en.wikipedia.org/wiki/Completing_the_square"&gt;completing the square&lt;/a&gt;, a topic that was not given much attention during my high school education. After this exercise, I realize now that the purpose of completing the square is to essentially &lt;strong&gt;move any linear terms of a quadratic equation into squared parantheses&lt;/strong&gt;. This allows one to then remove the linear terms entirely by applying a suitable transformation, leaving only quadratic and constant terms.&lt;/p&gt;
&lt;h2&gt;Converting the Quadric to its Normal Form&lt;/h2&gt;
&lt;p&gt;The conversion of the above equation proceeds as follows. We first factor out \( ( K + 1 ) \) from the terms involving \( z \).&lt;/p&gt;
&lt;p&gt;$$
x^2 + y^2 + ( K + 1 ) \left[ z^2 - \frac{ 2 R z }{ K + 1 }\right] = 0
$$&lt;/p&gt;
&lt;p&gt;Next, we "add zero" to the term inside the square brackets by adding \( [ 2 R / 2 ( K + 1 ) ]^2 - [ 2 R / 2 ( K + 1 ) ]^2 = [ R / ( K + 1 ) ]^2 - [ R / ( K + 1 ) ]^2 \):&lt;/p&gt;
&lt;p&gt;$$
x^2 + y^2 + ( K + 1 ) \left[ z^2 - \frac{ 2 R z }{ K + 1 } + \left( \frac{ R }{ K + 1 } \right)^2 - \left( \frac{ R }{ K + 1 } \right)^2 \right] = 0
$$&lt;/p&gt;
&lt;p&gt;We can understand this a bit more generally by considering the expression \( z^2 - a z \). Here I need to add and subtract \( ( a / 2 )^2 \). The reason is that now we can rewrite the first three terms inside the square brackets as a squared binomial:&lt;/p&gt;
&lt;p&gt;$$
x^2 + y^2 + ( K + 1 ) \left[ \left( z - \frac{ R }{ K + 1 } \right)^2 - \left( \frac{ R } { K + 1 } \right)^2 \right] = 0
$$&lt;/p&gt;
&lt;p&gt;These last two steps complete the square. To place the equation into its normal form, I apply the Euclidean transformation \( z' = z - \frac{ R }{ K + 1 } \) and carry through the \( K + 1 \).&lt;/p&gt;
&lt;p&gt;$$
x^2 + y^2 + ( K + 1 ) z^{ \prime 2 } - R^2 / ( K + 1 ) = 0
$$&lt;/p&gt;
&lt;p&gt;The above equation is &lt;em&gt;almost&lt;/em&gt; a normal form expression for a quadric. To finish the job, I would need to substiute in a specific value for the conic constant and divide through so that the constant is either -1, 0, or 1.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;The coefficients of each term need not be equal in general. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/completing-the-square-and-normal-form-quadrics/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;A cylindrical lens does actually contain a quadric, but rather would consist of at least one toroidal surface. These are less common than lenses with spherical profiles, however. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/completing-the-square-and-normal-form-quadrics/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>algebra</category><category>algebraic geometry</category><category>mathematics</category><category>ray tracing</category><guid>https://kylemdouglass.com/posts/completing-the-square-and-normal-form-quadrics/</guid><pubDate>Mon, 07 Jul 2025 06:42:35 GMT</pubDate></item><item><title>Why is Camera Read Noise Gaussian Distributed?</title><link>https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;As a microscopist I work with very weak light signals, often just tens of photons per camera pixel. The images I record are noisy as a result&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;. To a good approximation, the value of a pixel is a sum of two random variables describing two different physical processes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;photon shot noise, which is described by a Poisson probability mass function, and&lt;/li&gt;
&lt;li&gt;camera read noise, which is described by a Gaussian probability density function.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Read noise has units of electrons, which must be discrete, positive integers. So why is it modeled as a continuous probability density function&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;?&lt;/p&gt;
&lt;h2&gt;The Source(s) of Read Noise&lt;/h2&gt;
&lt;p&gt;Janesick&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt; defines read noise as "any noise source that is not a function of signal." This means that there is not necessarily one single source of read noise. It is commonly understood that it comes from somewhere in the camera electronics, but "somewhere" need not imply that it is isolated to one location.&lt;/p&gt;
&lt;p&gt;The signal from a camera pixel is the number of photoelectrons that were generated inside the pixel. I imagine readout of this signal as a linear path consisting of many steps. The signal might change form along this path, such as going from number of electrons to a voltage. At each step, there is a small probability that some small error is added to (or maybe also removed from?) the signal. The final result is a value that differs randomly from the original signal.&lt;/p&gt;
&lt;p&gt;Importantly, I do not think that it matters which physical process each step actually represents; rather there just has to be many of them for this abstraction to be valid.&lt;/p&gt;
&lt;p&gt;"But aren't there only a handful of steps?" you might ask. After all, linear models of photon transfer typically consist of a few processes such as detection, amplification, readout, and analog-to-digital conversion. I am not referring to these when I use the term "step." Rather, I am referring to processes that are much more microscopic, such as passage of a signal through a transistor or amplifier chip. At the very least Johnson noise, or random currents induced by thermal motion of the charge carriers, will be present in all of the camera's components.&lt;/p&gt;
&lt;h2&gt;Read Noise is Gaussian because of the Central Limit Theorem&lt;/h2&gt;
&lt;p&gt;The reason for my conclusion that I can ignore the details so long as there are many steps is the following:&lt;/p&gt;
&lt;p&gt;I can model the error introduced by each step as a random variable. Let's assume that each step is independent of the others. The result of camera readout is a sum of a large number of independent random variables. And of course the &lt;a href="https://en.wikipedia.org/wiki/Central_limit_theorem"&gt;Central Limit Theorem&lt;/a&gt; states that the distribution of the sum of random variables tends towards a normal distribution, i.e. Gaussian, as the number of random variables tends towards infinity. This happens regardless of the distributions of the underlying random variables.&lt;/p&gt;
&lt;p&gt;So read noise can appear to be effectively Gaussian so long as there are many steps along the path of conversion from photoelectrons to pixel values and each step has a chance of introducing an error.&lt;/p&gt;
&lt;h3&gt;Sums of Discrete Random Variables&lt;/h3&gt;
&lt;p&gt;I encountered one conceptual difficulty here: the sum of discrete random variables is still discrete. If I have several variables that produce only integers, their sum is still an integer. I cannot get, say, 3.14159 as a result. Does the Gaussian approximation, which is for continuous random variables, still apply in this case?&lt;/p&gt;
&lt;p&gt;This question is relevant because the signal in a camera is transformed between discrete a continuous representations at least twice: from electrons to voltage and from voltage to analog-to-digital units (ADUs).&lt;/p&gt;
&lt;p&gt;Let's say that I have a discrete random variable that can assume values of 0 or 1, and the probability that the value is 1 is denoted \( p \). This is known as a Bernoulli trial. Now let's say that I have a large number \( n \) of Bernoulli trials. But the sum of \( n \) Bernoulli trials has a distribution that is binomial, and this is well-known to be approximated as a Gaussian when certain conditions are met, including large \( n \)&lt;sup id="fnref:4"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fn:4"&gt;4&lt;/a&gt;&lt;/sup&gt;. So a sum of a large number of discrete random variables can have a probability distribution function that is approximated as a Gaussian.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This does not mean that the sum of discrete random variables can take on continuous values.&lt;/strong&gt; Rather, the probability associated with any one output value can be estimated by a Gaussian probability density function.&lt;/p&gt;
&lt;p&gt;But how exactly can I use a continuous distribution to approximate a discrete one? After all, if the random variable \( Y \) is a continuous, Gaussian random variable, then \(P (Y = a)  = 0 \) for all values of \( a \). To get a non-zero probability from a probability density function, I need to integrate it over some interval of its domain. I can therefore integrate the Gaussian in a small interval around each possible value of the discrete random variable, and then associate this integrated area with the probability of the obtaining that discrete value. This is called a &lt;a href="https://en.wikipedia.org/wiki/Continuity_correction"&gt;continuity correction&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Example of a Continuity Correction&lt;/h4&gt;
&lt;p&gt;As a very simple example, consider a discrete random variable \( X \) that is approximated by a Gaussian continuous random variable \( Y \). The probability of getting a discrete value 5 is \( P (X = 5) \). The Gaussian approximation is \( P ( 4.5 \lt Y \lt 5.5 ) \), i.e. I integrate the Gaussian from 4.5 to 5.5 to compute the approximate probability of getting the discrete value 5.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I wrote a blog post about this a while back: &lt;a href="https://kmdouglass.github.io/posts/modeling-noise-for-image-simulations/"&gt;https://kmdouglass.github.io/posts/modeling-noise-for-image-simulations/&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;This is often asserted without justification. See for example Janesick, Photon Transfer, page 34. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://doi.org/10.1117/3.725073"&gt;https://doi.org/10.1117/3.725073&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Binomial_distribution#Normal_approximation"&gt;https://en.wikipedia.org/wiki/Binomial_distribution#Normal_approximation&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/#fnref:4" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>cameras</category><category>microscopy</category><category>statistics</category><guid>https://kylemdouglass.com/posts/why-is-camera-read-noise-gaussian-distributed/</guid><pubDate>Thu, 19 Jun 2025 08:40:12 GMT</pubDate></item><item><title>A Very Brief Summary of The Analytic Signal in Fourier Optics</title><link>https://kylemdouglass.com/posts/a-very-brief-summary-of-the-analytic-signal-in-fourier-optics/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;h2&gt;The Analytic Signal Representation of a Monochromatic Wave&lt;/h2&gt;
&lt;h3&gt;Monochromatic Scalar Waves&lt;/h3&gt;
&lt;p&gt;A monochromatic, scalar waveform is described by the expression:&lt;/p&gt;
&lt;p&gt;$$ u \left( \mathbf{r}, t\right) = A ( \mathbf{r} ) \cos \left[2 \pi f_0 t + \phi \left( \mathbf{r} \right) \right] $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The signal is real-valued&lt;/li&gt;
&lt;li&gt;The signal has a known phase for all \( t \)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Analytic Signal&lt;/h3&gt;
&lt;p&gt;An analytic signal is a generalization of a phasor. It is used to represent a real-valued signal as a complex exponential or a sum of complex exponentials. When Goodman&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/a-very-brief-summary-of-the-analytic-signal-in-fourier-optics/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; refers to a phasor, he often means the analytic signal. This is made clear in Chapter 6 where he describes the construction of the phasor for a narrowband signal as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;compute its Fourier transform&lt;/li&gt;
&lt;li&gt;set the positive frequency components to zero&lt;/li&gt;
&lt;li&gt;double the amplitudes of the negative frequency components&lt;/li&gt;
&lt;li&gt;inverse Fourier transform the resulting one-sided spectrum&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Strictly speaking, the analytic signal is obtained by setting the negative frequencies to zero and doubling by application of the Hilbert transform. However, many engineering fields have adopted the convention of setting the positive frequencies to zero instead. The results will be the same, except that the direction of power flow will be reversed (if I recall correctly).&lt;/p&gt;
&lt;p&gt;The Fourier transform of \( u \left( \mathbf{r}, t\right) \) is:&lt;/p&gt;
&lt;p&gt;$$ \mathcal{F} \left\{ u \left( \mathbf{r}, t\right) \right\} = \frac{A ( \mathbf{r} ) }{2} \left[ e^{j \phi \left( \mathbf{r} \right) } \delta \left( f - f_0 \right) + e^{-j \phi \left( \mathbf{r} \right) } \delta \left( f + f_0 \right) \right] $$&lt;/p&gt;
&lt;p&gt;Drop the positive frequency term \( e^{j \phi \left( \mathbf{r} \right) } \delta \left( f - f_0 \right) \) and double the result. This produces:&lt;/p&gt;
&lt;p&gt;$$ A ( \mathbf{r} ) e^{-j \phi \left( \mathbf{r} \right) } \delta \left( f + f_0 \right) $$&lt;/p&gt;
&lt;p&gt;Let \( U ( \mathbf{r} ) := A ( \mathbf{r} ) e^{-j \phi \left( \mathbf{r} \right) } \). The inverse Fourier transform of this signal is:&lt;/p&gt;
&lt;p&gt;$$ \mathcal{F}^{-1} \left\{ U ( \mathbf{r} ) \delta \left( f + f_0 \right) \right\} = U ( \mathbf{r} ) e^{-j 2 \pi f_0 t } $$&lt;/p&gt;
&lt;p&gt;We can recover the original field by taking the real part of this expression, which is equivalent to applying Euler's identity and dropping the imaginary part:&lt;/p&gt;
&lt;p&gt;$$ u \left( \mathbf{r}, t \right) = \Re \left[ U ( \mathbf{r} ) e^{-j 2 \pi f t} \right] $$&lt;/p&gt;
&lt;h2&gt;Polychromatic Scalar Waves&lt;/h2&gt;
&lt;p&gt;To model a polychromatic wave, we integrate over the analytic signals of each spectral component and take the real part of the result:&lt;/p&gt;
&lt;p&gt;$$ u \left( \mathbf{r}, t\right) = \Re \left[ \int_{-\infty}^{\infty} \tilde{U} \left( \mathbf{r}, f \right) e^{-j 2 \pi f t} \,df \right] $$&lt;/p&gt;
&lt;h3&gt;The Narrowband Assumption&lt;/h3&gt;
&lt;p&gt;We get a useful representation to the expression above if we assume that the bandwidth of the signal is much smaller than its center frequency \( \Delta f \ll f_0 \):&lt;/p&gt;
&lt;p&gt;$$ \int_{-\infty}^{\infty} \tilde{U} \left( \mathbf{r}, f \right) e^{-j 2 \pi f t} \,df = U \left( \mathbf{r}, t \right) e^{-j 2 \pi f_0 t} $$&lt;/p&gt;
&lt;p&gt;To better understand the meaning of this assumption, make the substitution \( \nu = f - f_0 \) into the expression on the left hand side:&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
\int_{-\infty}^{\infty} \tilde{U} \left( \mathbf{r}, f \right) e^{-j 2 \pi f t} \,df &amp;amp;=&amp;amp; \int_{-\infty}^{\infty} \tilde{U} \left( \mathbf{r}, \nu + f0 \right) e^{-j 2 \pi \left( \nu + f_0 \right) t} \,d\nu \\
&amp;amp;=&amp;amp; e^{-j 2 \pi f_0 t} \int_{-\infty}^{\infty} \tilde{U} \left( \mathbf{r}, \nu + f0 \right) e^{-j 2 \pi \nu t} \,d\nu
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;Under the narrowband assumption, the integration in the expression above is constrained to small values around \( \nu = 0 \) that are much less than the phasor term that is oscillating at frequency \( f_0 \). If we define the following function:&lt;/p&gt;
&lt;p&gt;$$ U \left( \mathbf{r}, t \right) := \int_{-\infty}^{\infty} \tilde{U} \left( \mathbf{r}, \nu + f0 \right) e^{-j 2 \pi \nu t} \,d\nu $$ &lt;/p&gt;
&lt;p&gt;then it will vary slowly with respect to the carrier frequency \( f_0 \).&lt;/p&gt;
&lt;p&gt;As a result, under the assumptions of narrowbandedness, we can interpret the complex function \( U \left( \mathbf{r}, t \right) \) as an "envelope" modulating the amplitude of the fast oscillating carrier wave. If the assumption is not valid, then this interpretation fails.&lt;/p&gt;
&lt;h3&gt;The Slowly Varying Envelope Assumption&lt;/h3&gt;
&lt;p&gt;It is instructive to reverse our reasoning and see why a slowly-varying envelope implies a narrowband signal. Compute the Fourier transforms of the narrowband waveform, along with the Fourier transform of the derivative of \( U \left( \mathbf{r}, t \right) \).&lt;/p&gt;
&lt;p&gt;The Fourier transform of the analytic signal:&lt;/p&gt;
&lt;p&gt;$$ \int_{-\infty}^{\infty} \left[ U \left( \mathbf{r}, t \right) e^{-j 2 \pi f_0 t} \right] e^{-j 2 \pi f t} \,dt = \tilde{U} \left( \mathbf{r}, f + f_0 \right) $$&lt;/p&gt;
&lt;p&gt;The Fourier transform of the derivative of \( U \):&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
\int_{-\infty}^{\infty} \frac{d}{dt} \left[ U \left( \mathbf{r}, t \right) \right] e^{-j 2 \pi f t} \,dt &amp;amp;=&amp;amp; j 2 \pi f \tilde{U} \left( \mathbf{r}, f \right)
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;Now, apply the &lt;a href="https://en.wikipedia.org/wiki/Slowly_varying_envelope_approximation"&gt;slowly varying envelope approximation (SVEA)&lt;/a&gt; by asserting that the rate of change of \( U \) with respect to time is much less than the value of \( U \) multiplied by the center frequency, or \( \left| \frac{d}{dt} U \left( \mathbf{r}, t\right) \right| \ll \left| 2 \pi f_0 U \left( \mathbf{r, t} \right) \right| \)&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
\left| j 2 \pi f \tilde{U} \left( \mathbf{r}, f \right) \right| &amp;amp;=&amp;amp; \left| \int_{-\infty}^{\infty} \frac{d}{dt} \left[ U \left( \mathbf{r}, t \right) \right] e^{-j 2 \pi f t} \,dt \right| \\
&amp;amp;\ll&amp;amp; \left| \int_{-\infty}^{\infty} 2 \pi f_0 U \left( \mathbf{r}, t \right) e^{-j 2 \pi f t} \,dt \right| \\
&amp;amp;\ll&amp;amp; 2 \pi f_0 \left| \tilde{U} \left( \mathbf{r}, f \right) \right|
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;This expression means that the appreciable frequency components of \( U \left( \mathbf{r} , t \right) \) are much less than the frequency \( f_0 \)&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/a-very-brief-summary-of-the-analytic-signal-in-fourier-optics/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;. And when we consider the spectrum of \( U \left( \mathbf{r} , t \right) \) centered around \( f_0 \), we find that the bandwidth \( \Delta f \) is small with respect to \( f_0 \).&lt;/p&gt;
&lt;h3&gt;Assumptions, not Approximations!&lt;/h3&gt;
&lt;p&gt;The narrowband and slowly varying envelope assumptions are usually referred to as approximations. This is misleading! The resulting expression for the field is not an approximation at all; instead, under the assumptions of narrowbandedness, we can interpret the complex function \( U \left( \mathbf{r}, t \right) \) an "envelope" modulating the amplitude of the fast oscillating carrier wave. If the assumption is not valid, then this interpretation is not correct.&lt;/p&gt;
&lt;h3&gt;Narrowband Polychromatic Waves&lt;/h3&gt;
&lt;p&gt;In summary, narrowband polychromatic waves with a center frequency \( f_0 \) are modeled as the product of a fast rotating phasor and slowly varying envelope:&lt;/p&gt;
&lt;p&gt;$$ u \left( \mathbf{r}, t \right) = \Re \left[ U \left( \mathbf{r}, t \right) e^{-j 2 \pi f_0 t} \right] $$&lt;/p&gt;
&lt;p&gt;The amplitude and the phase of the envelope are the amplitude and phase of the real optical wave.&lt;/p&gt;
&lt;h2&gt;Coherence&lt;/h2&gt;
&lt;p&gt;While the expression for the analytic signal \( U \left( \mathbf{r}, t \right) \) as an integral over frequency components appears deterministic, the phase relationships between the spectral components are often unknown and vary randomly in time. As a result, the envelope of the optical wave will vary unpredictably and must be analyzed in terms of its statistical properties.&lt;/p&gt;
&lt;h3&gt;Monochromatic Light is Coherent&lt;/h3&gt;
&lt;p&gt;Since monochromatic light has only one spectral component by definition, it is completely coherent.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I mean monochromatic in the ideal sense, not like how we sometimes describe lasers.&lt;/li&gt;
&lt;li&gt;Monochromatic waves, like plane waves, cannot exist in real life. The uncertainy principle requires that a monochromatic wave exist for an infinite duration.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Goodman, Joseph W. Introduction to Fourier optics. Roberts and Company publishers (2005). ISBN 978-0974707723. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/a-very-brief-summary-of-the-analytic-signal-in-fourier-optics/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://physics.stackexchange.com/questions/451239/slowly-varying-envelope-approximation-what-does-it-imply"&gt;https://physics.stackexchange.com/questions/451239/slowly-varying-envelope-approximation-what-does-it-imply&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/a-very-brief-summary-of-the-analytic-signal-in-fourier-optics/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>analytic signal</category><category>coherence</category><category>fourier optics</category><guid>https://kylemdouglass.com/posts/a-very-brief-summary-of-the-analytic-signal-in-fourier-optics/</guid><pubDate>Tue, 01 Apr 2025 13:53:21 GMT</pubDate></item><item><title>A Very Brief Summary of Fresnel and Fraunhofer Diffraction Integrals</title><link>https://kylemdouglass.com/posts/a-very-brief-summary-of-fresnel-and-fraunhofer-diffraction-integrals/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;Fourier Optics is complicated, and though I have internalized its concepts over the years, I often still need to review the specifics of its mathematical models. Unfortunately, my go-to resource for this, Goodman's Fourier Optics&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/a-very-brief-summary-of-fresnel-and-fraunhofer-diffraction-integrals/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;, tends to disperse information across chapters and homework problems. This makes quick review difficult.&lt;/p&gt;
&lt;p&gt;Here I condense what I think are the essentials of Fresnel and Fraunhofer diffraction into one blog post.&lt;/p&gt;
&lt;h2&gt;Starting Point: the Huygens-Fresnel Principle&lt;/h2&gt;
&lt;p&gt;Ignore Chapter 3 of Goodman; it's largely irrelevant for practical work. The Huygens-Fresnel principle itself is a good intuitive model to start with.&lt;/p&gt;
&lt;h3&gt;The Model&lt;/h3&gt;
&lt;p&gt;An opaque screen with a clear aperture \( \Sigma \)  is located in the \( z = 0 \) plane with transverse coordinates \( \left( \xi , \eta \right ) \). It is illuminated by a complex-valued scalar field \( U \left( \xi, \eta \right) \). Let \( \vec{r_0} = \left( \xi, \eta, 0 \right) \) be a point in the plane of the aperture and \( \vec{r_1} = \left( x, y, z \right) \) be a point in the observation plane. The Huygens-Fresnel Principle provides the following formula for the diffracted field \( U \left( x, y \right) \) in the plane \( z \):&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y; z \right) = \frac{z}{j \lambda} \iint_{\Sigma} U \left( \xi , \eta \right) \frac{\exp \left( j k r_{01} \right)}{r_{01}^2} \, d\xi d\eta $$&lt;/p&gt;
&lt;p&gt;with the distance \( r_{01}^2 = \left( x - \xi \right)^2 + \left( y - \eta \right)^2 + z^2 \).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We assumed an obliquity factor \( cos \, \theta = z / r_{01}\). The choice of obliquity factor depends on the boundary conditions discussed in Chapter 3, but again this isn't terribly important for practical work.&lt;/li&gt;
&lt;li&gt;The integral is a sum over secondary spherical wavelets emitted by each point in the aperture and weighted by the incident field and the obliquity factor.&lt;/li&gt;
&lt;li&gt;The factor \( 1 / j \) means that each secondary wavelet from a point \( \left( \xi, \eta \right) \) is 90 degrees out-of-phase with the incident field at that point.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Approximations used in the Huygens-Fresnel Principle&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;The electromagnetic field can be approximated as a complex-valued scalar field.&lt;/li&gt;
&lt;li&gt;\( r_{01} \gg \lambda \), or the observation screen is many multiples of the wavelength away from the aperture.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Fresnel Diffraction Integral&lt;/h2&gt;
&lt;h3&gt;The Fresnel Approximation&lt;/h3&gt;
&lt;p&gt;Rewrite \( r_{01} \) as:&lt;/p&gt;
&lt;p&gt;$$ r_{01} = z \sqrt{ 1 + \frac{\left( x - \xi \right)^2 + \left( y - \eta \right)^2}{z^2} } $$&lt;/p&gt;
&lt;p&gt;Apply the binomial approximation:&lt;/p&gt;
&lt;p&gt;$$ r_{01} \approx z + \frac{\left( x - \xi \right)^2 + \left( y - \eta \right)^2}{2z} $$&lt;/p&gt;
&lt;p&gt;In the Huygens-Fresnel diffraction integral, replace:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;\(r_{01}^2 \) in the denominator with \( z^2 \)&lt;/li&gt;
&lt;li&gt;\(r_{01}\) in the argument of the exponential with \( z + \frac{\left( x - \xi \right)^2 + \left( y - \eta \right)^2}{2z} \)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Diffraction Integral: Form 1&lt;/h4&gt;
&lt;p&gt;Perform the substitutions for \( r_{01} \) into the Huygens-Fresnel formula that were mentioned above to get the first form of the Fresnel diffraction integral:&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y; z \right) = \frac{ e^{jkz} }{j \lambda z} \iint_{-\infty}^{\infty} U \left( \xi , \eta \right) \exp \left\{ \frac{jk}{2z} \left[ \left( x - \xi \right)^2 + \left( y - \eta \right)^2 \right] \right\}  \,d\xi \,d\eta $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is space invariant, i.e. it depends only on the differences in coordinates \( \left( x - \xi \right) \) and \( \left( y - \eta \right) \).&lt;/li&gt;
&lt;li&gt;It represents a convolution of the input field with the kernel \( h \left( x, y \right) = \frac{e^{j k z}}{j \lambda z} \exp \left[ \frac{j k}{2 z} \left( x^2 + y^2 \right) \right] \).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;The Diffraction Integral: Form 2&lt;/h4&gt;
&lt;p&gt;Expand the squared quantities inside the parantheses of Form 1 to get the second from of the integral:&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y; z \right) = \frac{ e^{jkz} }{j \lambda z} e^{\frac{j k}{2 z} \left( x^2 + y^2 \right)} \iint_{-\infty}^{\infty} \left[ U \left( \xi , \eta \right) e^{\frac{j k}{2 z} \left( \xi^2 + \eta^2 \right)} \right] e^{-j \frac{2 \pi }{\lambda z} \left( x \xi + y \eta \right) }  \,d\xi \,d\eta $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is proportional to the Fourier transform of the product of the incident field and a parabolic phase curvature \( e^{\frac{j k}{2 z} \left( \xi^2 + \eta^2 \right)} \).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Phasor Conventions&lt;/h2&gt;
&lt;p&gt;Section 4.2.1 of Goodman is an interesting practical aside about how to identify whether a spherical or parabolic wavefront is converging or diverging based on the sign of its phasor. It is useful for solving the important homework problem 4.16 which concerns the diffraction pattern from an aperture that is illuminated by a converging spherical wave.&lt;/p&gt;
&lt;p&gt;Unfortunately, Figure 4.2 does not align well with its description in the text about negative z-values, and it's not clear how the interpretations change for point sources not at \( z = 0 \). I address this below.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let the point of convergence (or center of divergence) of a spherical wave sit on the z-axis at \( z = Z \).&lt;/li&gt;
&lt;li&gt;The phasor describing the time-dependent part of the field in Goodman's notation is \( e^{-j 2 \pi f t} \).&lt;/li&gt;
&lt;li&gt;If we move away from the center of the wave such that \( z - Z \) is positive and we encounter wavefronts emitted earlier in time, then \( t \) is decreasing and the argument to the phasor is increasing. The wave is therefore diverging if the argument is positive.&lt;/li&gt;
&lt;li&gt;If we move away from the center of the wave  such that \( z - Z \) is negative and we encounter wavefronts emitted earlier in time, then \( t \) is decreasing and the argument to the phasor is increasing. However, a negative \( z - Z \) makes the phasor negative again so that it is in fact decreasing. The wave is therefore diverging if the argument is negative.&lt;/li&gt;
&lt;li&gt;Likewise for converging waves.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To summarize:&lt;/p&gt;
&lt;table border="1"&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phasor&lt;/th&gt;
      &lt;th&gt; \( \left( z - Z \right) \) positive &lt;/th&gt;
      &lt;th&gt; \( \left( z - Z \right) \) negative &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;\( e^{ j k r} \)&lt;/td&gt;
      &lt;td&gt;diverging&lt;/td&gt;
      &lt;td&gt;converging&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;\( e^{ -j k r} \)&lt;/td&gt;
      &lt;td&gt;converging&lt;/td&gt;
      &lt;td&gt;diverging&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;The Fraunhofer Diffraction Integral&lt;/h2&gt;
&lt;h3&gt;The Fraunhofer Approximation&lt;/h3&gt;
&lt;p&gt;Assume we are so far from the screen that the quadratic phasor inside the diffraction integral is effectively flat. This means: &lt;/p&gt;
&lt;p&gt;$$ z \gg \frac{k \left( \xi^2 + \eta^2 \right)_{\text{max}}}{2} $$&lt;/p&gt;
&lt;h3&gt;The Diffraction Integral&lt;/h3&gt;
&lt;p&gt;Applying the approximation above allows us to drop the quadratic phasor inside the Fresnel diffraction integral because it is effectively 1:&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y; z \right) = \frac{ e^{jkz} }{j \lambda z} e^{\frac{j k}{2 z} \left( x^2 + y^2 \right)} \iint_{-\infty}^{\infty} U \left( \xi , \eta \right) e^{-j \frac{2 \pi }{\lambda z} \left( x \xi + y \eta \right) }  \,d\xi \,d\eta $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apart from the phase term that depends on \( z \), this expression represents a Fourier transform of the incident field.&lt;/li&gt;
&lt;li&gt;It appears to break spatial invariance because we no longer depend on differences of coordinates, e.g. \( x - \xi \). However, we can still use the Fresnel transfer function (the Fourier transform of the Fresnel convolution kernel) as the transfer function for Fraunhofer diffraction because if the Fraunhofer approximation is valid, then so is the Fresnel approximation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Solution to Homework Problem 4.16&lt;/h2&gt;
&lt;p&gt;Problem 4.16 is important because it is a basis for the development of the frequency analysis of image-forming systems in later chapters of Goodman.&lt;/p&gt;
&lt;p&gt;The purpose of 4.16 is to show that the diffraction pattern of an aperture that is illuminated by a spherical converging wave in the Fresnel regime is the Fraunhofer diffraction pattern of the aperture.&lt;/p&gt;
&lt;h3&gt;Part a: Quadratic phase approximation to the incident wave&lt;/h3&gt;
&lt;p&gt;Let \( z = 0 \) be the plane of the aperture and \( z = Z \) be the observation plane. Additionally, let \( \left( \xi, \eta \right) \) represent the coordinates in the plane of the aperture, and \( \left( x, y \right) \) the coordinates in the observation plane. The spherical wave that illuminates the aperture is convering to a point \( \vec{r}_P = Y \hat{ \jmath} + Z \hat{k} \) in the observation plane.&lt;/p&gt;
&lt;p&gt;To find a quadratic phase approximation for the incident wave, start with its representation as a time-harmonic spherical wave of amplitude \( A \):&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y, z \right) = A \frac{e^{j k |\vec{r} - \vec{r}_P|}}{|\vec{r} - \vec{r}_P|} $$&lt;/p&gt;
&lt;p&gt;Note that \( \vec{r} - \vec{r}_P = x \hat{\imath} + \left( y - Y \right) \hat{\jmath} + \left( z - Z \right) \hat{k} \). Its magnitude is&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
| \vec{r} - \vec{r}_P | &amp;amp;=&amp;amp; \sqrt{x^2 + \left( y - Y \right)^2 + \left( z - Z \right)^2} \\
&amp;amp;=&amp;amp; \left( z - Z \right) \sqrt{1 + \frac{x^2 + \left( y - Y \right)^2}{\left( z - Z \right)^2} } \\
&amp;amp;\approx&amp;amp; \left( z - Z \right) + \frac{ x^2 + \left( y - Y \right)^2 }{2 \left( z - Z \right)}
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;At first glance, there's a problem here because allowing \( \left( z - Z \right) \) to be negative will result in a negative value for the magnitude of the vector \( \left( \hat{r} - \hat{r}_P \right) \). However, if we use the above table for selecting \( e^{j k r} \) as the phasor for a converging wave when \( \left( z - Z \right) \) is negative, then we will have the correct sign of the argument to the phasor. We do however need to take the absolute value of the \( z - Z \) term in the denominator of the expression of the spherical wave.&lt;/p&gt;
&lt;p&gt;Replacing the distance in the phasor's argument with the two lowest order terms in the binomial expansion and the lowest order term in the denominator:&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y, z \right) \approx A \frac{e^{j k \left(z - Z \right)} e^{j k \left[ x^2 + \left( y - Y \right)^2 \right] / 2 \left(z - Z \right) }}{\left|z - Z \right|} $$&lt;/p&gt;
&lt;p&gt;In the \( z = 0 \) plane, this becomes:&lt;/p&gt;
&lt;p&gt;$$ U \left( x, y; z = 0 \right) \approx A \left(x, y \right) \frac{e^{-j k Z} e^{-j k \left[ x^2 + \left( y - Y \right)^2 \right] / 2 Z }}{Z} $$&lt;/p&gt;
&lt;p&gt;I moved the finite extent of the aperture into a new function for the amplitude \( A \) above. This function is zero outside the aperture and a constant \( A \) inside it.&lt;/p&gt;
&lt;h3&gt;Part b: Diffraction pattern at the point \( P \)&lt;/h3&gt;
&lt;p&gt;Use the second form of the Fresnel diffraction integral to compute the diffraction pattern at \( P \):&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
U \left( x = 0, y = Y, z = Z \right) &amp;amp;=&amp;amp; \frac{ e^{jkZ} }{j \lambda Z} e^{\frac{j k Y^2}{2 Z}} \iint_{-\infty}^{\infty} \left[ U \left( \xi , \eta ; z = 0 \right) e^{\frac{j k}{2 Z} \left( \xi^2 + \eta^2 \right)} \right] e^{-j \frac{2 \pi }{\lambda Z} y \eta }  \,d\xi \,d\eta \\
&amp;amp;\approx&amp;amp; \frac{ e^{jkZ} e^{-jkZ} }{j \lambda Z^2} e^{\frac{j k Y^2}{2 Z}} \iint_{-\infty}^{\infty} A \left(\xi, \eta \right) \left[ e^{-\frac{j k}{2Z} \left[ \xi^2 + \left( \eta - Y \right)^2 \right]} e^{\frac{j k}{2 Z} \left( \xi^2 + \eta^2 \right)} \right] e^{-j \frac{2 \pi }{\lambda Z} y \eta }  \,d\xi \,d\eta \\
&amp;amp;\approx&amp;amp; \frac{1}{j \lambda Z^2} e^{\frac{j k Y^2}{2 Z} } \iint_{-\infty}^{\infty} A \left(\xi, \eta \right) \left[ e^{-\frac{j k}{2Z} \left( \xi^2 + \eta^2 - 2 \eta Y + Y^2 \right)} e^{\frac{j k}{2 Z} \left( \xi^2 + \eta^2 \right)} \right] e^{-j \frac{2 \pi }{\lambda Z} y \eta }  \,d\xi \,d\eta \\
&amp;amp;\approx&amp;amp; \frac{1}{j \lambda Z^2} \iint_{-\infty}^{\infty} A \left(\xi, \eta \right) e^{\frac{j k \eta Y}{Z}} e^{-j \frac{2 \pi}{\lambda Z} y \eta }  \,d\xi \,d\eta \\
&amp;amp;\approx&amp;amp; \frac{1}{j \lambda Z^2} \iint_{-\infty}^{\infty} A \left(\xi, \eta \right) e^{-j \frac{2 \pi }{\lambda Z} \left(\eta - Y \right) }  \,d\xi \,d\eta
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;The final expression above is proportional to the Fraunhofer diffraction pattern of the aperture. The reason that the Fraunhofer diffraction pattern appears as the result is that the converging spherical wavefronts exactly cancel the diverging quadratic phase term inside the Fresnel diffraction formula, leaving a simple Fourier transform of the aperture as a result.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Goodman, Joseph W. Introduction to Fourier optics. Roberts and Company publishers (2005). ISBN 978-0974707723. &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/a-very-brief-summary-of-fresnel-and-fraunhofer-diffraction-integrals/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>diffraction</category><category>Fraunhofer</category><category>Fresnel</category><guid>https://kylemdouglass.com/posts/a-very-brief-summary-of-fresnel-and-fraunhofer-diffraction-integrals/</guid><pubDate>Fri, 28 Mar 2025 08:06:03 GMT</pubDate></item><item><title>Coordinate Systems for Modeling Microscope Objectives</title><link>https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;A common model for infinity corrected microscope objectives is that of an aplanatic and telecentric optical system. In many developments of this model, emphasis is placed upon the calculation of the electric field near the focus. However, this has the effect that the definition of the coordinate systems and geometry are conflated with the determination of the fields. In addition, making the model amenable to computation often occurs as an afterthought.&lt;/p&gt;
&lt;p&gt;In this post I will explore the geometry of an aplanatic system for modeling high NA objectives with an emphasis on computational implementations. My approach follows Novotny and Hecht&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; and Herrera and Quinto-Su&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;The Model Components&lt;/h2&gt;
&lt;p&gt;The model system is illustrated below:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src="https://kylemdouglass.com/images/aplanatic-telecentric-system.png"&gt;
  &lt;figcaption&gt;A high NA, infinity corrected microscope objective as an aplanatic and telecentric optical system.
&lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;In this model, we abstract over the details of the objective by representing it as four surfaces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A back focal plane containing an aperture stop&lt;/li&gt;
&lt;li&gt;A back principal plane, \( P \)&lt;/li&gt;
&lt;li&gt;A front principal surface, \( P' \)&lt;/li&gt;
&lt;li&gt;A front focal plane&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The space to the left of the back principal plane is called the infinity space. The space to the right of the front principal surface is called the sample space.&lt;/p&gt;
&lt;p&gt;We let the infinity space refractive index \( n_1 = 1 \) because it is in air. The refractive index \( n_2 \) is the refractive index of the immersion medium.&lt;/p&gt;
&lt;p&gt;The unit vectors \( \mathbf{n} \) are not used in this discussion; they are relevant for computing the fields.&lt;/p&gt;
&lt;h3&gt;Assumptions&lt;/h3&gt;
&lt;p&gt;We make one assumption: the system obeys the sine condition. The meaning of this will be explained later.&lt;/p&gt;
&lt;p&gt;An aplanatic system is one that obeys the sine condition.&lt;/p&gt;
&lt;p&gt;We will not assume the intensity law to conserve energy because it is only necessary when computing the electric field near the focus.&lt;/p&gt;
&lt;h3&gt;The Aperture Stop and Back Focal Plane&lt;/h3&gt;
&lt;p&gt;The aperture stop (AS) of an optical system is the element that limits the angle of the marginal ray.&lt;/p&gt;
&lt;p&gt;The system is telecentric because the aperture stop is located in the back focal plane (BFP). We can shape the focal field by spatially modulating any of the amplitude, phase, or polarization of the incident light in a plane conjugate to the BFP.&lt;/p&gt;
&lt;h3&gt;The Back Principal Plane&lt;/h3&gt;
&lt;p&gt;This is the plane in infinity space at which rays appear to refract. It is a plane because rays coming from a point in the front focal plane all emerge into the infinity space in the same direction.&lt;/p&gt;
&lt;p&gt;Strictly speaking, focus field calculations require us to propagate the field from the AS to the back principal plane before computing the Debye diffraction integral, but this step is often omitted&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt;. The assumptions of paraxial optics should hold here.&lt;/p&gt;
&lt;h3&gt;The Front Principal Surface&lt;/h3&gt;
&lt;p&gt;The front principal surface is the surface at which rays appear to refract in the sample space. It is a surface because&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;this is a non-paraxial system, and&lt;/li&gt;
&lt;li&gt;we assumed the sine condition.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The sine condition states that refraction of a ray coming from an on-axis point in the front focal plane occurs on a spherical cap centered upon the focal point. The distance from the optical axis of the point of intersection of the ray with the surface is proportional to the sine of the angle that the ray makes with the axis.&lt;/p&gt;
&lt;p&gt;The principal surface is in the far field of the electric field coming from the focal region. For this reason, we can represent a point on this surface as representing a single ray or a plane wave&lt;sup id="fnref2:1"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3&gt;The Front Focal Plane&lt;/h3&gt;
&lt;p&gt;This plane is located a distance \( n_2 f \) from the principal surface&lt;sup id="fnref:4"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:4"&gt;4&lt;/a&gt;&lt;/sup&gt;. It is not at a distance \( f \) from this surface. This is a result of imaging in an immersion medium.&lt;/p&gt;
&lt;h2&gt;Geometry and Coordinate Systems&lt;/h2&gt;
&lt;h3&gt;The Aperture Stop Radius&lt;/h3&gt;
&lt;p&gt;The aperture stop radius \( R \) corresponds to the distance from the axis to the point where the marginal ray intersects the front prinicpal surface. In the sample space, the marginal ray travels at an angle \( \theta_{max} \) with respect to the axis.&lt;/p&gt;
&lt;p&gt;Under the sine condition, this height is&lt;/p&gt;
&lt;p&gt;$$ R = n_2 f \sin{ \theta_{max} } = f \, \text{NA} $$&lt;/p&gt;
&lt;p&gt;The right-most expression uses the definition of the numerical aperture \( \text{NA} \equiv n \sin{ \theta_{max} } \).&lt;/p&gt;
&lt;p&gt;Compare this result to the oft-cited expression for the entrance pupil diameter of an objective lens: \( D = 2 f \, \text{NA} \). They are the same. This makes sense because an entrance pupil is either&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;an image of an aperture stop, or&lt;/li&gt;
&lt;li&gt;a physical stop.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Back Principal Plane&lt;/h3&gt;
&lt;p&gt;There are two independent coordinate systems in the back principal plane:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the spatial coordinate system defining the far field positions \( \left( x_{\infty} , y_{\infty} \right) \), and&lt;/li&gt;
&lt;li&gt;the coordinate system of the angular spectrum of plane waves \( \left( k_x, k_y \right) \).&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Far Field Coordinate System&lt;/h4&gt;
&lt;p&gt;The far field coordinate system may be written in Cartesian form as \( \left( x_{\infty} , y_{\infty} \right) \). It also has a cylindrical representation as&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
\rho &amp;amp;=&amp;amp; \sqrt{x_{\infty}^2 + y_{\infty}^2} \\
\phi &amp;amp;=&amp;amp; \arctan \left( \frac{y_{\infty}}{x_{\infty}} \right)
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;The cylindrical representation appears to be preferred in textbook developments of the model. The Cartesian representation is likely preferred for computational models because it works naturally with two-dimensional arrays of numbers, and because beam shaping elements such as spatial light modulators are rectangular arrays of pixels&lt;sup id="fnref2:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h4&gt;The Angular Spectrum Coordinate System&lt;/h4&gt;
&lt;p&gt;Each point in the angular spectrum coordinate system represents a plane wave in the sample space that is traveling at an angle \( \theta \) to the axis according to:&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
k_x &amp;amp;=&amp;amp; k \sin \theta \cos \phi \\
k_y &amp;amp;=&amp;amp; k \sin \theta \sin \phi \\
k_z &amp;amp;=&amp;amp; k \cos \theta
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;where \( k = 2 \pi n_2 / \lambda = n_2 k_0 \).&lt;/p&gt;
&lt;p&gt;Along the y-axis ( \( x_{\infty} = 0 \) ), the maximum value of \( k_y \) is \(n_2 k_0 \sin \theta_{max} = k_0 \, \text{NA} \).&lt;/p&gt;
&lt;p&gt;Substitute in the expression \( \text{NA} = R / f \) and we get \(k_{y, max} = k_0 R / f\). But \( R = y_{\infty, max} \). This (and similar reasoning for the x-axis) implies that:&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
k_x &amp;amp;=&amp;amp; k_0 x_{\infty} / f \\
k_y &amp;amp;=&amp;amp; k_0 y_{\infty} / f
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;The above equations link the angular spectrum coordinate system to the far field coordinate system. They are no longer independent once \( f \) and \( \lambda \) are specified.&lt;/p&gt;
&lt;h2&gt;Numerical Meshes&lt;/h2&gt;
&lt;p&gt;There are four free parameters for defining the coordinate systems of the numerical meshes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The numerical aperture, \( \text{NA} \)&lt;/li&gt;
&lt;li&gt;The wavelength, \( \lambda \)&lt;/li&gt;
&lt;li&gt;The focal length, \( f \)&lt;/li&gt;
&lt;li&gt;The linear mesh size, \( L \)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Below is a figure that illustrates the construction of the meshes. Both the far field and angular spectrum coordinate systems are represented by a \( L \times L \) array. \( L = 16 \) in the figure below. In general the value of \( L \) should be a power of 2 to help ensure the efficiency of the Fast Fourier Transform (FFT). By considering only powers of 2, we need only consider arrays of even size as well.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src="https://kylemdouglass.com/images/pupil-function-simulation-mesh.png"&gt;
  &lt;figcaption&gt;A numeric mesh representing the far field and angular spectrum coordinate systems of a microscope objective. Fields are sampled at the center of each mesh pixel.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The fields are defined on a region of circular support that is centered on this array. The radius of the domain of the far field coordinate system is \( f \text{NA} \); the radius of the domain of the angular spectrum coordinate system is \( k_0 \text{NA} \).&lt;/p&gt;
&lt;p&gt;The boxes that are bound by the gray lines indicate the location of each field sample. The \( \left( x_{\infty} , y_{\infty} \right) \) and the \( \left( k_x, k_y \right) \) coordinate systems are sampled at the center of each gray box. The origin is therefore not sampled, which will help avoid division by zero errors when the fields are eventually computed.&lt;/p&gt;
&lt;p&gt;The figure suggests that we could create only one mesh and scale it by either \( f \text{NA} \) or \( k_0 \text{NA} \) depending on which coordinate system we are working with. The normalized coordinates become \( \left( x_{\infty} / \left( f \text{NA} \right), y_{\infty} / \left( f \text{NA} \right) \right) \) and \( \left( k_x / \left( k_0 \text{NA} \right), k_y / \left( k_0 \text{NA} \right) \right) \).&lt;/p&gt;
&lt;h3&gt;1D Mesh Example&lt;/h3&gt;
&lt;p&gt;As an example, let \( L = 16 \). To four decimal places, the normalized coordinates are \( -1.0000, -0.8667, \ldots, -0.0667, 0.0667, \ldots, 0.8667, 1.0000 \).&lt;/p&gt;
&lt;p&gt;The spacing between array elements is \( 2 / \left( L - 1 \right) = 0.1333 \). Note that 0 is not included in the 1D mesh as it goes from -0.0667 to 0.0667.&lt;/p&gt;
&lt;p&gt;A 2D mesh is easily constructed from the 1D mesh using tools such as NumPy's &lt;a href="https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html"&gt;meshgrid&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Back Principal Plane Mesh Spacings&lt;/h3&gt;
&lt;p&gt;In the x-direction, the mesh spacing of the far field coordinate system is&lt;/p&gt;
&lt;p&gt;$$ \Delta x_{\infty} = 2 R / \left( L - 1 \right) = 2 f \text{NA} / \left( L - 1 \right) $$&lt;/p&gt;
&lt;p&gt;In the \( k_x \)-direction, the mesh spacing of the angular spectrum coordinate system is&lt;/p&gt;
&lt;p&gt;$$ \Delta k_x = 2 k_{max} / \left( L - 1 \right) = 2 k_0 \text{NA} / \left( L - 1 \right) $$&lt;/p&gt;
&lt;p&gt;Note the symmetry between these two expressions. One scales with \( f \text{NA} \) and the other \( k_0 \text{NA} \). Recall that these are free parameters of the model.&lt;/p&gt;
&lt;h3&gt;Sample Space Mesh Spacing&lt;/h3&gt;
&lt;p&gt;It is interesting to compute the spacing between mesh elements \( \Delta x \) in the sample space when the fields are eventually computed.&lt;/p&gt;
&lt;p&gt;The sampling angular frequency in the sample space is \( k_S = 2 \pi / \Delta x \).&lt;/p&gt;
&lt;p&gt;The Nyquist-Shannon sampling theory states that the maximum informative angular frequency is \( k_{max} = k_S / 2 \).&lt;/p&gt;
&lt;p&gt;From the previous section, we know that \( k_{max} = \left(L - 1 \right) \Delta k_x / 2 \), and that \( \Delta k_x = 2 k_0 \text{NA} / \left( L - 1 \right) \).&lt;/p&gt;
&lt;p&gt;Combining all the previous expressions and simplifying, we get:&lt;/p&gt;
&lt;p&gt;$$\begin{eqnarray}
k_S &amp;amp;=&amp;amp; 2 k_{max} \\
2 \pi / \Delta x &amp;amp;=&amp;amp; \left(L - 1 \right) \Delta k_x \\
2 \pi / \Delta x &amp;amp;=&amp;amp; \left(L - 1 \right) \left[ 2 k_0 \text{NA} / \left( L - 1 \right) \right] \\
2 \pi / \Delta x &amp;amp;=&amp;amp; \left(L - 1 \right) \left[ 2 \left(2 \pi / \lambda \right) \text{NA} / \left( L - 1 \right) \right]
\end{eqnarray}$$&lt;/p&gt;
&lt;p&gt;Solving the above expression for \( \Delta x \), we arrive at&lt;/p&gt;
&lt;p&gt;$$ \Delta x = \frac{\lambda}{2 \text{NA}} $$&lt;/p&gt;
&lt;p&gt;which is of course the Abbe diffraction limit.&lt;/p&gt;
&lt;h3&gt;Effect of not Sampling the Origin&lt;/h3&gt;
&lt;p&gt;Herrera and Quinto-Su&lt;sup id="fnref3:2"&gt;&lt;a class="footnote-ref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt; point out that an error will be introduced if we naively apply the FFT to compute the field components in the \( \left( k_x, k_y \right) \) coordinate system because the origin is not sampled, whereas the FFT assumes that we sample the zero frequency component. The effect is that the result of the FFT has a constant phase error that accounts for a half-pixel shift in each direction of the mesh.&lt;/p&gt;
&lt;p&gt;Consider again the 1D mesh example with \(L = 16 \): \( -1.0000, -0.8667, \ldots, -0.0667, 0.0667, \ldots, 0.8667, 1.0000 \)&lt;/p&gt;
&lt;p&gt;In Python and other languages that index arrays starting at 0, the origin is located at \(L / 2 - 0.5 \), i.e. halfway between the samples at index 7 and 8. A lateral shift in Fourier space is equivalent to a phase shift in real space:&lt;/p&gt;
&lt;p&gt;$$ \phi_{shift} \left(X, Y \right) =  -j 2 \pi \frac{0.5}{L} X - j 2 \pi \frac{0.5}{L} Y $$&lt;/p&gt;
&lt;p&gt;where \( X \) and \( Y \) are normalized coordinates.&lt;/p&gt;
&lt;p&gt;At this point, I am uncertain whether the phasor with the above argument needs to be multiplied or divided with the result of the FFT because 1. there are a few typos in the signs for the coordinate system bounds in the manuscript of Herrera and Quinto-Su, and 2. the correction was developed for use in MATLAB, which indexes arrays starting at 1. Once the fields are computed, it would be easy to verify the correct sign of the phase terms following the procedure outlined in Figure 3 of Herrera and Quinto-Su's manuscript.&lt;/p&gt;
&lt;h3&gt;Structure of the Algorithm&lt;/h3&gt;
&lt;p&gt;The algorithm to compute the focus fields will proceed as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;(optional) Propgate the inputs fields from the AS to the back principal plane using paraxial wave propagation&lt;/li&gt;
&lt;li&gt;Input the sampled fields in the back principal plane in the \( \left( x_{\infty}, y_{\infty} \right) \) coordinate system&lt;/li&gt;
&lt;li&gt;Transform the fields to the \( \left( k_x, k_y \right) \) coordinate system&lt;/li&gt;
&lt;li&gt;Compute the fields in the \( \left(x, y, z \right) \) coordinate system using the FFT&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Additional Remarks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Zero padding the mesh will increase the sample space resolution beyond the Abbe limit, but since the fields remain zero outside of the support, no new information is added.&lt;/li&gt;
&lt;li&gt;On the other hand, zero padding might be required when computing fields going from the sample space to the back principal plane to faithfully reproduce any evanescent components.&lt;/li&gt;
&lt;li&gt;Separating the coordinate system and mesh construction from the calculation of the fields reveals that the two assumptions of the model belong separately to each part. The sine condition is used in the construction of the coordinate systems, whereas energy conservation is used when computing the fields.&lt;/li&gt;
&lt;li&gt;This post did not explain how to compute the fields.&lt;/li&gt;
&lt;li&gt;Herrera and Quinto-Su (and possibly also Novotny and Hecht) appear to use an "effective" focal length which can be obtained by multiplying the one that I use by the sample space refractive index. I prefer my formulation because it is consistent with geometric optics and the well-known expression for the diameter of an objective's entrance pupil. When the fields are calculated, however, I do not yet know whether the arguments of the phasors of the Debye integral will require modification.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Lukas Novotny and Bert Hecht, "Principles of Nano-Optics," Cambridge University Press (2006). &lt;a href="https://doi.org/10.1017/CBO9780511813535"&gt;https://doi.org/10.1017/CBO9780511813535&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref2:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Isael Herrera and Pedro A. Quinto-Su, "Simple computer program to calculate arbitrary tightly focused (propagating and evanescent) vector light fields," arXiv:2211.06725 (2022). &lt;a href="https://doi.org/10.48550/arXiv.2211.06725"&gt;https://doi.org/10.48550/arXiv.2211.06725&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref2:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref3:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Marcel Leutenegger, Ramachandra Rao, Rainer A. Leitgeb, and Theo Lasser, "Fast focus field calculations," Opt. Express 14, 11277-11291 (2006). &lt;a href="https://doi.org/10.1364/OE.14.011277"&gt;https://doi.org/10.1364/OE.14.011277&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Sun-Uk Hwang and Yong-Gu Lee, "Simulation of an oil immersion objective lens: A simplified ray-optics model considering Abbe’s sine condition," Opt. Express 16, 21170-21183 (2008). &lt;a href="https://doi.org/10.1364/OE.16.021170"&gt;https://doi.org/10.1364/OE.16.021170&lt;/a&gt; &lt;a class="footnote-backref" href="https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/#fnref:4" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>microscopy</category><category>optics</category><guid>https://kylemdouglass.com/posts/coordinate-systems-for-modeling-microscope-objectives/</guid><pubDate>Thu, 21 Nov 2024 09:52:48 GMT</pubDate></item><item><title>The Mono16 Format and Flir Cameras</title><link>https://kylemdouglass.com/posts/the-mono16-format-and-flir-cameras/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;For a long time I had found the Mono16 image format of Flir's cameras a bit strange. In the lab I have several Flir cameras with 12-bit ADC's, but the images they output in Mono16 would span a range from 0 to around 65535. How does the camera map a 12-bit number to a 16-bit number?&lt;/p&gt;
&lt;p&gt;If you search for the Mono16 format you will find that it's a padded format. This means that, in the 12-bit ADC example, 4 bits in each pixel are always 0, and the remaining 12 bits represent the pixel's value. But this should mean that we should get pixel values only between 0 and 2^12 - 1, or 4095. So how is it that we can saturate one of these cameras with values near 65535?&lt;/p&gt;
&lt;p&gt;Today it occurred to me that Flir's Mono16 format might not use all the values in the range [0, 65535]. This is indeed the case, as I show below with an image stack that I acquired from one of these cameras:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sorted_unique_pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ravel&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_unique_pixels&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;144&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;uint16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This prints all the possible, unique differences between the sorted and flattened pixel values in my particular image stack. Notice how they are all multiples of 16?&lt;/p&gt;
&lt;p&gt;Let's look also at the sorted array of unique values itself:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sorted_unique_pixels&lt;/span&gt;
&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt; &lt;span class="mi"&gt;5808&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;5824&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;5856&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;57312&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;57328&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;57472&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;uint16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are more than a million pixels in this array, yet they all take values that are integer multiples of 16.&lt;/p&gt;
&lt;p&gt;It looks like Flir's Mono16 format rescales the camera's output onto the interval [0, 65535] by introducing "gaps" between the numbers equal to 2^16 - 2^N where N is the bit-depth of the camera's ADC.&lt;/p&gt;
&lt;p&gt;But wait just a moment. Above I said that 4 bits in the Mono16 are zero, but I assumed that these were the most significant bits. If the least significant bits are the zero padding, then the allowed pixel values would be, for example, 
&lt;code&gt;0000 0000 = 0&lt;/code&gt;, &lt;code&gt;0001 0000 = 16&lt;/code&gt;, &lt;code&gt;0010 0000 = 32&lt;/code&gt;, &lt;code&gt;0011 0000 = 48&lt;/code&gt;, etc. (Here I ignored the first 8 bits for clarity.)&lt;/p&gt;
&lt;p&gt;So it appears that Flir is indeed padding the 12-bit ADC data with 0's in its Mono16 format. But, somewhat counter-intuitively, &lt;em&gt;it is the four least significant bits that are the zero padding.&lt;/em&gt; I say this is counter-intuitive because I have another camera that pads the most significant bits, so that the maximum pixel value is really 2^N - 1, with N being the ADC's bit-depth.&lt;/p&gt;</description><category>cameras</category><category>computer vision</category><guid>https://kylemdouglass.com/posts/the-mono16-format-and-flir-cameras/</guid><pubDate>Tue, 27 Aug 2024 12:15:36 GMT</pubDate></item><item><title>Automated Testing of Simulation Code via Hypothesis Testing</title><link>https://kylemdouglass.com/posts/testing-simulation-code/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;h2&gt;Missing a Theory of Testing for Scientific Code&lt;/h2&gt;
&lt;p&gt;If you search the Internet for resources on the theory of testing code, you will find information about the different types of tests and how to write them. You will also find that it is generally accepted among programmers that good code is tested and bad code is not. The problem for scientists and engineers, however, is that the theory concerning the testing of computer code was developed primarily by programmers that work on systems that model business processes. There is little theory on how, for example, to test the outcome of physics simulations. To further exacerbate the problem, scientific programmers feel obliged to write tests without the guidance of such a theory because of the imperative to test their code. This leads to convoluted tests that are difficult to understand and maintain.&lt;/p&gt;
&lt;h3&gt;Scientific Code is Different&lt;/h3&gt;
&lt;p&gt;Code that models business processes is based on explicit rules that are developed from a set of requirements. An example of a rule that a business system might follow is "If a customer has ordered an item and has not paid, then send her an invoice."&lt;/p&gt;
&lt;p&gt;To test the above rule, we write out all the possible cases and write a test for each one. For example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A customer orders an item without paying. Expected result: an invoice is sent.&lt;/li&gt;
&lt;li&gt;A customer orders an item and pays at the time of checkout: Expected result: no invoice is sent.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I have found that a good way to identify test cases in business logic is to look for if/else statements in a rule. Each branch of the statement should be a different test.&lt;/p&gt;
&lt;p&gt;Now let's consider a physics simulation. I am an optical engineer, so I will use an example from optics. One thing I have often done in my work is to simulate the image formation process of a lens system, including the noise imparted by the camera. A simple model of a CMOS camera pixel is one that takes an input signal in photons, adds shot noise, converts it to photoelectrons, adds dark noise, and then converts the electron signal into analog-to-digital units. Schematically:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;photons&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;electrons&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADUs&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A simplified Python code snippet that models this process, including noise, is below. An instance of the camera class has a method called &lt;code&gt;snap&lt;/code&gt; that takes input array of photons and converts it to ADUs.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dataclasses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Camera&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# ADU&lt;/span&gt;
    &lt;span class="n"&gt;bit_depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="n"&gt;dark_noise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;6.83&lt;/span&gt;  &lt;span class="c1"&gt;# e-&lt;/span&gt;
    &lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;  &lt;span class="c1"&gt;# ADU / e-&lt;/span&gt;
    &lt;span class="n"&gt;quantum_efficiency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.76&lt;/span&gt;
    &lt;span class="n"&gt;well_capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32406&lt;/span&gt;  &lt;span class="c1"&gt;# e-&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Simulate shot noise and convert to electrons&lt;/span&gt;
        &lt;span class="n"&gt;photoelectrons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poisson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantum_efficiency&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Add dark noise&lt;/span&gt;
        &lt;span class="n"&gt;electrons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dark_noise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;photoelectrons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;photoelectrons&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Clip to the well capacity to model electron saturation&lt;/span&gt;
        &lt;span class="n"&gt;electrons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;electrons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;well_capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Convert to ADU&lt;/span&gt;
        &lt;span class="n"&gt;adu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;electrons&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;

        &lt;span class="c1"&gt;# Clip to the bit depth to model ADU saturation&lt;/span&gt;
        &lt;span class="n"&gt;adu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bit_depth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;adu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;How can we test this code? In this case, there are no if/else statements to help us identify test cases. Some possible solutions are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;An expert can review it. But what if we don't have an expert? Or, if you are an expert, how do we know that we haven't made a mistake? I have worked professionally as both an optical and a software engineer and I can tell you that I make coding mistakes many times a day. And what if the simulation is thousands of lines of code? This solution, though useful, cannot be sufficient for testing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compute what the results ought to be for a given set of inputs. Rules like "If the baseline is 100, and the bit depth is 12, etc., then the output is 542 ADU" are not that useful here because the output is random.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Evaluate the code and manually check that it produces the desired results. This is similar to expert review. The problem with this approach is that you would need to recheck the code every time a change is made. One of the advantages of testing business logic is that the tests can be automated. It would be advantageous to preserve automation in testing scientific code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We could always fix the value of the seed for the random number generator to at least make the test deterministic, but then we would not know whether the variation in the simulation output is what we would expect from run-to-run. I'm also unsure whether the same seed produces the same results across different hardware architectures. Since the simulation is non-deterministic at its core, it would be nice to include this attribute within the test case.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Automated Testing of Simulation Results via Hypothesis Testing&lt;/h2&gt;
&lt;p&gt;The solution that I have found to the above-listed problems is derived from ideas that I learned in a class on quality control that I took in college. In short, we run the simulation a number of times and compute one or more statistics from the results. The statistics are compared to their theoretical values in a hypothesis test, and, if the result is outside of a given tolerance, the test fails. If the probability of failure is made small enough, then a failure of the test &lt;strong&gt;practically&lt;/strong&gt; indicates an error in the simulation code rather than a random failure due to the stochastic output.&lt;/p&gt;
&lt;h3&gt;Theoretical Values for Test Statistics&lt;/h3&gt;
&lt;p&gt;In the example of a CMOS camera, both the theoretical mean and the variance of a pixel are known. The &lt;a href="https://www.emva.org/standards-technology/emva-1288/"&gt;EMVA 1288 Linear Model&lt;/a&gt; states that&lt;/p&gt;
&lt;p&gt;$$ \mu_y = K \left( \eta \mu_p + \mu_d \right) + B $$&lt;/p&gt;
&lt;p&gt;where \( \mu_y \) is the mean ADU count, \( K \) is the gain, \( \eta \) is the quantum efficiency, \( \mu_p \) is the mean photon count, \( \mu_d \) is the mean dark noise, and \( B \) is the baseline value, i.e. the average ADU count under no illumination. Likewise, the variance of the pixel describes the noise:&lt;/p&gt;
&lt;p&gt;$$ \sigma_y = \sqrt{K^2 \sigma_d^2 + \sigma_q^2 + K \left( \mu_y - B \right)} $$&lt;/p&gt;
&lt;p&gt;where \( \sigma_y \) is the standard deviation of the ADU counts, \( \sigma_d^2 \) is the dark noise variance, and \( \sigma_q^2 = 1 / 12 \, \text{ADU} \) is the quantization noise, i.e. the noise from converting an analog voltage into discrete ADU values.&lt;/p&gt;
&lt;h3&gt;Hypothesis Testing&lt;/h3&gt;
&lt;p&gt;We can formulate a hypothesis test for each test statistic. The test for each is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Null hypothesis :&lt;/strong&gt; the simulation statistics and the theoretical values are the same&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alternative hypothesis :&lt;/strong&gt; the simulation statistics and the theoretical values are different&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's first focus on the mean pixel values. To perform this hypothesis test, I ran the simulation code a number of times. For convenience, I chose an input signal of 1000 photons. Here's the resulting histogram:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://kylemdouglass.com/images/camera-mean-adus.png"&gt;&lt;/p&gt;
&lt;p&gt;The mean of this distribution is 190.721 ADU and the standard deviation is 3.437 ADU. The theoretical values are 191.2 ADU and 3.420 ADU, respectively. Importantly, if I re-run the simulation, then I get a different histogram because the simulation's output is random.&lt;/p&gt;
&lt;p&gt;The above histogram is called the &lt;strong&gt;sampling distribution of the mean&lt;/strong&gt;, and its width is proportional to the &lt;strong&gt;standard error of the mean&lt;/strong&gt;. (&lt;em&gt;Edit 2024/05/30&lt;/em&gt; Actually, I think I am wrong here. This is not the sampling distribution of the mean. To get it we would need to repeat the above experiment a number of times and compute the mean each time, much like I do in the following section. The set of all means from doing so would be its sampling distribution. Fortunately, the estimate of the confidence intervals in what follows should still hold because the sampling distribution of the mean tends to a normal distribution for large \(N \), and this allows for the expression in the equation that follows.)&lt;/p&gt;
&lt;h4&gt;Hypothesis Testing of the Mean Pixel Value&lt;/h4&gt;
&lt;p&gt;To perform the hypthosesis test on the mean, I build a confidence interval around the simulated value using the following formula:&lt;/p&gt;
&lt;p&gt;$$ \mu_y \pm X \frac{s}{\sqrt{N}} $$&lt;/p&gt;
&lt;p&gt;Here \( s \) is my estimated standard deviation (3.437 ADU in the example above), and \( N = 10,000 \) is the number of simulated values. Their ratio \( \frac{s}{\sqrt{N}} \) is an estimate of the &lt;strong&gt;standard error of the mean&lt;/strong&gt;. \( X \) is a proportionality factor that is essentially a tolerance on how close the simulated value must be to the theoretical one to be considered "equal". A larger tolerance means that it is less likely that the hypothesis test will fail, but I am less certain that the value of the simulation is exactly equal to the theoretical value.&lt;/p&gt;
&lt;p&gt;If this looks familiar, it should. In introductory statistics classes, this approach is called &lt;a href="https://en.wikipedia.org/wiki/Student%27s_t-test"&gt;Student's one sample t-test&lt;/a&gt;. In the t-test, the value for \( X \) is denoted as \( t \) and depends on the desired confidence level and on the number of data points in the sample. (Strictly speaking, it's the number of data points minus 1.)&lt;/p&gt;
&lt;p&gt;As far as I can tell there's no rule for selecting a value of \( X \); rather, it's a free parameter. I often choose 3. Why? Well, if the sampling distribution is approximately normally distributed, and the number of sample points is large, then the theoretical mean should lie within 3 standard errors of the simulated one approximately 99.7% of the time &lt;strong&gt;if the algorithm is correct.&lt;/strong&gt; Alternatively, this means that a correct simulation will produce a result that is more than three standard errors from the theoretical mean about every 1 out of 370 test runs.&lt;/p&gt;
&lt;h4&gt;Hypothesis Testing of the Noise&lt;/h4&gt;
&lt;p&gt;Recall that standard deviation of pixel values is a measure of the noise. The approach to testing it remains the same as before. We write the confidence interval as&lt;/p&gt;
&lt;p&gt;$$ \sigma_y \pm X \left( s.e. \right) $$&lt;/p&gt;
&lt;p&gt;where we have \( s.e. \) as the standard error of the standard deviation. If the simulated standard deviation is outside this interval, then we reject the null hypothesis and fail the test.&lt;/p&gt;
&lt;p&gt;Now, how do we calculate the standard error of the standard deviation? Unlike with the mean value, we have only one value for the standard deviation of the pixel values. Furthermore, there doesn't seem to be a simple formula for the standard error of the variance or standard error of the standard deviation. (I looked around the Math and Statistics Stack Exchanges, but what I did find produced standard errors that were way too large.)&lt;/p&gt;
&lt;p&gt;Faced with this problem, I have two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;run the simulation a number of times to get a distribution of standard deviations&lt;/li&gt;
&lt;li&gt;draw pixel values from the existing simulation data &lt;strong&gt;with replacement&lt;/strong&gt; to estimate the sampling distribution. This approach is known as &lt;a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)"&gt;bootstrapping&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this situation, both are valid approaches because the simulation runs quite quickly. However, if the simulation is slow, bootstrapping might be desirable because resampling the simulated data is relatively fast.&lt;/p&gt;
&lt;p&gt;I provide below a function that makes a bootstrap estimate of the standard error of pixel values to give you an idea of how this works. It draws &lt;code&gt;n&lt;/code&gt; samples from the simulated pixel values with replacement and places the results in the rows of an array. Then, the standard devation of each row is computed. Finally, since the standard error is the standard deviation of the sampling distribution, the standard deviation of resampled standard deviations is computed and returned.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;se_std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ravel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;std_sampling_distribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std_sampling_distribution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Of course, the value of &lt;code&gt;n&lt;/code&gt; in the function above is arbitrary. From what I can tell, setting &lt;code&gt;n&lt;/code&gt; to be the size of the data is somewhat standard practice.&lt;/p&gt;
&lt;h4&gt;Automated Hypothesis Testing&lt;/h4&gt;
&lt;p&gt;At this point, we can calculate the probability that the mean and standard deviation of the simulated pixel values will lie farther than some distance from their theoretical values. This means that we know roughly how often a test will fail due to pure luck.&lt;/p&gt;
&lt;p&gt;To put these into an automated test function, we need only translate the two hypotheses into an assertion. The null hypothesis should correspond to the argument of the assertion being true; the alternative hypothesis corresponds to a false argument.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;TOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_cmos_camera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;num_pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
    &lt;span class="n"&gt;mean_photons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;photons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_photons&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_pixels&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected_mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;191.2&lt;/span&gt;
    &lt;span class="n"&gt;expected_std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.42&lt;/span&gt;

    &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tol_mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOL&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_pixels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;num_pixels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;tol_std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOL&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;se_std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;expected_mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tol_mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;expected_std&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tol_std&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With a &lt;code&gt;TOL&lt;/code&gt; value of 3 and with the sampling distributions being more-or-less normally distributed, each assertion should fail about 1 / 370 times because the area in the tails of the distribution beyond three standard errors is 1 / 370. We can put this test into our test suite and continuous integration (CI) system and run it automatically using whatever tools we wish, e.g. GitHub Actions and pytest.&lt;/p&gt;
&lt;h2&gt;Discussion&lt;/h2&gt;
&lt;h3&gt;Non-deterministic Tests&lt;/h3&gt;
&lt;p&gt;It is an often-stated rule of thumb that automated tests should never fail randomly because it makes failures difficult to diagnose and makes you likely to ignore the tests. Here however it is in the very nature of this test that it will fail randomly from time to time. What are we to do?&lt;/p&gt;
&lt;p&gt;An easy solution would be to isolate these sorts of tests and run them separately from the deterministic ones so that we know exactly where the error occurred. Then, if there is a failure of the non-deterministic tests, the CI could just run them again. If &lt;code&gt;TOL&lt;/code&gt; is set so that a test failure is very rare, then any failure of these tests twice would practically indicate a failure of the algorithm to produce the theoretical results.&lt;/p&gt;
&lt;h3&gt;Testing Absolute Tolerances&lt;/h3&gt;
&lt;p&gt;It could be argued that what I presented here is a lot of work just to make an assertion that a simulation result is close to a known value. In other words, it's just a fancy way to test for absolute tolerances, and possibly is more complex than it needs to be. I can't say that I entirely disagree with this.&lt;/p&gt;
&lt;p&gt;As an alternative, consider the following: if we run the simulation a few times we can get a sense of the variation in its output, and we can use these values to roughly set a tolerance that states by how much the simulated and theoretical results should differ. This is arguably faster than constructing the confidence intervals like we did above.&lt;/p&gt;
&lt;p&gt;The value in the hypothesis testing approach is that you can know the probability of failure to a high degree of accuracy. Whether or not this is important probably depends on what you want to do, but it does provide you with a deeper understanding of the behavior of the simulation that might help debug difficult problems.&lt;/p&gt;
&lt;h3&gt;Testing for Other Types of Errors&lt;/h3&gt;
&lt;p&gt;There are certainly other problems in testing simulation code that are not covered here. The above approach won't tell you directly if you have entered an equation incorrectly. It also requires theoretical values for the summary statistics of the simulation's output. If you have a theory for these already, you might argue that a simulation would be superfluous.&lt;/p&gt;
&lt;p&gt;If it's easy to implement automated tests for your simulation that are based on hypothesis testing, and if you expect the code to change often, then having a few of these sorts of tests will at least provide you a degree of confidence that everything is working as you expect as you make changes. And that is one of the goals of having automated tests: fearless refactoring.&lt;/p&gt;
&lt;h3&gt;Testing the Frequency of Failures&lt;/h3&gt;
&lt;p&gt;I stated often that with hypothesis testing we know how often the code should fail, but we never actually tested that. We could have run the simulation a large number of times and verified that the number of failures was approximately equal to the theoretical number of failures.&lt;/p&gt;
&lt;p&gt;To my mind, it seems that this is just the exact same problem that was addressed above, but instead of testing summary statistics on the output values we test the number of failures. And since the number of failures will vary randomly, we would need a sampling distribution for this. So really this approach requires more CPU clock cycles to do the same thing because we need to run the simulation a large number of times.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Automated testing of simulation code is different than testing business logic due to its stochastic nature and inability to be reduced to "rules"&lt;/li&gt;
&lt;li&gt;We can formulate hypothesis tests to determine how often the simulation produces values that are farther than a given distance from what theory predicts&lt;/li&gt;
&lt;li&gt;The hypothesis tests can be translated into test cases: accepting the null hypothesis means the test passes, whereas rejecting the null hypothesis means the test fails&lt;/li&gt;
&lt;li&gt;Non-deterministic testing is useful when it is quick to implement and you expect to change the code often&lt;/li&gt;
&lt;/ul&gt;</description><category>cameras</category><category>simulation</category><category>statistics</category><guid>https://kylemdouglass.com/posts/testing-simulation-code/</guid><pubDate>Tue, 21 May 2024 07:54:40 GMT</pubDate></item><item><title>A Simple Object-Space Telecentric System</title><link>https://kylemdouglass.com/posts/a-simple-object-space-telecentric-system/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;h2&gt;Object-space telecentricity&lt;/h2&gt;
&lt;p&gt;I have been working on a software package recently for optical systems design. The process of building the package has proceeded like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Think of a particular case that I want to model; for example an infinite conjugate afocal system&lt;/li&gt;
&lt;li&gt;Implement it in the code&lt;/li&gt;
&lt;li&gt;Discover that the code doesn't work&lt;/li&gt;
&lt;li&gt;Create a test case that helps debug the code&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I am modeling a telecentric lens in the current iteration of this loop. To keep things simple, I am limiting myself to an &lt;a href="https://en.wikipedia.org/wiki/Telecentric_lens#Object-space_telecentric_lenses"&gt;object-space telecentric system&lt;/a&gt;. This was more challenging than I expected. In part, the reason is that I was trying to infer whether a system was or was not telecentric from the lens prescription data and a ray trace, which has two problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I need to do a floating point comparison between two numbers to say whether a system is telecentric. Either the chief ray angle in object-space has to be zero or the entrance pupil must be located at infinity. Floating point comparisons are notoriously difficult to get right, and if you're doing them then you might want to rethink what you're trying to model.&lt;/li&gt;
&lt;li&gt;Numerous checks are needed before we can even trace any rays. For example, I should check first whether the user placed the object at infinity. This would form the image in the same plane as the aperture stop, which does not really make sense.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I find it interesting that &lt;a href="https://support.zemax.com/hc/en-us/articles/1500005488201-Modeling-a-lens-that-is-telecentric-in-image-space"&gt;Zemax addresses these problems&lt;/a&gt; by introducing object-space telecentricity as an extra boolean flag that forces the chief ray angle to be zero in the object-space. In other words, the user needs to know what they're doing and to specify that they want telecentricity from the beginning.&lt;/p&gt;
&lt;h2&gt;An object-space telecentric example&lt;/h2&gt;
&lt;p&gt;I adapted the following example from lens data presented in this video: &lt;a href="https://www.youtube.com/watch?v=JfstTsuNAz0"&gt;https://www.youtube.com/watch?v=JfstTsuNAz0&lt;/a&gt;. Notably, the object distance was increased by nearly a factor of two from what was given in the video so that the image plane was at a finite distance from the lens. Paraxial ray trace results were computed by hand.&lt;/p&gt;
&lt;table border="1"&gt;
    &lt;caption&gt;
        A simple object-space telecentric system comprising a planoconvex lens and a stop.
    &lt;/caption&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;Surface&lt;/th&gt;
            &lt;th&gt;0&lt;/th&gt;
            &lt;th&gt;1&lt;/th&gt;
            &lt;th&gt;2&lt;/th&gt;
            &lt;th&gt;3&lt;/th&gt;
            &lt;th&gt;4&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;Comment&lt;/th&gt;
            &lt;td&gt;OBJ&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;STOP&lt;/td&gt;
            &lt;td&gt;IMG&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( R \)&lt;/th&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;\( \infty \)&lt;/td&gt;
            &lt;td&gt;-9.750&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( t \)&lt;/th&gt;
            &lt;td&gt;29.4702&lt;/td&gt;
            &lt;td&gt;2&lt;/td&gt;
            &lt;td&gt;15.97699&lt;/td&gt;
            &lt;td&gt;17.323380&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( n \)&lt;/th&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;1.610248&lt;/td&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( C \)&lt;/th&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;0&lt;/td&gt;
            &lt;td&gt;-0.10256&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
         &lt;/tr&gt; 
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( -\Phi \)&lt;/th&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;0&lt;/td&gt;
            &lt;td&gt;-0.06259&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( t/n \)&lt;/th&gt;
            &lt;td&gt;29.4702&lt;/td&gt;
            &lt;td&gt;1.24204&lt;/td&gt;
            &lt;td&gt;15.97699&lt;/td&gt;
            &lt;td&gt;17.323380&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( y \)&lt;/th&gt;
            &lt;td&gt;0&lt;/td&gt;
            &lt;td&gt;29.4702&lt;/td&gt;
            &lt;td&gt;30.712240&lt;/td&gt;
            &lt;td&gt;15.97699&lt;/td&gt;
            &lt;td&gt;0&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( nu \)&lt;/th&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;-0.922279&lt;/td&gt;
            &lt;td&gt;-0.922279&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( \bar{y} \)&lt;/th&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;1&lt;/td&gt;
            &lt;td&gt;0&lt;/td&gt;
            &lt;td&gt;-1.084270&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th scope="row"&gt;\( n \bar{u} \)&lt;/th&gt;
            &lt;td&gt;0&lt;/td&gt;
            &lt;td&gt;0&lt;/td&gt;
            &lt;td&gt;-0.06259&lt;/td&gt;
            &lt;td&gt;-0.06259&lt;/td&gt;
            &lt;td&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;This system is shown below with lens semi-diameters of 5 mm. Note that the stop is at the paraxial focus of the lens. The rays in the sketch cross the axis before the stop because of spherical aberration.&lt;/p&gt;
&lt;p&gt;&lt;svg viewbox="0, 0, 1344, 150" width="120%" fill="none" stroke="black" xmlns="http://www.w3.org/2000/svg"&gt;&lt;path d="M 632.646354675293 142.5 L 632.646354675293 142.5 L 632.646354675293 142.5 L 632.646354675293 135.39473819732666 L 632.646354675293 128.2894731760025 L 632.646354675293 121.18420815467834 L 632.646354675293 114.078946352005 L 632.646354675293 106.97368454933167 L 632.646354675293 99.86841952800751 L 632.646354675293 92.76315450668335 L 632.646354675293 85.65789270401001 L 632.646354675293 78.55263090133667 L 632.646354675293 71.44736909866333 L 632.646354675293 64.34210085868835 L 632.646354675293 57.236839056015015 L 632.646354675293 50.131577253341675 L 632.646354675293 43.0263090133667 L 632.646354675293 35.92104721069336 L 632.646354675293 28.81578540802002 L 632.646354675293 21.71052360534668 L 632.646354675293 14.60526180267334 L 632.646354675293 7.5 L 632.646354675293 7.5 L 641.0208705067635 7.5 L 641.0208705067635 7.5 L 644.972696185112 14.60526180267334 L 648.3765449523926 21.71052360534668 L 651.27783036232 28.81578540802002 L 653.7113556861877 35.92104721069336 L 655.7038663029671 43.0263090133667 L 657.275763630867 50.131577253341675 L 658.4422525763512 57.236839056015015 L 659.2141510248184 64.34210085868835 L 659.5984016060829 71.44736909866333 L 659.5984016060829 78.55263090133667 L 659.2141510248184 85.65789270401001 L 658.4422541856766 92.76315450668335 L 657.275763630867 99.86841952800751 L 655.7038679122925 106.97368454933167 L 653.7113556861877 114.078946352005 L 651.2778335809708 121.18420815467834 L 648.3765481710434 128.2894731760025 L 644.972696185112 135.39473819732666 L 641.0208705067635 142.5 L 641.0208705067635 142.5 L 632.646354675293 142.5 Z" stroke="black" stroke-width="1" stroke-linejoin="bevel" fill="none"&gt;&lt;/path&gt;&lt;path d="M 632.646354675293 142.5 L 632.646354675293 142.5 L 632.646354675293 135.39473819732666 L 632.646354675293 128.2894731760025 L 632.646354675293 121.18420815467834 L 632.646354675293 114.078946352005 L 632.646354675293 106.97368454933167 L 632.646354675293 99.86841952800751 L 632.646354675293 92.76315450668335 L 632.646354675293 85.65789270401001 L 632.646354675293 78.55263090133667 L 632.646354675293 71.44736909866333 L 632.646354675293 64.34210085868835 L 632.646354675293 57.236839056015015 L 632.646354675293 50.131577253341675 L 632.646354675293 43.0263090133667 L 632.646354675293 35.92104721069336 L 632.646354675293 28.81578540802002 L 632.646354675293 21.71052360534668 L 632.646354675293 14.60526180267334 L 632.646354675293 7.5" stroke="black" stroke-width="1" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 875.3357162475586 142.5 L 875.3357162475586 142.5 L 875.3357162475586 81.75" stroke="black" stroke-width="1" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 875.3357162475586 68.25 L 875.3357162475586 68.25 L 875.3357162475586 7.5" stroke="black" stroke-width="1" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 641.0208705067635 142.5 L 641.0208705067635 142.5 L 644.972696185112 135.39473819732666 L 648.3765481710434 128.2894731760025 L 651.2778335809708 121.18420815467834 L 653.7113556861877 114.078946352005 L 655.7038679122925 106.97368454933167 L 657.275763630867 99.86841952800751 L 658.4422541856766 92.76315450668335 L 659.2141510248184 85.65789270401001 L 659.5984016060829 78.55263090133667 L 659.5984016060829 71.44736909866333 L 659.2141510248184 64.34210085868835 L 658.4422525763512 57.236839056015015 L 657.275763630867 50.131577253341675 L 655.7038663029671 43.0263090133667 L 653.7113556861877 35.92104721069336 L 651.27783036232 28.81578540802002 L 648.3765449523926 21.71052360534668 L 644.972696185112 14.60526180267334 L 641.0208705067635 7.5" stroke="black" stroke-width="1" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 1109.2013397216797 142.5 L 1109.2013397216797 142.5 L 1109.2013397216797 135.39473819732666 L 1109.2013397216797 128.2894731760025 L 1109.2013397216797 121.18420815467834 L 1109.2013397216797 114.078946352005 L 1109.2013397216797 106.97368454933167 L 1109.2013397216797 99.86841952800751 L 1109.2013397216797 92.76315450668335 L 1109.2013397216797 85.65789270401001 L 1109.2013397216797 78.55263090133667 L 1109.2013397216797 71.44736909866333 L 1109.2013397216797 64.34210085868835 L 1109.2013397216797 57.236839056015015 L 1109.2013397216797 50.131577253341675 L 1109.2013397216797 43.0263090133667 L 1109.2013397216797 35.92104721069336 L 1109.2013397216797 28.81578540802002 L 1109.2013397216797 21.71052360534668 L 1109.2013397216797 14.60526180267334 L 1109.2013397216797 7.5" stroke="#999999" stroke-width="1" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 142.5 L 234.7986602783203 142.5 L 234.7986602783203 135.39473819732666 L 234.7986602783203 128.2894731760025 L 234.7986602783203 121.18420815467834 L 234.7986602783203 114.078946352005 L 234.7986602783203 106.97368454933167 L 234.7986602783203 99.86841952800751 L 234.7986602783203 92.76315450668335 L 234.7986602783203 85.65789270401001 L 234.7986602783203 78.55263090133667 L 234.7986602783203 71.44736909866333 L 234.7986602783203 64.34210085868835 L 234.7986602783203 57.236839056015015 L 234.7986602783203 50.131577253341675 L 234.7986602783203 43.0263090133667 L 234.7986602783203 35.92104721069336 L 234.7986602783203 28.81578540802002 L 234.7986602783203 21.71052360534668 L 234.7986602783203 14.60526180267334 L 234.7986602783203 7.5" stroke="#999999" stroke-width="1" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 115.5 L 234.7986602783203 115.5 L 632.646354675293 115.5 L 653.2606882452965 115.5 L 875.3357162475586 69.18727111816406 L 1109.2013397216797 20.41567325592041" stroke="red" stroke-width="0.5" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 75 L 234.7986602783203 75 L 632.646354675293 75 L 659.646354675293 75 L 875.3357162475586 75 L 1109.2013397216797 75" stroke="red" stroke-width="0.5" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 34.5 L 234.7986602783203 34.5 L 632.646354675293 34.5 L 653.2606882452965 34.5 L 875.3357162475586 80.81272888183594 L 1109.2013397216797 129.5843267440796" stroke="red" stroke-width="0.5" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 2451228.234375 L 234.7986602783203 2451228.234375 L 632.646354675293 2451193.4296875" stroke="red" stroke-width="0.5" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 2451187.734375 L 234.7986602783203 2451187.734375 L 632.646354675293 2451152.9296875" stroke="red" stroke-width="0.5" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;path d="M 234.7986602783203 2451147.234375 L 234.7986602783203 2451147.234375 L 632.646354675293 2451112.4296875" stroke="red" stroke-width="0.5" stroke-linejoin="miter" fill="none"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/p&gt;
&lt;h2&gt;Remarks&lt;/h2&gt;
&lt;h3&gt;Marginal ray trace&lt;/h3&gt;
&lt;p&gt;At first the marginal ray trace was a bit confusing because the entrance pupil is at infinity. How can the marginal ray, which intersects the pupil at its edge, be traced when the pupil is at infinity? Then I remembered that I don't aim for the edge of the pupil when tracing the marginal ray. Instead, I launch a ray from the axis in the object plane at a random angle taking the surface with the smallest ray height as the aperture stop. (I chose a paraxial angle of 1 in the table above. Technically, this is called a pseudo-marginal ray. The real marginal ray is calculated from it by rescaling the surface intersection heights by the aperture stop semi-diameter.) Once you have the marginal ray in image space, just find its intersection with the axis to determine the image location.&lt;/p&gt;
&lt;h3&gt;Telecentric lens design&lt;/h3&gt;
&lt;p&gt;So how would an object-space telecentric design be implemented in software? First, I'd set an option that would force the chief ray angle to 0 in the object space. Then, I'd simply place a solve on the aperture stop that puts it at the location where the chief ray intersects the axis.&lt;/p&gt;</description><category>ray tracing</category><category>telecentricity</category><guid>https://kylemdouglass.com/posts/a-simple-object-space-telecentric-system/</guid><pubDate>Mon, 11 Mar 2024 07:59:17 GMT</pubDate></item><item><title>Fusion 360 Core Concepts</title><link>https://kylemdouglass.com/posts/fusion-360-core-concepts/</link><dc:creator>Kyle M. Douglass</dc:creator><description>&lt;p&gt;I decided recently to learn Fusion 360 to help with some custom optomechanical designs that I need in the lab. The following are my notes about its core concepts.&lt;/p&gt;
&lt;h2&gt;Assemblies&lt;/h2&gt;
&lt;p&gt;An assembly is a group of parts in one design file.&lt;/p&gt;
&lt;p&gt;In CAD, there are two ways to create assemblies:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Bottom-up&lt;/strong&gt;&lt;ol&gt;
&lt;li&gt;Create parts&lt;/li&gt;
&lt;li&gt;Add parts to the assembly&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Top-down&lt;/strong&gt; (used by Fusion 360)&lt;ol&gt;
&lt;li&gt;Start with an assembly&lt;/li&gt;
&lt;li&gt;Add parts to it&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Bodies vs. components&lt;/h2&gt;
&lt;h3&gt;Bodies&lt;/h3&gt;
&lt;p&gt;A body is a 3D shape used to add or remove components.&lt;/p&gt;
&lt;p&gt;There are two core types:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Solid bodies&lt;/li&gt;
&lt;li&gt;Surface bodies (denoted by a yellow face)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Other types include T-Splines (created in the Form environment) used to create freeform shapes, and mesh bodies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bodies must be of the same type to interact with one another.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Components&lt;/h3&gt;
&lt;p&gt;A component is a part or "container" used within an assembly.&lt;/p&gt;
&lt;p&gt;Components can contain&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bodies&lt;/li&gt;
&lt;li&gt;construction planes&lt;/li&gt;
&lt;li&gt;sketches&lt;/li&gt;
&lt;li&gt;canvases&lt;/li&gt;
&lt;li&gt;origin planes&lt;/li&gt;
&lt;li&gt;other components (a.k.a. &lt;strong&gt;subassemblies&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Joints&lt;/h2&gt;
&lt;p&gt;Joints are how components are forced to stay together.&lt;/p&gt;
&lt;h2&gt;Guidelines&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Always start an assembly with a new component&lt;/li&gt;
&lt;li&gt;Always rename components and bodies right after creation&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=TzG2deElWqI&amp;amp;t=0s"&gt;Bodies vs Components&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description><category>cad</category><guid>https://kylemdouglass.com/posts/fusion-360-core-concepts/</guid><pubDate>Fri, 08 Mar 2024 14:16:22 GMT</pubDate></item></channel></rss>