{"id":"6328fc7ba0803522","slug":"finding-roots-in-the-complex-plane","trashed":false,"description":"","likes":59,"publish_level":"live","forks":3,"fork_of":null,"has_importers":false,"update_time":"2023-09-05T04:22:23.317Z","first_public_version":6160,"paused_version":null,"publish_time":"2023-09-05T04:25:22.430Z","publish_version":6160,"latest_version":6160,"thumbnail":"277c16eb64b16b63b2b8754c287ca27729a6d6d1f33ae1260de47b178436d117","default_thumbnail":"150fba4a5d8fd4ba4a34fa82e97093c1612761a05c1563700a1f3e5ac52aa004","roles":[],"sharing":null,"owner":{"id":"f1b8352b963a77b4","avatar_url":"https://avatars.observableusercontent.com/avatar/49c18a2ac0092176d4ea8348a1dd538e0b460b086b89d602b8d21ffe0821d92e","login":"rreusser","name":"Ricky Reusser","bio":"","home_url":"https://rreusser.github.io","type":"team","tier":"starter_2024"},"creator":{"id":"908c4be2279a714e","avatar_url":"https://avatars.observableusercontent.com/avatar/fa41989f47bb986723af5b9c22176cb503748426f56e565000ea93acd9eab86c","login":"rreusser","name":"Ricky Reusser","bio":"","home_url":"https://rreusser.github.io","tier":"pro"},"authors":[{"id":"908c4be2279a714e","avatar_url":"https://avatars.observableusercontent.com/avatar/fa41989f47bb986723af5b9c22176cb503748426f56e565000ea93acd9eab86c","name":"Ricky Reusser","login":"rreusser","bio":"","home_url":"https://rreusser.github.io","tier":"pro","approved":true,"description":""}],"collections":[{"id":"dcd095ad05c2b722","type":"public","slug":"writeups","title":"Fun Notebooks","description":"","update_time":"2025-06-03T17:30:32.590Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"5fed9a039a8666a708b624ddf49250636b502e1d13342d5d838ada83c55e9501","thumbnail":"5fed9a039a8666a708b624ddf49250636b502e1d13342d5d838ada83c55e9501","listing_count":62,"parent_collection_count":0,"owner":{"id":"f1b8352b963a77b4","avatar_url":"https://avatars.observableusercontent.com/avatar/49c18a2ac0092176d4ea8348a1dd538e0b460b086b89d602b8d21ffe0821d92e","login":"rreusser","name":"Ricky Reusser","bio":"","home_url":"https://rreusser.github.io","type":"team","tier":"starter_2024"}},{"id":"85cbbc7776cf19d5","type":"public","slug":"math","title":"Math","description":"","update_time":"2019-08-09T17:18:56.524Z","pinned":true,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"2f6b21d1b419e29f4900708a6106b44846f8b3a9a61bb6db0aeb20309597bd23","thumbnail":"2f6b21d1b419e29f4900708a6106b44846f8b3a9a61bb6db0aeb20309597bd23","listing_count":19,"parent_collection_count":0,"owner":{"id":"f1b8352b963a77b4","avatar_url":"https://avatars.observableusercontent.com/avatar/49c18a2ac0092176d4ea8348a1dd538e0b460b086b89d602b8d21ffe0821d92e","login":"rreusser","name":"Ricky Reusser","bio":"","home_url":"https://rreusser.github.io","type":"team","tier":"starter_2024"}},{"id":"2ef792b9227476b4","type":"public","slug":"webgl","title":"WebGL","description":"","update_time":"2019-07-06T05:01:13.577Z","pinned":true,"ordered":false,"custom_thumbnail":"2a6946861257da301c000c444d7cf9ad8906d70be8fa524efd94d17e69863ac3","default_thumbnail":"51282fe49b2cea85d8d7ad2a8b3d9d4b84cc9ff4eb939cb8054450920486879d","thumbnail":"2a6946861257da301c000c444d7cf9ad8906d70be8fa524efd94d17e69863ac3","listing_count":42,"parent_collection_count":0,"owner":{"id":"f1b8352b963a77b4","avatar_url":"https://avatars.observableusercontent.com/avatar/49c18a2ac0092176d4ea8348a1dd538e0b460b086b89d602b8d21ffe0821d92e","login":"rreusser","name":"Ricky Reusser","bio":"","home_url":"https://rreusser.github.io","type":"team","tier":"starter_2024"}}],"files":[],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":6160,"title":"Finding Roots in the Complex Plane","license":"mit","copyright":"Copyright 2021 Ricky Reusser","nodes":[{"id":3271,"value":"md`# Finding Roots in the Complex Plane\n\nOur goal in this notebook is simple. Given a [complex function](https://en.wikipedia.org/wiki/Meromorphic_function) ${$`f(z)`}, find the *roots*, or values ${$`z_i`} at which ${$$`f(z_i) = 0.`} If ${$`f(z)`} is a polynomial (even one with complex coefficients), we're nicely set up to throw a pretty standard black box [polynomial roots function](https://github.com/scijs/durand-kerner) at it. General complex functions (like with sines or cosines, for example) are less straightforward. In fact complex analytic functions have [strict conditions on their structure](https://en.wikipedia.org/wiki/Cauchy%E2%80%93Riemann_equations) that will make this root-finding problem much more interesting and challenging than its real-valued counterpart.\n\nLet's be specific at the outset about which functions we're talking about. The techniques in this notebook won't work for just any complex function ${$`f(z)`}. If we're being precise, our complex functions must be [meromorphic](http://mathworld.wolfram.com/MeromorphicFunction.html). In very rough terms, such functions are nice at all but isolated points and include the complex analog of most of the regular functions we're used to dealing with, including addition, subtraction, multiplication, division, sines, cosines, exponentials, etc. Since this is pretty general, we won't have a difficult time coming up with them. What meromorphic functions *do not* permit is treating the real and complex parts separately, e.g. ${$`f(z) = Re(z)`}, or particularly bad (\"essential\") singularities. Wikipedia has [some helpful examples](https://en.wikipedia.org/wiki/Meromorphic_function#Examples) if you want to know more.\n\nFinally, I should note that I'm not a mathemetician and I haven't taken a proper course in Complex Analysis. There was a time when I needed to find roots in the complex plane to make sense of the ultrasonic waves I was measuring in the lab, but that time has passed. But here we are, and the rich and unexpected structure has made me excited to share one of my favorite problems. So if I've made mistakes, missed important things, or been overly negligent in simplifying the matter, please do [let me know](https://twitter.com/rickyreusser/status/1163332357144735744).\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":4074,"value":"DOM.element('div') ||\n  html`<div>\n${\n  /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)\n    ? `<div style=\"border:1px solid red; background-color:#fee; border-radius: 3px; margin-bottom:15px; padding: 15px; max-width:640px;\">🚨 Looks like you might be using Google Chrome. Due to <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=852348\">this Chrome bug</a>, the tall iframe that powers this page might cause lots of flickering. The solutions for now are either to temporarily disable GPU rasterization by setting the <code>chrome://flags#enable-gpu-rasterization</code> flag to <code>disabled</code> or to try the Firefox or Safari browsers instead. Simply narrowing the window also works for me.</div>`\n    : ''\n}\n<div style=\"overflow:hidden\">\n\n</div>`","pinned":false,"mode":"js","data":null,"name":null},{"id":4129,"value":"md`\n## Newton's method in the complex plane\n\nWe start with the first thing we always do when we want to find roots easily and efficiently: [Newton's method](https://en.wikipedia.org/wiki/Newton%27s_method).\n\nThe idea of Newton's method for real-valued functions is simple: linearize the function at a point, follow that line until it hits zero, and repeat. Stated mathematically, to find the roots of function ${$`f(x)`} given an initial guess ${$`x_0`}, we repeat the iteration ${$$`x_{n+1}=x_{n}-{\\frac {f(x_{n})}{f'(x_{n})}}.`} Extending the method to the complex plane is extremely simple. We replace ${$`x`} with ${$`z`} and write ${$$`z_{n+1}=z_{n}-{\\frac {f(z_{n})}{f'(z_{n})}}.`} That's all there is to it! Such a simple extension doesn't work for just any two-dimensional function, but the [Cauchy-Riemann equations](https://en.wikipedia.org/wiki/Cauchy%E2%80%93Riemann_equations) which our functions obey make them very special. \n\nThe plot below performs Newton's method on a function—one that will be sufficiently messy to keep this exploration interesting. (The function describes elastic wave modes in a plate. [See below](#integrationTolerance) for more details.)`","pinned":false,"mode":"js","data":null,"name":null},{"id":3590,"value":"md`👉Move the initial guess and observe the behavior of Newton's method.\n\n> Q: What does a root look like in this plot? (If this is unclear after a bit of experimentation, feel free to [let me know](https://twitter.com/rickyreusser)! Otherwise the article on [domain coloring](https://en.wikipedia.org/wiki/Domain_coloring) should offer assistance.)\n\n> Q: Where does it perform poorly? Where does it perform well? Why?\n\n> Q: Can we find all roots using this method?`","pinned":false,"mode":"js","data":null,"name":null},{"id":3386,"value":"function complexNewtonRaphson (out, f, z0, tolerance, maxIterations, log) {\n  maxIterations = maxIterations === undefined ? 50 : maxIterations\n  tolerance = tolerance === undefined ? 1e-5 : tolerance\n  var iteration = maxIterations\n  var delta = 1, previousDelta = 0\n  var tmp = [0, 0, 0, 0]\n  var zre = z0[0]\n  var zim = z0[1]\n  if (log) log.push([zre, zim])\n  while (delta / previousDelta > tolerance && --iteration > 0) {\n    previousDelta = delta\n    \n    // Compute the function and its derivative tmp <- [f_re, f_im, f'_re, f'_im]\n    f(tmp, zre, zim)\n    \n    // Divide tmp <- f / f'\n    cdivSS(tmp, [tmp[0], tmp[1]], [tmp[2], tmp[3]])\n    \n    // Update the current guess\n    zre -= tmp[0]\n    zim -= tmp[1]\n    \n    if (log) log.push([zre, zim])\n    \n    // Check convergence\n    delta = Math.hypot(tmp[0], tmp[1])\n  }\n  // Return the answer if converged\n  if (iteration > 0 || delta / previousDelta <= tolerance) {\n    out[0] = zre\n    out[1] = zim\n  } else {\n    out[0] = z0[0]\n    out[1] = z0[1]\n  }\n  return out;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3332,"value":"{\n  const {\n    stack,\n    regl,\n    xScale,\n    yScale,\n    currentXScale,\n    currentYScale,\n    container,\n    configureRegl,\n    viewport,\n    searchPoint\n  } = newtonRaphsonPlot;\n  const svg = d3.select(stack.layers.svg);\n  svg.selectAll('g.plot, g.axes, defs, .nrpath, g.pathPoints').remove();\n  const plot = svg\n    .append('g')\n    .attr('class', 'plot')\n    .attr('clip-path', 'url(#clip-rect)');\n\n  var nrPath = [];\n  var roots;\n  const createLine = d3\n    .line()\n    .x(d => currentXScale(d[0]))\n    .y(d => currentYScale(d[1]));\n\n  function computeRoots() {\n    nrPath.length = 0;\n    roots = [\n      complexNewtonRaphson(\n        [],\n        fLambWave.f,\n        searchPoint.slice(),\n        1e-3,\n        100,\n        nrPath\n      )\n    ];\n  }\n\n  const axes = svg.append('g').attr('class', 'axes');\n  const nrPathLine = plot\n    .append('path')\n    .attr('class', 'nrpath')\n    .attr('fill', 'none')\n    .attr('stroke', 'rgba(0, 0, 0, 0.5)')\n    .attr('stroke-width', 2);\n  const gPathPoints = plot.append('g').attr('class', 'pathPoints');\n  const gRoot = plot.append('g').attr('class', 'roots');\n  const handles = plot.append('g').attr('class', 'handles');\n\n  svg\n    .append('defs')\n    .append('clipPath')\n    .attr('id', 'clip-rect')\n    .append('rect')\n    .attr('x', viewport.margin.l)\n    .attr('y', viewport.margin.t)\n    .attr('width', viewport.width - viewport.margin.l - viewport.margin.r)\n    .attr('height', viewport.height - viewport.margin.t - viewport.margin.b);\n\n  function updateRoots() {\n    gRoot.selectAll('g.root').remove();\n    const enter = gRoot\n      .selectAll('g.root')\n      .data(roots)\n      .enter()\n      .append('g')\n      .attr('class', 'root');\n    enter\n      .append('circle')\n      .attr('r', 6)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(255, 255, 255, 1)')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]));\n    enter\n      .append('circle')\n      .attr('r', 4)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(0, 0, 0, 1)');\n    enter\n      .selectAll('circle')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]));\n\n    handles\n      .selectAll('circle')\n      .attr('cx', d => currentXScale(searchPoint[0]))\n      .attr('cy', d => currentYScale(searchPoint[1]));\n\n    gPathPoints.selectAll('circle').remove();\n    gPathPoints\n      .selectAll('circle')\n      .data(nrPath)\n      .enter()\n      .append('circle')\n      .attr('fill', 'rgba(255, 255, 255, 1)')\n      .attr('r', 2.5)\n      .attr('stroke-width', 1)\n      .attr('stroke', 'rgba(0, 0, 0, 1)')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]));\n\n    nrPathLine.datum(nrPath).attr(\"d\", createLine);\n  }\n\n  function updateAxes() {\n    axes.call(viewportAxes(viewport, currentXScale, currentYScale));\n  }\n\n  const drawFunction = createDrawFunction(regl, fLambWave);\n  function updatePlot() {\n    updateRoots();\n    regl.poll();\n    regl.clear({ color: [1, 1, 1, 1] });\n    configureRegl.viewport(viewport, () => {\n      configureRegl.scale(currentXScale, currentYScale, () => {\n        configureRegl.map(() => {\n          drawFunction({ ω2, γ2 });\n        });\n      });\n    });\n  }\n\n  handles\n    .selectAll('g.handles')\n    .data(['z0', 'r0'])\n    .enter()\n    .append('g')\n    .attr('class', 'handle')\n    .call(el => {\n      el.append('circle')\n        .attr('r', 6)\n        .attr('stroke-width', 2)\n        .attr('stroke', 'rgba(255, 255, 255, 0.9)')\n        .attr('fill', '#35e');\n      el.append('circle')\n        .attr('cursor', 'move')\n        .attr('r', 40)\n        .attr('stroke-width', 0)\n        .attr('stroke', 'transparent')\n        .attr('fill', 'transparent')\n        .call(\n          d3.drag().on('drag', d => {\n            searchPoint[0] = currentXScale.invert(d3.event.x);\n            searchPoint[1] = currentYScale.invert(d3.event.y);\n            computeRoots();\n            updateRoots();\n          })\n        );\n    });\n\n  svg.call(\n    persistentZoom(currentXScale, currentYScale, xScale, yScale)\n      .scaleExtent([1, 1])\n      .on('zoom.axes', updateAxes)\n      .on('zoom.plot', updatePlot)\n  );\n\n  computeRoots();\n  updateAxes();\n  updatePlot();\n  updateRoots();\n\n  yield container;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3300,"value":"newtonRaphsonPlot = createPlot({\n  xrange: [-14, 14],\n  yrange: [-14, 14],\n  extras: {searchPoint: [6, 6]}\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":5727,"value":"html`<div style=\"border:1px solid black; border-radius: 3px; padding: 15px; max-width:640px;\"> 🌈 This notebook visualizes functions in the complex plane using domain coloring. There are two main things to know. Color represents <strong>phase</strong> (<span style=\"color:#bb0\">+real</span>, <span style=\"color:blue\">-real</span>, <span style=\"color:red\">+imaginary</span>, and <span style=\"color:green\">-imaginary</span>) while shading represents <strong>magnitude</strong>. Magnitude shading scales logarithmically so that regions with uniformly spaced contours exhibit exponential growth. When all colors meet at a point, it may indicate the presence of a zero (root). You can learn more about the general technique <a href=\"https://en.wikipedia.org/wiki/Domain_coloring\">here</a> and about the specific implementation <a href=\"https://observablehq.com/@rreusser/adaptive-domain-coloring\">here</a>.</div>`","pinned":false,"mode":"js","data":null,"name":null},{"id":3461,"value":"md`Playing around a bit, it's clear why we're not content with Newton's method. It jumps around wildly in slowly changing regions, converges slowly in others, and doesn't consistently find the nearest root.\n\nThese are analogous to the problems Newton's method encounters in one dimension, but the extra cost and complexity of getting our bearings in two dimensions make Newton's method unacceptable in the complex plane.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":4827,"value":"md`## An aside on fractals\nJust how wildly does it jump around? Consider the function ${$$`f(z) = z^3 - 1.`} This function has three roots at ${$`1`}, ${$`-\\frac{1}{2} + \\frac{\\sqrt{3}}{2}i`}, and ${$`-\\frac{1}{2} - \\frac{\\sqrt{3}}{2}i`}. The plot below colors the complex plane by the root to which Newton iteration converges. Darker regions take more iterations to converge. The resulting pattern is called a [Newton fractal](https://en.wikipedia.org/wiki/Newton_fractal). (Tip of the hat to [Jacob Rus](https://observablehq.com/@jrus) for pointing me toward this!)\n\nThings get all complicated and lovely when we don't start right next to a root, so it perhaps suffices to say that we'd like to avoid this sort of complexity if we seek a robust root-finding algorithm.`","pinned":false,"mode":"js","data":null,"name":null},{"id":4913,"value":"viewof plotFractal = radio({options: [{\n  label: 'root to which Newton iteration converges', value: 'root'\n}, {label: 'phase of function value, f(z)', value: 'value'}], value: 'root', description: 'color complex plane by'})","pinned":false,"mode":"js","data":null,"name":null},{"id":4831,"value":"{\n  const {stack, regl, xScale, yScale, currentXScale, currentYScale, container, configureRegl, viewport, searchPoint} = fractalPlot;\n  const func = fz\n  \n  const drawFunction = (plotFractal === 'root' ? createFractalDrawFunction : createDrawFunction)(regl, func)\n  \n  const svg = d3.select(stack.layers.svg)\n  svg.selectAll('g.plot, g.axes, defs, .nrpath, g.pathPoints').remove();\n  const plot = svg.append('g')\n    .attr('class', 'plot')\n    .attr('clip-path', 'url(#clip-rect)');\n    \n  var nrPath = []\n  var roots\n  const createLine = d3.line()\n      .x(d => currentXScale(d[0]))\n      .y(d => currentYScale(d[1]));\n  \n  function computeRoots () {\n    nrPath.length = 0\n    roots = [complexNewtonRaphson([], func.f, searchPoint.slice(), 1e-3, 100, nrPath)]\n  }\n    \n  const axes = svg.append('g').attr('class', 'axes');\n  const nrPathLine = plot.append('path')\n      .attr('class', 'nrpath')\n      .attr('fill', 'none')\n      .attr('stroke', 'rgba(0, 0, 0, 0.5)')\n      .attr('stroke-width', 2)\n  const gPathPoints = plot.append('g').attr('class', 'pathPoints');\n  const gRoot = plot.append('g').attr('class', 'roots');\n  const handles = plot.append('g').attr('class', 'handles')\n  \n  svg.append('defs')\n    .append('clipPath').attr('id', 'clip-rect')\n    .append('rect')\n      .attr('x', viewport.margin.l)\n      .attr('y', viewport.margin.t)\n      .attr('width', viewport.width - viewport.margin.l - viewport.margin.r)\n      .attr('height', viewport.height - viewport.margin.t - viewport.margin.b);\n  \n  function updateRoots () {\n    gRoot.selectAll('g.root').remove()\n    const enter = gRoot.selectAll('g.root')\n      .data(roots)\n      .enter().append('g').attr('class', 'root')\n    enter.append('circle')\n      .attr('r', 6)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(255, 255, 255, 1)')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]))\n    enter.append('circle')\n      .attr('r', 4)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(0, 0, 0, 1)')\n    enter.selectAll('circle')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]))\n    \n    handles.selectAll('circle')\n      .attr('cx', d => currentXScale(searchPoint[0]))\n      .attr('cy', d => currentYScale(searchPoint[1]))\n    \n    gPathPoints.selectAll('circle').remove()\n    gPathPoints.selectAll('circle').data(nrPath)\n        .enter().append('circle')\n        .attr('fill', 'rgba(255, 255, 255, 1)')\n        .attr('r', 2.5)\n        .attr('stroke-width', 1)\n        .attr('stroke', 'rgba(0, 0, 0, 1)')\n        .attr('cx', d => currentXScale(d[0]))\n        .attr('cy', d => currentYScale(d[1]))\n    \n    \n    nrPathLine.datum(nrPath).attr(\"d\", createLine);\n  }\n    \n  function updateAxes () {\n    axes.call(viewportAxes(viewport, currentXScale, currentYScale))\n  }\n  \n  function updatePlot () {\n    updateRoots()\n    regl.poll()\n    regl.clear({color: [1, 1, 1, 1]})\n    configureRegl.viewport(viewport, () => {\n      configureRegl.scale(currentXScale, currentYScale, () => {\n        configureRegl.map(() => {\n          drawFunction({ω2, γ2})\n        })\n      })\n    })\n  }\n  \n  handles.selectAll('g.handles')\n    .data(['z0', 'r0'])\n    .enter().append('g')\n      .attr('class', 'handle')\n      .call(el => {\n        el.append('circle')\n          .attr('r', 6)\n          .attr('stroke-width', 2)\n          .attr('stroke', 'rgba(255, 255, 255, 0.9)')\n          .attr('fill', '#35e')\n        el.append('circle')\n          .attr('cursor', 'move')\n          .attr('r', 40)\n          .attr('stroke-width', 0)\n          .attr('stroke', 'transparent')\n          .attr('fill', 'transparent')\n          .call(d3.drag().on('drag', d => {\n            searchPoint[0] = currentXScale.invert(d3.event.x)\n            searchPoint[1] = currentYScale.invert(d3.event.y)\n            computeRoots()\n           updateRoots()\n          }));\n      })\n  \n  svg.call(persistentZoom(currentXScale, currentYScale, xScale, yScale)\n        .scaleExtent([0.01, 100000])\n        .on('zoom.axes', updateAxes)\n        .on('zoom.plot', updatePlot))\n  \n  computeRoots();\n  updateAxes();\n  updatePlot();\n  updateRoots()\n  \n  yield container\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":605,"value":"viewof derivatives = radio({\n  options: ['Automatic differentiation', 'OES_standard_derivatives'],\n  value: 'Automatic differentiation',\n  description: 'Domain coloring differentiation method'\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":4836,"value":"fractalPlot = createPlot({\n  xrange: [-3, 3],\n  yrange: [-3, 3],\n  extras: {searchPoint: [0.86, 1.6]}\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":5220,"value":"fz = ({\n  glsl: `\n    // Compute the value of z^3 - 1 in a glsl fragment shader. It's a vec4\n    // since we use automatic differentiation to compute the derivative.\n    vec4 f (vec4 z) {\n      return csub(cmul(cmul(z, z), z), vec2(1, 0));\n    }\n  `,\n  f: (function() {\n    var one = [1, 0];\n    var z = [0, 0, 0, 0];\n    var tmp = [0, 0, 0, 0];\n    // Compute the value of z^3 - 1. Unfortunately we have no choice here but\n    // to reimplement the function in plain JavaScript. We once again use\n    // automatic differentiation to compute the derivative, hence a little\n    // bit of the complexity. You're welcome to try some different functions\n    // in this cell, but until something fancier is hooked up, the Newton\n    // iteration won't match the coloring unless the implementations also\n    // match.\n    return function f(out, a, b) {\n      csetValuesD(z, a, b, 1, 0);\n      csubDS(out, cmulDD(tmp, cmulDD(tmp, z, z), z), one);\n    };\n  })()\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":0,"value":"md`## Cauchy's Argument Principle\n\nWe seek a better method. The most memorable theorem in what I did learn of Complex Analysis is [Cauchy's Argument Principle](https://en.wikipedia.org/wiki/Argument_principle). It states that upon integrating a complex function ${$`f(z)`} around a closed contour ${$`C`}, we have${$$`\\displaystyle s_0 \\equiv \\frac{1}{2 \\pi i}\\oint _{C}{f'(z) \\over f(z)}\\,dz=Z-P,`} where ${$`Z`} and ${$`P`} are the number of _zeros_ and _poles_ fully enclosed by the contour, respectively. We label this quantity ${$`s_0`} for reasons that will become apparent.\n\nLet's tease this apart slowly.\n\n*Zeros* (which we somewhat interchangeably call *roots*) are points where the function has the value ${$`0 + 0i`}. *Poles* are points where we divide by zero and the magnitude diverges to infinity. For the sort of function we're looking at, zeros and poles may only occur at isolated points rather than over whole regions.\n\nWorking through the terms of ${$`s_0`} one by one, the first contains the number ${$`2\\pi i`}, which is just a constant [complex number](https://en.wikipedia.org/wiki/Complex_number) by which we divide.\n\nNext, the quantity we integrate, ${$`f'(z) \\over f(z)`}, is the derivative of the complex function at point ${$`z`} divided by the value. That means we need the derivative, which can be a little tricky. [People try hard](https://www.sciencedirect.com/science/article/pii/0021999185901251) to avoid needing the derivative, but I've used [automatic differentiation](https://en.wikipedia.org/wiki/Automatic_differentiation) in this notebook (both [in JavaScript](https://observablehq.com/@rreusser/complex-auto-differentiation) and [in GLSL](https://observablehq.com/@rreusser/glsl-complex-auto-differentiation)) to compute it automatically along with the function value.\n\nFinally, the symbol ${$`\\oint_C \\cdots dz`} represents an [integral in the complex plane](https://en.wikipedia.org/wiki/Contour_integration). Just like integration of real numbers, it represents the sum of lots of little pieces, except instead of two limits of integration along the real number line (a lower and upper limit, commonly the ${$`a`} and ${$`b`} in ${$`\\int_a^b f(x) dx`}), we choose a path, or [*contour*](https://en.wikipedia.org/wiki/Contour_integration#Contours), in the two-dimensional complex plane. Each small step along the path is represented by the complex differential ${$`dz`}, and the circle in the integral symbol ${$`\\oint`} means the path is closed.\n\nIn short, this somewhat ugly-looking integral is just a value we can compute relatively easily by stepping around a path and adding up the little pieces of the derivative divided by the value. Upon doing so, we find that we've counted the difference between the number of zeros and the number of poles!\n\nIt's not as unlikely as one may think to have encountered this in real science and engineering applications—or perhaps in 3Blue1Brown's excellent video \"[Solving 2D equations using color, a story of winding numbers and composition](https://www.youtube.com/watch?v=b7FxPsqfkOY).\" In fact, if the above equation doesn't make much sense, this would be a great time to go watch that video!`\n","pinned":false,"mode":"js","data":null,"name":null},{"id":3748,"value":"md`👉 Move the contour below and observe that it counts how many roots are inside.\n\n> Q: What happens when the contour intersects a root?\n\n> Q: Can you tell how many roots there are when ${$`s_0`} is not perfectly an integer?`","pinned":false,"mode":"js","data":null,"name":null},{"id":3477,"value":"{\n  const {stack, regl, xScale, yScale, currentXScale, currentYScale, container, configureRegl, viewport, searchCircle} = cauchyPlot;\n\n  const svg = d3.select(stack.layers.svg)\n  svg.selectAll('g.plot, g.axes, defs').remove();\n  const plot = svg.append('g')\n    .attr('class', 'plot')\n    .attr('clip-path', 'url(#clip-rect)');\n  \n  var searchOutput = []\n  var roots\n  \n  function computeRoots () {\n    roots = findComplexRoots(fLambWave.f, searchCircle.z, searchCircle.r, {\n      s0IntegrationTolerance: 1e-8,\n      s0IntegrationDepth: 8,\n      integrationTolerance: 1e-10,\n      maxIntegrationDepth: 1,\n      searchOutput,\n      maxRootsPerCircle: 1,\n      maxSearchDepth: 1\n    })\n  }\n    \n  let axes = svg.append('g').attr('class', 'axes');\n  const gRoot = plot.append('g').attr('class', 'roots');\n  const gSearchCircles = plot.append('g').attr('class', 'searchCircles');\n  let handles = plot.append('g').attr('class', 'handles')\n  \n  svg.append('defs')\n    .append('clipPath').attr('id', 'clip-rect')\n    .append('rect')\n      .attr('x', viewport.margin.l)\n      .attr('y', viewport.margin.t)\n      .attr('width', viewport.width - viewport.margin.l - viewport.margin.r)\n      .attr('height', viewport.height - viewport.margin.t - viewport.margin.b);\n    \n  let searchCircles\n  \n  function updateRoots () {\n    let searchCircleSelection = gSearchCircles.selectAll('g.searchCircle')\n    .data(searchOutput)\n    .join(enter => {\n      var appended = enter.append('g')\n        .attr('class', 'searchCircle')\n      appended.append('circle')\n        .attr('stroke-width', 1)\n        .attr('stroke', 'rgba(0, 0, 0, 0.8)')\n        .attr('fill', 'none');\n      appended.append('text')\n        .attr(\"font-style\", \"italic\")\n        .attr(\"font-size\", \"20px\")\n        .attr('dy', 47)\n        .attr('alignment-baseline', 'text-top')\n        .attr('text-anchor', 'middle')\n        .attr('stroke', 'rgba(255, 255, 255, 0.7)')\n        .attr('stroke-width', 3)\n        .attr('paint-order', 'stroke')\n        .attr('stroke-linecap', 'butt')\n        .attr('stroke-linejoin', 'miter')\n        .attr('stroke-miterlimit', 2);\n    })\n    searchCircleSelection.exit().remove()\n\n    gSearchCircles.selectAll('g.searchCircle')\n      .select('circle')\n      .attr('cx', d => currentXScale(d.z[0]))\n      .attr('cy', d => currentYScale(d.z[1]))\n      .attr('r', d => currentXScale(d.r) - currentXScale(0))\n      .attr('stroke', d => 'rgba(0, 0, 0, 1.0)')\n    gSearchCircles.selectAll('g.searchCircle').select('text')\n      .attr('x', d => currentXScale(d.z[0]))\n      .attr('y', d => currentYScale(d.z[1]))\n      .text(d => d.text)\n    \n    handles.selectAll('circle')\n      .attr('cx', d => currentXScale(searchCircle.z[0] + (d === 'z0' ? 0 : searchCircle.r)))\n      .attr('cy', d => currentYScale(searchCircle.z[1]))\n  }\n    \n  function updateAxes () {\n    axes.call(viewportAxes(viewport, currentXScale, currentYScale))\n  }\n  \n  const drawFunction = createDrawFunction(regl, fLambWave)\n  \n  function updatePlot () {\n    updateRoots()\n    \n    regl.poll()\n    regl.clear({color: [1, 1, 1, 1]})\n    configureRegl.viewport(viewport, () => {\n      configureRegl.scale(currentXScale, currentYScale, () => {\n        configureRegl.map(() => {\n          drawFunction({ω2, γ2})\n        })\n      })\n    })\n  }\n  \n  handles.selectAll('g.handles')\n    .data(['z0', 'r0'])\n    .enter().append('g')\n      .attr('class', 'handle')\n      .call(el => {\n        el.append('circle')\n          .attr('r', 6)\n          .attr('stroke-width', 2)\n          .attr('stroke', 'rgba(255, 255, 255, 0.9)')\n          .attr('fill', '#35e')\n        el.append('circle')\n          .attr('cursor', 'move')\n          .attr('r', 40)\n          .attr('stroke-width', 0)\n          .attr('stroke', 'transparent')\n          .attr('fill', 'transparent')\n          .call(d3.drag().on('drag', d => {\n            if (d === 'r0') {\n              searchCircle.r = currentXScale.invert(d3.event.x) - searchCircle.z[0];\n            } else {\n              searchCircle.z[0] = currentXScale.invert(d3.event.x)\n              searchCircle.z[1] = currentYScale.invert(d3.event.y)\n            }\n            computeRoots()\n            updatePlot()\n          }));\n      })\n    \n  svg.call(persistentZoom(currentXScale, currentYScale, xScale, yScale)\n        .scaleExtent([1, 1])\n        .on('zoom.axes', updateAxes)\n        .on('zoom.plot', updatePlot))\n  \n  computeRoots()\n  updateAxes();\n  updatePlot();\n  updateRoots()\n  \n  yield cauchyPlot.container\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3944,"value":"cauchyPlot = createPlot({\n  xrange: [-8, 8],\n  yrange: [-8, 8],\n  extras: {searchCircle: {z: [0, 0], r: 5}}\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":3657,"value":"md`## Problems and annoyances`","pinned":false,"mode":"js","data":null,"name":null},{"id":3721,"value":"md`Let's take a moment to talk about some things that can go wrong. The plot below shows the function ${$$`f(z) = \\frac{\\sqrt{z + 5 + 3i}\\sqrt{z + 5 - 3i}(z - 8 - 8i)}{(z - 3 - 7i)(z - 8 + 6i)^2}.`}\n\nFor a few different reasons, counting the zeros with ${$`s_0`} requires some care.\n\n👉Move the circle below and locate the various terms in the equation.\n\n> Q: What happens when you enclose the twice-repeated pole at ${$`8 - 6i`}?\n\n> Q: What happens when you enclose both the root and the pole in the upper-right corner?\n\n> Q: What happens when you enclose one of the square roots at ${$`-5 \\pm 3i`}? Both?`","pinned":false,"mode":"js","data":null,"name":null},{"id":3670,"value":"{\n  const {stack, regl, xScale, yScale, currentXScale, currentYScale, container, configureRegl, viewport, searchCircle} = pathologicalPlot;\n\n  const svg = d3.select(stack.layers.svg)\n  svg.selectAll('g.plot, g.axes, defs').remove();\n  const plot = svg.append('g')\n    .attr('class', 'plot')\n    .attr('clip-path', 'url(#clip-rect)');\n  \n  var searchOutput = []\n  var roots\n  \n  function computeRoots () {\n    roots = findComplexRoots(fPathological.f, searchCircle.z, searchCircle.r, {\n      s0IntegrationTolerance: 1e-9,\n      s0IntegrationDepth: 11,\n      integrationTolerance: 1e-10,\n      maxIntegrationDepth: 1,\n      searchOutput,\n      maxRootsPerCircle: 1,\n      maxSearchDepth: 1\n    })\n  }\n    \n  let axes = svg.append('g').attr('class', 'axes');\n  const gRoot = plot.append('g').attr('class', 'roots');\n  const gSearchCircles = plot.append('g').attr('class', 'searchCircles');\n  let handles = plot.append('g').attr('class', 'handles')\n  \n  svg.append('defs')\n    .append('clipPath').attr('id', 'clip-rect')\n    .append('rect')\n      .attr('x', viewport.margin.l)\n      .attr('y', viewport.margin.t)\n      .attr('width', viewport.width - viewport.margin.l - viewport.margin.r)\n      .attr('height', viewport.height - viewport.margin.t - viewport.margin.b);\n    \n  let searchCircles\n  \n  function updateRoots () {\n    let searchCircleSelection = gSearchCircles.selectAll('g.searchCircle')\n    .data(searchOutput)\n    .join(enter => {\n      var appended = enter.append('g')\n      .attr('class', 'searchCircle')\n      appended.append('circle')\n        .attr('stroke-width', 1)\n        .attr('stroke', 'rgba(0, 0, 0, 0.8)')\n        .attr('fill', 'none');\n      appended.append('text')\n        .attr(\"font-style\", \"italic\")\n        .attr(\"font-size\", \"20px\")\n        .attr('dy', 47)\n        .attr('alignment-baseline', 'text-top')\n        .attr('text-anchor', 'middle')\n        .attr('stroke', 'rgba(255, 255, 255, 0.7)')\n        .attr('stroke-width', 3)\n        .attr('paint-order', 'stroke fill')\n        .attr('stroke-linecap', 'butt')\n        .attr('stroke-linejoin', 'miter')\n        .attr('stroke-miterlimit', 2);\n    })\n    searchCircleSelection.exit().remove()\n\n    gSearchCircles.selectAll('g.searchCircle')\n      .select('circle')\n      .attr('cx', d => currentXScale(d.z[0]))\n      .attr('cy', d => currentYScale(d.z[1]))\n      .attr('r', d => currentXScale(d.r) - currentXScale(0))\n      .attr('stroke', d => 'rgba(0, 0, 0, 1.0)')\n    gSearchCircles.selectAll('g.searchCircle').select('text')\n      .attr('x', d => currentXScale(d.z[0]))\n      .attr('y', d => currentYScale(d.z[1]))\n      .text(d => d.text)\n    \n    handles.selectAll('circle')\n      .attr('cx', d => currentXScale(searchCircle.z[0] + (d === 'z0' ? 0 : searchCircle.r)))\n      .attr('cy', d => currentYScale(searchCircle.z[1]))\n  }\n    \n  function updateAxes () {\n    axes.call(viewportAxes(viewport, currentXScale, currentYScale))\n  }\n  \n  const drawFunction = createDrawFunction(regl, fPathological)\n  \n  function updatePlot () {\n    updateRoots()\n  }\n  function updateFunction () {\n    regl.poll()\n    regl.clear({color: [1, 1, 1, 1]})\n    configureRegl.viewport(viewport, () => {\n      configureRegl.scale(currentXScale, currentYScale, () => {\n        configureRegl.map(() => {\n          drawFunction({ω2, γ2})\n        })\n      })\n    })\n  }\n  \n  handles.selectAll('g.handles')\n    .data(['z0', 'r0'])\n    .enter().append('g')\n      .attr('class', 'handle')\n      .call(el => {el.append('circle')\n          .attr('r', 7)\n          .attr('stroke-width', 0)\n          .attr('stroke', 'transparent')\n          .attr('fill', 'rgba(255, 255, 255, 0.9)')\n        el.append('circle')\n          .attr('cursor', 'move')\n          .attr('r', 5)\n          .attr('stroke-width', 30)\n          .attr('stroke', 'transparent')\n          .attr('fill', '#35e')\n\n          .call(d3.drag().on('drag', d => {\n            if (d === 'r0') {\n              searchCircle.r = currentXScale.invert(d3.event.x) - searchCircle.z[0];\n            } else {\n              searchCircle.z[0] = currentXScale.invert(d3.event.x)\n              searchCircle.z[1] = currentYScale.invert(d3.event.y)\n            }\n            computeRoots()\n            updatePlot()\n          }));\n      })\n    \n  svg.call(persistentZoom(currentXScale, currentYScale, xScale, yScale)\n        .scaleExtent([1, 1])\n        .on('zoom.axes', updateAxes)\n        .on('zoom.plot', updatePlot)\n        .on('zoom.fn', updateFunction))\n  \n  computeRoots()\n  updateAxes();\n  updatePlot();\n  updateRoots();\n  updateFunction()\n  \n  yield container\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3959,"value":"pathologicalPlot = createPlot({\n  xrange: [-16, 16],\n  yrange: [-15, 15],\n  extras: {searchCircle: {z: [0, 0], r: 6}}\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":3664,"value":"fPathological = ({\n  glsl: `complex f (complex z) {\n    return cdiv(\n      cmul(\n        cmul(\n          csqrt(csub(z, vec2(-5, 3))),\n          csqrt(csub(z, vec2(-5, -3)))\n        ),\n        csub(z, vec2(8, 8))\n      ),\n      cmul(\n        csub(z, vec2(3, 7)),\n        csqr(csub(z, vec2(8, -6)))\n      )\n    );\n  }`,\n  f: (function () {\n    var z = ccreateD()\n    var tmp1 = ccreateD()\n    var tmp2 = ccreateD()\n    var tmp3 = ccreateD()\n    var tmp4 = ccreateD()\n    return function (out, a, b) {\n      csetValuesD(z, a, b, 1, 0)\n      return cdivDD(out,\n        cmulDD(tmp1,\n          cmulDD(tmp1,\n            csqrtD(tmp1, csubDS(tmp1, z, [-5, 3])),\n            csqrtD(tmp2, csubDS(tmp2, z, [-5, -3]))\n          ),\n          csubDS(tmp2, z, [8, 8])\n        ),\n        cmulDD(tmp2,\n          csubDS(tmp2, z, [3, 7]),\n          csqrD(tmp3, csubDS(tmp4, z, [8, -6]))\n        )\n      );\n    }\n  })()\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":5256,"value":"md`The above equation has a couple things going on that can complicate our usage of the argument principle:\n- The [complex square roots](https://en.wikipedia.org/wiki/Square_root#Square_roots_of_negative_and_complex_numbers) on the left side exhibit discontinuities (\"branch cuts\") and show up in ${$`s_0`} as values of ${$`1 \\over 2`}.\n- a root and a pole together show up as ${$`s_0 = 1 - 1 = 0`}.`","pinned":false,"mode":"js","data":null,"name":null},{"id":4152,"value":"md`There are a few ways these difficulties can play out. If ${$`s_0`} is near an integer, we can hope it correctly counts the number of enclosed zeros ${$`Z`}—but we can't be certain. When we enclose both terms with a square root, ${$`s_0 = \\frac{1}{2} + \\frac{1}{2} = 1`}, and there's not immediately a way to tell whether it's two square roots or a single zero (or two zeros and a pole, for that matter).\n\nIt's my (limited) experience that the best way around many of these difficulties is to try avoiding them to begin with.\n\nFor example, consider solutions of the equation ${$$`\\tan z = 1.`} We can cast solutions as the root-finding problem ${$$`f(z) = \\tan z - 1 = 0,`} but this equation has poles (division by zero) that make life with our somewhat elementary tools more difficult. Instead, we may write ${$$`\\frac{\\sin z}{\\cos z} - 1 = 0`} and multiply through by ${$`\\cos z`} to obtain ${$$`f(z) = \\sin z - \\cos z = 0.`} Unlike ${$`\\tan z`} which has poles that may confuse our interpretation of ${$`s_0,`} this equivalent equation has only roots, so that rather than figuring out how to interpret poles, we've avoided them to begin with.\n\nBranch cuts (discontinuities resulting from things like square roots) that result in non-integer values of ${$`s_0`} are a bit harder to deal with but can sometimes be worked around by rearrangement or by avoiding contours large enough to include multiple branch cuts.\n\nOf course these workarounds don't work in every situation, but they're sufficiently useful that for the rest of this notebook we'll neglect some of these thornier problems.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3553,"value":"md`## Recursive Subdivision\nThe ${$`s_0`} tool for counting roots suggests a simple recursive algorithm in which we just zoom in and subdivide until we isolate each individual root inside its own contour:\n\n 1. Integrate the contour and count the number of zeros by computing ${$`s_0`}.\n 2. If the region contains no zeros, stop.\n 3. Otherwise, subdivide into smaller regions.\n 4. For each subdivided region, go to step 1.\n\nThis method is simple but rather inefficient. It misses out on a lot of what complex analysis has to offer.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3865,"value":"md`## The Method of Delves and Lyness\n\nComplex analysis has been around for a while, so it's not surprising the work on numerical methods goes pretty far back. This whole notebook does little more than recite the development from Delves' and Lyness' 1967 paper, [A Numerical Method for Locating the Zeros of an Analytic Function](https://pdfs.semanticscholar.org/1237/c0fabdd4f8c530e3b9453ce9e44e6607cedc.pdf).\n\nIn their paper, Delves and Lyness note as a generalization of Cauchy's Argument Principle the fact that ${$$`\\displaystyle s_N = \\frac{1}{2 \\pi i}\\oint_{C}z^N{f'(z) \\over f(z)}\\,dz=\\sum \\limits_{i = 1}^{Z} (z_i)^N`} where ${$`z_i\\;(i = 1,\\,2,\\,\\cdots,\\,Z)`} are the zeros inside the region bounded by ${$`C`}.\n\nAnother ugly equation. First let's consider the particular case of ${$`N = 0`}. Since ${$`z^0 = 1`}, we have ${$$`\n\\begin{aligned}\ns_0 =& \\frac{1}{2 \\pi i}\\oint _{C}{f'(z) \\over f(z)}\\,dz \\\\\n =& \\sum \\limits_{i = 1}^{Z} (z_i)^0 = \\sum \\limits_{i = 1}^{Z} 1 = Z,\n\\end{aligned}\n`}\n\nthereby reducing to our previous formula for counting the zeros (allowing for the time being that we may neglect poles). For other ${$`N`}, it's at the very least a value we may compute directly by way of numerical integration.\n\nSo basically ${$`s_N`} are like ${$`s_0`} but with powers of ${$`z`} stuck inside.\n\nThe important thing here is that the formula doesn't just allow computation of one value. We can plug in various ${$`N`} and—with a bit of straightforward numerical integration—compute as many ${$`s_N`} as we'd like.\n\nTherefore considering ${$`s_N`} as good as computed numbers, we relate them to the zeros via the formula and write\n\n${$$`\\displaystyle \n\\begin{aligned}\ns_1 &= \\sum \\limits_{i = 1}^{Z} z_i^1 = z_1 + z_2 + \\cdots + z_Z \\\\\ns_2 &= \\sum \\limits_{i = 1}^{Z} z_i^2 = z_1^2 + z_2^2 + \\cdots + z_Z^2 \\\\\n    & \\vdots \\\\\ns_Z &= \\sum \\limits_{i = 1}^{Z} z_i^Z = z_1^Z + z_2^Z + \\cdots + z_Z^Z\n\\end{aligned}\n`}\n\nThis looks an awful lot like a system of ${$`Z`} equations with ${$`Z`} unknowns which usually indicates a solved problem.`","pinned":false,"mode":"js","data":null,"name":null},{"id":4062,"value":"md`## Symmetric polynomials and Newton's identities\n\nDespite the apparent simplicity of our ${$`Z`} equations with ${$`Z`} unknowns, they're not linear so that we require more than our normal linear algebra tools to solve them.\n\nEquations of the form ${$$`s_k(z_1, z_2, \\ldots,z_N) = \\sum_{i = 1}^Nz_i^k`} are called [power sum symmetric polynomials](https://en.wikipedia.org/wiki/Power_sum_symmetric_polynomial). They're called symmetric because swapping any of the ${$`z_i`} leaves ${$`s`} unchanged. In other words, we don't care which root is which; we only care how they behave in aggregate.\n\nSymmetric polynomials are fascinating objects though are certainly not a topic I'm qualified to write about. More than anything, I've been surprised to encounter them in a few different entirely applied contexts, my favorite of which is Mikola Lysenko's delightful article, \"[Comparing Sequences Without Sorting](https://0fps.net/2013/01/24/comparing-sequences-without-sorting/)\" in which he makes use of the fact that symmetric polynomials have all the right properties—in particular, not caring which is which—to compare two sequences of numbers without first sorting them.\n\nTo make a long story short, [Newton's identities](https://en.wikipedia.org/wiki/Newton%27s_identities) give us the inversion we seek in the form of a recursive definition. In terms of the elementary symmetric polynomials ${$`e_k`} (about which details we need not worry here), we write\n\n${$$`\n\\begin{aligned}\ne_{0} &=1, \\\\\n-e_{1}&=-s_{1}, \\\\\ne_{2} &={\\frac {1}{2}}(e_{1}s_{1}-s_{2}), \\\\\n-e_{3}&=-{\\frac {1}{3}}(e_{2}s_{1}-e_{1}s_{2}+s_{3}), \\\\\ne_{4} &={\\frac {1}{4}}(e_{3}s_{1}-e_{2}s_{2}+e_{1}s_{3}-s_{4}), \\\\\n& \\vdots\n\\end{aligned}\n`}\n\ntogether with the polynomial ${$$`\n\\prod _{i=1}^{Z}\\left(z-z_{i}\\right)=\\sum _{k=0}^{Z}(-1)^{k}e_{k}z^{Z-k}.\n`}\n\nThe lefthand side, by virtue of its definition, is a polynomial containing the unknown zeros we're trying to find. The righthand side is a polynomial we may compute by plugging our known ${$`s_N`} into the inversion Newton's identities gives us. From there, any [lovely polynomial root-finder](https://github.com/scijs/durand-kerner) will complete the job and yield the roots of the complex function!\n\nRestating this all in a much simpler manner, *the method of Delves and Lyness converts the problem of finding roots in the *interior* of a closed region to a problem of analyzing values on the *boundary* of that region*. What's most amazing to me is that the computed polynomial contains the roots of ${$`f(z)`} even though the function itself doesn't have to be any sort of simple polynomial.\n\nThe plot below implements this method. For the circular contour, ${$`s_0 = Z`} values of ${$`s_1, s_2, \\ldots, s_Z`} are computed and used to construct a polynomial whose roots coincide with the roots inside the contour.\n\nObserve that if you ensnare enough roots, it gets pretty unstable or fails entirely.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1101,"value":"function newtonsIdentities (er, ei, M, p) {\n  var i, j, j2, sgn, sgn2;\n  // Use Newton's Identities to construct a polynomial, the roots of\n  // which match our complex analytic function:\n  er[0] = 1;\n  ei[0] = 0;\n  er[1] = p[0];\n  ei[1] = p[1];\n\n  for (i = 2, sgn = -1; i <= M; i++, sgn = -sgn) {\n    er[i] = p[2 * i - 2] * sgn;\n    ei[i] = p[2 * i - 1] * sgn;\n    for (j = 1, sgn2 = -sgn; j < i; j++, sgn2 = -sgn2) {\n      j2 = 2 * (i - j - 1);\n      er[i] += (er[j] * p[j2] - ei[j] * p[j2 + 1]) * sgn2;\n      ei[i] += (er[j] * p[j2 + 1] + ei[j] * p[j2]) * sgn2;\n    }\n    er[i] /= i;\n    ei[i] /= i;\n  }\n\n  sgn = M % 2 === 0 ? 1 : -1;\n  for (i = 0; i <= M; i++, sgn = -sgn) {\n    er[i] *= sgn;\n    ei[i] *= sgn;\n  }\n  er.reverse();\n  ei.reverse();\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1140,"value":"findComplexRoots = {\n  var out = new Array(2);\n  var params = {};\n  \n  function cmod (a, b) {\n    var c\n    var aa = Math.abs(a)\n    var ab = Math.abs(b)\n    if (aa === 0 && ab === 0) return 0\n    if (aa >= ab) {\n      c = b / a\n      return aa * Math.sqrt(1 + c * c)\n    }\n    c = a / b\n    return ab * Math.sqrt(1 + c * c)\n  }\n  \n  function cdiv (out, a, b, c, d ) {\n    var e, f\n    if (Math.abs(c) >= Math.abs(d)) {\n      e = d / c\n      f = 1 / (c + d * e)\n      out[0] = (a + b * e) * f\n      out[1] = (b - a * e) * f\n    } else {\n      e = c / d\n      f = 1 / (c * e + d)\n      out[0] = (a * e + b) * f\n      out[1] = (b * e - a) * f\n    }\n    return out\n  }\n  \n  var dzrsub = [\n    0, 0.81, 0.5727564927611035, 0, -0.5727564927611035,\n    -0.81, -0.5727564927611035, 0, 0.5727564927611035\n  ];\n\n  var dzisub = [\n    0, 0, 0.5727564927611035, 0.81, 0.5727564927611035, 0,\n    -0.5727564927611035, -0.81, -0.5727564927611035\n  ];\n\n  var rsub = [\n    0.5, 0.4166666666666667, 0.4166666666666667, 0.4166666666666667,\n    0.4166666666666667, 0.4166666666666667, 0.4166666666666667,\n    0.4166666666666667, 0.4166666666666667\n  ];\n\n  function s0Integrand (theta) {\n    var c, s, a, b, rtmp, itmp;\n\n    c = Math.cos(theta);\n    s = Math.sin(theta);\n    a = params.z0r + c * params.r;\n    b = params.z0i + s * params.r;\n\n    // tmp <- f(z)\n    params.f(out, a, b);\n    rtmp = out[0];\n    itmp = out[1];\n    \n    // out <- f'(z)\n    out[0] = out[2];\n    out[1] = out[3];\n\n    // out /= f(z)\n    cdiv(out, out[0], out[1], rtmp, itmp);\n\n    // re(tmp * dz / (2 pi i))\n    return out[0] * c - out[1] * s;\n  }\n  \n  function smIntegrand (out, theta) {\n    var c, s, a, b, rtmp, itmp;\n\n    c = Math.cos(theta);\n    s = Math.sin(theta);\n\n    // out <- f(z)\n    params.f(out, params.z0r + c * params.r, params.z0i + s * params.r);\n\n    // out <- f'(z) / f(z)\n    cdiv(out, out[2], out[3], out[0], out[1]);\n\n    // out *= dz   (/ (2 pi) but leave this part until outside the loop)\n    rtmp = out[0];\n    out[0] = out[0] * c - out[1] * s;\n    out[1] = rtmp * s + out[1] * c;\n\n    // out *= z\n    rtmp = out[0];\n    out[0] = (out[0] * c - out[1] * s);\n    out[1] = (rtmp * s + out[1] * c);\n\n    // Recursively multiply by z to compute successive moments z^N:\n    //   out[i] = out[i - 1] * z\n    for (var i = 2; i < 2 * params.M; i += 2) {\n      out[i] = out[i - 2] * c - out[i - 1] * s;\n      out[i + 1] = out[i - 2] * s + out[i - 1] * c;\n    }\n    return out\n  }\n\n  function locateZeros (qr, qi, f, zr, zi, r, z0r, z0i, r0, tol, maxIntegrationDepth, s0IntegrationTolerance, s0IntegrationDepth, curDepth, maxRootsPerCircle, searchCircleOutput) {\n    var p, s0, er, ei, i, j, qri, qii, qmod, qC, M, rr;\n    var qNr, qNi, tmp, qNew, success;\n\n    // Exit if we've reached the recursion limit:\n    if (curDepth <= 0) {\n      return false;\n    }\n    \n    // Exit early if this circle is completely outside the search region\n    if (cmod(zr - z0r, zi - z0i) - r > r0) {\n      return true\n    }\n\n    params.z0r = zr;\n    params.z0i = zi;\n    params.r = r;\n    params.f = f;\n\n    s0 = adaptiveSimpson(s0Integrand, 0, Math.PI * 2, s0IntegrationTolerance, s0IntegrationDepth) * r * (0.5 / Math.PI);\n    M = Math.round(s0);\n\n    qC = 0;\n    for (i = qr.length - 1; i >= 0; i--) {\n      // Count this zero if it's inside the circle:\n      if (cmod(qr[i] - zr, qi[i] - zi) < r) qC++;\n    }\n\n    if (M - qC <= 0.9 || s0 < 0.9) {\n    } else if (Math.abs(s0 - M) > 1e-1 || M - qC > maxRootsPerCircle) {\n      // If this happens, then we had a pretty inaccurate integration:\n      for (i = 0, success = true; i < 9; i++) {\n        rr = r * rsub[i];\n        success = locateZeros(qr, qi, f,\n          zr + r * dzrsub[i],\n          zi + r * dzisub[i],\n          rr, z0r, z0i, r0, tol, maxIntegrationDepth, s0IntegrationTolerance, s0IntegrationDepth, curDepth - 1, maxRootsPerCircle, searchCircleOutput\n        ) && success;\n      }\n    } else {\n      // Compute the zeros:\n      params.M = M - qC;\n      var p = vectorAdaptiveSimpson([], smIntegrand, 0, Math.PI * 2, tol, maxIntegrationDepth, 2 * params.M);\n\n      // post-multiply the integral by this constant factor\n      var f = r * 0.5 / Math.PI;\n      for (var i = 2 * params.M - 1; i >= 0; i--) {\n        p[i] *= f;\n      }\n      \n      // Deflate the result by known zeros inside the current circle:\n      for (i = qr.length - 1; i >= 0; i--) {\n        // Scale to the unit circle in which we're working\n        qri = (qr[i] - zr) / r;\n        qii = (qi[i] - zi) / r;\n        if (cmod(qri, qii) < 1) {\n          // Initialize the powers of qN\n          var qNr = qri;\n          var qNi = qii;\n\n          for (j = 0, qNr = 1, qNi = 0; j < p.length; j += 2) {\n            tmp = qNr;\n            qNr = tmp * qri - qNi * qii;\n            qNi = tmp * qii + qNi * qri;\n            \n            // Subtract off the power of the zero:\n            p[j] -= qNr;\n            p[j + 1] -= qNi;\n          }\n        }\n      }\n\n      // Apply Newton's Identities, constructing a polynomial sharing the roots\n      // of the analytic function:\n      er = [];\n      ei = [];\n      newtonsIdentities(er, ei, params.M, p);\n\n      // Compute the roots of the constructed polynomial:\n      qNew = findRoots(er, ei, 100 * params.M * params.M, tol);\n            \n      // Append and filter to zeros inside the original contour:\n      for (i = qNew[0].length - 1; i >= 0; i--) {\n        var qrRescaled = zr + qNew[0][i] * r;\n        var qiRescaled = zi + qNew[1][i] * r;\n        //if (cmod(qrRescaled - z0r, qiRescaled - z0i) < r0) {\n          qr.push(qrRescaled);\n          qi.push(qiRescaled);\n        //}\n      }\n    }\n    \n    if (searchCircleOutput) {\n      searchCircleOutput.push({z: [zr, zi], r: r, text: 's₀ = ' + s0.toFixed(5).toString().replace(/(\\.[0-9]*[1-9]|\\.0)0*$/,'$1').replace(/\\.0$/,'').replace(/^-0$/, '0')})\n    }\n\n    return [qr, qi];\n  }\n  \n  return function delvesLyness (f, z0, r, opts) {\n    opts = opts || {}\n    var s0IntegrationDepth = opts.s0IntegrationDepth === undefined ? 13 : opts.s0IntegrationDepth\n    var s0IntegrationTolerance = opts.s0IntegrationTolerance === undefined ? 1e-6 : opts.s0IntegrationTolerance\n    var maxIntegrationDepth = opts.maxIntegrationDepth === undefined ? 12 : opts.maxIntegrationDepth\n    var integrationTolerance = opts.integrationTolerance === undefined ? 1e-8 : opts.integrationTolerance\n    var maxSearchDepth = opts.maxSearchDepth === undefined ? 4 : opts.maxSearchDepth\n    var maxRootsPerCircle = opts.maxRootsPerCircle === undefined ? 4 : opts.maxRootsPerCircle\n    if (opts.searchOutput) opts.searchOutput.length = 0;\n    \n    var foundRoots = locateZeros([], [], f, z0[0], z0[1], r, z0[0], z0[1], r, integrationTolerance, maxIntegrationDepth, s0IntegrationTolerance, s0IntegrationDepth, maxSearchDepth, maxRootsPerCircle, opts.searchOutput)\n    \n    var output = [];\n    for (var i = 0; i < foundRoots[0].length; i++) {\n      if (cmod(foundRoots[0][i] - z0[0], foundRoots[1][i] - z0[1]) <= r) {\n        output.push([foundRoots[0][i], foundRoots[1][i]])\n      }\n    }\n    return output;\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3970,"value":"{\n    const {stack, regl, xScale, yScale, currentXScale, currentYScale, container, configureRegl, viewport, searchCircle} = momentMethodPlot;\n\n  const svg = d3.select(stack.layers.svg)\n  svg.selectAll('g.plot, g.axes, defs').remove();\n  const plot = svg.append('g')\n    .attr('class', 'plot')\n    .attr('clip-path', 'url(#clip-rect)');\n  \n  var searchOutput = []\n  var roots\n  \n  function computeRoots () {\n    roots = findComplexRoots(fLambWave.f, searchCircle.z, searchCircle.r, {\n      integrationTolerance:  1e-11,\n      maxIntegrationDepth: 11,\n      searchOutput,\n      maxRootsPerCircle: Infinity,\n      maxSearchDepth: 1,\n    })\n  }\n    \n  let axes = svg.append('g').attr('class', 'axes');\n  const gRoot = plot.append('g').attr('class', 'roots');\n  const gSearchCircles = plot.append('g').attr('class', 'searchCircles');\n  let handles = plot.append('g').attr('class', 'handles')\n  \n  svg.append('defs')\n    .append('clipPath').attr('id', 'clip-rect')\n    .append('rect')\n      .attr('x', viewport.margin.l)\n      .attr('y', viewport.margin.t)\n      .attr('width', viewport.width - viewport.margin.l - viewport.margin.r)\n      .attr('height', viewport.height - viewport.margin.t - viewport.margin.b);\n    \n  let searchCircles\n  \n  function updateRoots () {\n    gRoot.selectAll('g.root').remove()\n    const enter = gRoot.selectAll('g.root')\n      .data(roots)\n      .enter().append('g').attr('class', 'root')\n    enter.append('circle')\n      .attr('r', 5)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(255, 255, 255, 0.7)')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]))\n    enter.append('circle')\n      .attr('r', 3)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(0, 0, 0, 1)')\n    enter.selectAll('circle')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]))\n    \n    if (!debugRootFinder) {\n      searchOutput = [{z: searchCircle.z, r: searchCircle.r, text: ''}]\n    }\n      \n    let searchCircleSelection = gSearchCircles.selectAll('g.searchCircle')\n    .data(searchOutput)\n    .join(enter => {\n      var appended = enter.append('g')\n      .attr('class', 'searchCircle')\n      appended.append('circle')\n        .attr('stroke-width', 1)\n        .attr('stroke', 'rgba(0, 0, 0, 0.8)')\n        .attr('fill', 'none');\n      appended.append('text')\n        .attr(\"font-style\", \"italic\")\n        .attr(\"font-size\", \"17px\")\n        .attr('dx', 10)\n        .attr('alignment-baseline', 'middle')\n        .attr('stroke', 'rgba(255, 255, 255, 0.7)')\n        .attr('stroke-width', 3)\n        .attr('paint-order', 'stroke')\n        .attr('stroke-linecap', 'butt')\n        .attr('stroke-linejoin', 'miter')\n        .attr('stroke-miterlimit', 2);\n    })\n    searchCircleSelection.exit().remove()\n\n    gSearchCircles.selectAll('g.searchCircle')\n      .select('circle')\n      .attr('cx', d => currentXScale(d.z[0]))\n      .attr('cy', d => currentYScale(d.z[1]))\n      .attr('r', d => currentXScale(d.r) - currentXScale(0))\n      .attr('stroke', d => {\n        return 'rgba(0, 0, 0, 1.0)'\n      });\n    gSearchCircles.selectAll('g.searchCircle').select('text')\n      .attr('x', d => currentXScale(d.z[0]))\n      .attr('y', d => currentYScale(d.z[1]))\n      .text(d => d.text)\n    \n    handles.selectAll('circle')\n      .attr('cx', d => currentXScale(searchCircle.z[0] + (d === 'z0' ? 0 : searchCircle.r)))\n      .attr('cy', d => currentYScale(searchCircle.z[1]))\n  }\n    \n  function updateAxes () {\n    axes.call(viewportAxes(viewport, currentXScale, currentYScale))\n  }\n  \n  const drawFunction = createDrawFunction(regl, fLambWave)\n  \n  function updatePlot () {\n    updateRoots()\n  }\n  \n  function updateFunction () {\n    regl.poll()\n    regl.clear({color: [1, 1, 1, 1]})\n    configureRegl.viewport(viewport, () => {\n      configureRegl.scale(currentXScale, currentYScale, () => {\n        configureRegl.map(() => {\n          drawFunction({ω2, γ2})\n        })\n      })\n    })\n  }\n  \n  handles.selectAll('g.handles')\n    .data(['z0', 'r0'])\n    .enter().append('g')\n      .attr('class', 'handle')\n      .call(el => {\n        el.append('circle')\n          .attr('r', 6)\n          .attr('stroke-width', 2)\n          .attr('stroke', 'rgba(255, 255, 255, 0.9)')\n          .attr('fill', '#35e')\n        el.append('circle')\n          .attr('cursor', 'move')\n          .attr('r', 40)\n          .attr('stroke-width', 0)\n          .attr('stroke', 'transparent')\n          .attr('fill', 'transparent')\n          .call(d3.drag().on('drag', d => {\n            if (d === 'r0') {\n              searchCircle.r = currentXScale.invert(d3.event.x) - searchCircle.z[0];\n            } else {\n              searchCircle.z[0] = currentXScale.invert(d3.event.x)\n              searchCircle.z[1] = currentYScale.invert(d3.event.y)\n            }\n            computeRoots()\n            updatePlot()\n          }));\n      })\n    \n  svg.call(persistentZoom(currentXScale, currentYScale, xScale, yScale)\n        .scaleExtent([1, 1])\n        .on('zoom.axes', updateAxes)\n        .on('zoom.plot', updatePlot)\n        .on('zoom.fn', updateFunction))\n  \n  computeRoots()\n  updateAxes();\n  updatePlot();\n  updateRoots()\n  updateFunction()\n  \n  yield stack.container\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3967,"value":"momentMethodPlot = createPlot({\n  xrange: [-12, 12],\n  yrange: [-10, 10],\n  extras: {searchCircle: {z: [0, 0], r: 5}}\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":4509,"value":"md`## Putting it all together\n\nIf we assemble the pieces we've learned so far—subdivision, contour integration to compute zeros, and Newton iteration to refine _pretty good_ guesses—we have something that starts to work. Our procedure looks like this:\n\n1. Compute ${$`s_0`} for a given contour\n2. If there are too many zeros or ${$`s_0`} is not close to an integer, subdivide the contour\n3. For each subdivided contour, compute ${$`s_1, s_2, \\ldots, s_Z`}\n4. Use Newton's identities to construct a polynomial which shares the roots of the complex function\n5. Pass the constructed polynomial to a standard polynomial root-finder.\n6. Refine the computed roots with Newton's method.\n\nNaturally, this sequence of steps glosses over a number of little details like ensuring we don't accidentally compute the same zeros twice. Additionally, since we deal in large powers of ${$`z`}, it's better to integrate around the *unit* circle and transform variables to compute ${$`s_N`}. Finally, Ioakidimis et al. suggest avoiding the need for the function's derivative in their paper [A Modification of the Delves-Lyness Method for Locating the Zeros of Analytic Functions](https://www.sciencedirect.com/science/article/pii/00219991859012510). It sounds generally promising, but I haven't implemented that extension here.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3608,"value":"{\n  const {\n    stack,\n    regl,\n    xScale,\n    yScale,\n    currentXScale,\n    currentYScale,\n    container,\n    configureRegl,\n    viewport,\n    searchCircle\n  } = delvesLynessPlot;\n\n  const svg = d3.select(stack.layers.svg);\n  svg.selectAll('g.plot, g.axes, defs').remove();\n  const plot = svg\n    .append('g')\n    .attr('class', 'plot')\n    .attr('clip-path', 'url(#clip-rect)');\n\n  var searchOutput = [];\n  var roots;\n\n  function computeRoots() {\n    roots = findComplexRoots(fLambWave.f, searchCircle.z, searchCircle.r, {\n      integrationTolerance,\n      maxIntegrationDepth,\n      searchOutput,\n      maxRootsPerCircle,\n      maxSearchDepth\n    });\n\n    if (refineRoots) {\n      for (let i = 0; i < roots.length; i++) {\n        complexNewtonRaphson(roots[i], fLambWave.f, roots[i]);\n      }\n    }\n  }\n\n  let axes = svg.append('g').attr('class', 'axes');\n  const gRoot = plot.append('g').attr('class', 'roots');\n  const gSearchCircles = plot.append('g').attr('class', 'searchCircles');\n  let handles = plot.append('g').attr('class', 'handles');\n\n  svg\n    .append('defs')\n    .append('clipPath')\n    .attr('id', 'clip-rect')\n    .append('rect')\n    .attr('x', viewport.margin.l)\n    .attr('y', viewport.margin.t)\n    .attr('width', viewport.width - viewport.margin.l - viewport.margin.r)\n    .attr('height', viewport.height - viewport.margin.t - viewport.margin.b);\n\n  let searchCircles;\n\n  function updateRoots() {\n    gRoot.selectAll('g.root').remove();\n    const enter = gRoot\n      .selectAll('g.root')\n      .data(roots)\n      .enter()\n      .append('g')\n      .attr('class', 'root');\n    enter\n      .append('circle')\n      .attr('r', 5)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(255, 255, 255, 0.7)')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]));\n    enter\n      .append('circle')\n      .attr('r', 3)\n      .attr('stroke-width', 0)\n      .attr('stroke', 'transparent')\n      .attr('fill', 'rgba(0, 0, 0, 1)');\n    enter\n      .selectAll('circle')\n      .attr('cx', d => currentXScale(d[0]))\n      .attr('cy', d => currentYScale(d[1]));\n\n    if (!debugRootFinder) {\n      searchOutput = [{ z: searchCircle.z, r: searchCircle.r, text: '' }];\n    }\n\n    let searchCircleSelection = gSearchCircles\n      .selectAll('g.searchCircle')\n      .data(searchOutput)\n      .join(enter => {\n        var appended = enter.append('g').attr('class', 'searchCircle');\n        appended\n          .append('circle')\n          .attr('stroke-width', 1)\n          .attr('stroke', 'rgba(0, 0, 0, 0.8)')\n          .attr('fill', 'none');\n        appended\n          .append('text')\n          .attr(\"font-style\", \"italic\")\n          .attr(\"font-size\", \"17px\")\n          .attr('dx', 10)\n          .attr('alignment-baseline', 'middle')\n          .attr('stroke', 'rgba(255, 255, 255, 0.7)')\n          .attr('stroke-width', 3)\n          .attr('paint-order', 'stroke fill')\n          .attr('stroke-linecap', 'butt')\n          .attr('stroke-linejoin', 'miter')\n          .attr('stroke-miterlimit', 2);\n      });\n    searchCircleSelection.exit().remove();\n\n    gSearchCircles\n      .selectAll('g.searchCircle')\n      .select('circle')\n      .attr('cx', d => currentXScale(d.z[0]))\n      .attr('cy', d => currentYScale(d.z[1]))\n      .attr('r', d => currentXScale(d.r) - currentXScale(0))\n      .attr('stroke', d => {\n        var r = currentXScale(d.r) - currentXScale(0);\n        return (r < 100 || r > 200) && debugRootFinder\n          ? 'rgba(0, 0, 0, 0.2)'\n          : 'rgba(0, 0, 0, 1.0)';\n      });\n    gSearchCircles\n      .selectAll('g.searchCircle')\n      .select('text')\n      .attr('x', d => currentXScale(d.z[0]))\n      .attr('y', d => currentYScale(d.z[1]))\n      .text(d => {\n        var r = currentXScale(d.r) - currentXScale(0);\n        if (r < 100 || r > 200) return '';\n        return d.text;\n      });\n\n    handles\n      .selectAll('circle')\n      .attr('cx', d =>\n        currentXScale(searchCircle.z[0] + (d === 'z0' ? 0 : searchCircle.r))\n      )\n      .attr('cy', d => currentYScale(searchCircle.z[1]));\n  }\n\n  function updateAxes() {\n    axes.call(viewportAxes(viewport, currentXScale, currentYScale));\n  }\n\n  const drawFunction = createDrawFunction(regl, fLambWave);\n\n  function updatePlot() {\n    updateRoots();\n\n    regl.poll();\n    regl.clear({ color: [1, 1, 1, 1] });\n    configureRegl.viewport(viewport, () => {\n      configureRegl.scale(currentXScale, currentYScale, () => {\n        configureRegl.map(() => {\n          drawFunction({ ω2, γ2 });\n        });\n      });\n    });\n  }\n\n  handles\n    .selectAll('g.handles')\n    .data(['z0', 'r0'])\n    .enter()\n    .append('g')\n    .attr('class', 'handle')\n    .call(el => {\n      el.append('circle')\n        .attr('r', 6)\n        .attr('stroke-width', 2)\n        .attr('stroke', 'rgba(255, 255, 255, 0.9)')\n        .attr('fill', '#35e');\n      el.append('circle')\n        .attr('cursor', 'move')\n        .attr('r', 40)\n        .attr('stroke-width', 0)\n        .attr('stroke', 'transparent')\n        .attr('fill', 'transparent')\n\n        .call(\n          d3.drag().on('drag', d => {\n            if (d === 'r0') {\n              searchCircle.r =\n                currentXScale.invert(d3.event.x) - searchCircle.z[0];\n            } else {\n              searchCircle.z[0] = currentXScale.invert(d3.event.x);\n              searchCircle.z[1] = currentYScale.invert(d3.event.y);\n            }\n            computeRoots();\n            updatePlot();\n          })\n        );\n    });\n\n  svg.call(\n    persistentZoom(currentXScale, currentYScale, xScale, yScale)\n      .scaleExtent([0.5, 30])\n      .on('zoom.axes', updateAxes)\n      .on('zoom.plot', updatePlot)\n  );\n\n  computeRoots();\n  updateAxes();\n  updatePlot();\n  updateRoots();\n\n  yield stack.container;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3953,"value":"delvesLynessPlot = createPlot({\n  xrange: [-15, 15],\n  yrange: [-10, 10],\n  extras: {searchCircle: {z: [0, 0], r: 5}}\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":2010,"value":"viewof debugRootFinder = checkbox({options: [{label: 'Visualize search hierarchy', value: 'debug'}], value: 'debug'})","pinned":false,"mode":"js","data":null,"name":null},{"id":2946,"value":"viewof refineRoots = checkbox({options: [{label: 'Refine roots with Newton-Raphson', value: 'refine'}], value: 'refine'})","pinned":false,"mode":"js","data":null,"name":null},{"id":2552,"value":"viewof maxRootsPerCircle = slider({min: 1, max: 16, step: 1, value: 5, description: 'maximum number of roots per search circle'})","pinned":false,"mode":"js","data":null,"name":null},{"id":2741,"value":"viewof maxSearchDepth = slider({min: 1, max: 16, step: 1, value: 7, description: 'maximum search depth'})","pinned":false,"mode":"js","data":null,"name":null},{"id":2591,"value":"viewof maxIntegrationDepth = slider({min: 4, max: 20, step: 1, value: 10, description: 'adaptive integration recursion limit'})","pinned":false,"mode":"js","data":null,"name":null},{"id":2596,"value":"viewof integrationTolerance = {\n  const form = html`<form>\n    <input name=i type=range min=1 max=14 step=0.1 value=7 style=\"width:240px;\">\n    <output style=\"font: 14px Menlo, Consolas, monospace; margin-left: 0.5em;\" name=o></output>\n    <div style=\"font-size: 0.85rem; font-style: italic;\">adaptive integration tolerance</div>\n  </form>`;\n  form.i.oninput = () => form.o.value = `${(form.value = Math.pow(10, -form.i.valueAsNumber).toExponential(2))}`;\n  form.i.oninput();\n  return form;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":4774,"value":"md`## Where do we go from here?\nSo this is something. We found some roots without meandering about too haphazardly in the complex plane. Is the algorithm good? To be honest, it's not great. It's not the worst, but it's still inefficient compared to the state of the art, the inversion is, in general, ill-conditioned, and it struggles to distinguish what's actually present even for relatively straightforward cases (like poles or square roots).\n\nWhat could we improve? Well for one, we've integrated the function directly with a pretty standard [adaptive numerical integrator](https://observablehq.com/@rreusser/integration). In doing that, we've sampled points *non-uniformly* around the unit circle. What if we instead sampled the points *uniformly* around the unit circle?\n\nIf you're familiar with the [Fast Fourier Transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform) (FFT), talk of complex values uniformly spaced around the unit circle (\"roots of unity\") might perhaps ring a bell. Suddenly our analysis puts us FFT land, which is always a good sign. The bottom line is that it's a deep rabbit hole, and more sophisticated methods take advantage of additional structure to compute more valuable information with fewer function evaluations.\n\n[Jacob Rus](https://observablehq.com/@jrus) has directed me to some excellent papers including Austin et al.'s [Numerical algorithms based on analytic function values at roots of unity](http://people.maths.ox.ac.uk/trefethen/publication/PDF/2015_153.pdf) and Nakatsukasa et al.'s [The AAA algorithm for rational approximation](https://arxiv.org/pdf/1612.00337.pdf). They even include concise (though not permissibly licensed, I believe?) code to implement the methods.\n\nI really hope this has been interesting and has provided a small window into the intuition and mental picture behind some interesting math, but as far as writing numerical software goes, the rabbit hole is deep and I'll keep my day job for now.\n\nIn the future, I'll use this technique to help explore the physical waves the pictured equation represents.`","pinned":false,"mode":"js","data":null,"name":null},{"id":5311,"value":"md`## References\n- L. M. Delves, J. N. Lyness, [A Numerical Method for Locating the Zeros of an Analytic Function](https://pdfs.semanticscholar.org/1237/c0fabdd4f8c530e3b9453ce9e44e6607cedc.pdf). 1967.\n- N. I. Ioakidimis, E. G. Anastasselou, [A Modification of the Delves-Lyness Method for Locating the Zeros of Analytic Functions](https://www.sciencedirect.com/science/article/pii/0021999185901251). 1985.\n- A. Austin, P. Kravanja, L. Trefethen, [Numerical Algorithms Based on Analytic Function Values at Roots of Unity](http://people.maths.ox.ac.uk/trefethen/publication/PDF/2015_153.pdf). 2014.\n- Y. Nakatsukasa, O. Sète, L. Trefethen, [The AAA Algorithm for Rational Approximation](https://arxiv.org/pdf/1612.00337.pdf). 2017.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3157,"value":"md`## Addendum: The Lamb wave dispersion relation as a fun equation to work with\n\nWe've been working with an abstract function, but it's not just a made up function. [Lamb waves](https://en.wikipedia.org/wiki/Lamb_waves) are planar elastodynamic waves that travel in a plate. For example, when you strike a metal plate and sound reverberates throughout—that's Lamb waves.\n\nWaves in unbounded media (like sound waves) are simple and may travel at any frequency. Waves in bounded media (like a metal plate) are significantly more complicated. The boundary conditions enforce constraints on which waves can travel so that only isolated spatial frequencies, or *modes*, may exist.\n\nA [dispersion relation](https://en.wikipedia.org/wiki/Dispersion_relation) relates spatial and temporal frequencies of traveling waves. The dispersion relation for sound in air is effectively just the speed of sound. For Lamb waves at a given temporal frequency, the dispersion relation describes the spatial frequencies of waves that can travel in the plate. The dispersion relations for symmetric (${$`+1`}) and antisymmetric (${$`-1`}) Lamb waves are given by\n\n${$$`\\displaystyle\\frac{\\tanh(\\beta d / 2)}{\\tanh(\\alpha d / 2)}\n= \\left( \\frac{4 \\alpha \\beta k^2}{(k^2 + \\beta^2)^2} \\right)^{\\pm 1}`}\n\nwith \n\n${$$`\\displaystyle \\alpha^2 = k^2 - \\frac{\\omega^2}{c_l^2},`}\n${$$`\\beta^2 = k^2 - \\frac{\\omega^2}{c_t^2}`}\n\nwhere ${tex`d`} is the thickness of the plate, ${tex`k`} the wavenumber in the plane of the plate, ${tex`\\omega`} the temporal frequency, ${tex`c_l`} the longitudinal wave speed, and ${tex`c_t`} the shear (transverse) wave speed.\n\nAs noted above, we of course don't actually implement this function directly but cross-multiply to remove any possible poles. The branch cuts resulting from the implicit square roots in ${$`\\alpha`} and ${$`\\beta`} are somewhat more problematic. Squaring the entire equation would resolve the branch cuts at the cost of repeating all other roots, so we grin and bear the branch cuts.\n\nWavenumbers ${tex`k`} which satisfy this equation represent the different superpositions of bulk waves that satisfy the boundary conditions of the plate. That is, in order to determine which waves are valid, we must find the zeros of this equation in the complex plane.`","pinned":false,"mode":"js","data":null,"name":null},{"id":91,"value":"viewof f = slider({min: 100, max: 2e6, step: 100, value: 500e3, description: 'frequency, f, Hz'})","pinned":false,"mode":"js","data":null,"name":null},{"id":22,"value":"viewof ν = slider({min: 0.0, max: 0.49, step: 0.01, value: 0.3, description: \"Poisson's ratio, ν\"})","pinned":false,"mode":"js","data":null,"name":null},{"id":26,"value":"viewof θ = slider({min: 0.0, max: Math.PI, step: 0.01, value: 0.3, description: 'Viscoelastic loss angle, radians'})","pinned":false,"mode":"js","data":null,"name":null},{"id":988,"value":"viewof parity = radio({\n  options: ['Symmetric', 'Antisymmetric'],\n  value: 'Symmetric',\n  description: 'Lamb wave parity'\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":5286,"value":"md`Plate thickness, ${$`d`}, in meters`","pinned":false,"mode":"js","data":null,"name":null},{"id":13,"value":"d = 10e-3 // meters","pinned":false,"mode":"js","data":null,"name":null},{"id":5291,"value":"md`Density, ${$`\\rho`} in ${$`kg/m^3`}`","pinned":false,"mode":"js","data":null,"name":null},{"id":9,"value":"ρ = 2700 // kg / m^3","pinned":false,"mode":"js","data":null,"name":null},{"id":5323,"value":"md`Magnitude of Young's modulus, ${$`E`}, in ${$`Pa`}`","pinned":false,"mode":"js","data":null,"name":null},{"id":963,"value":"Emag = 68.9e9 // Pascals","pinned":false,"mode":"js","data":null,"name":null},{"id":5352,"value":"md`[Complex Young's modulus](https://en.wikipedia.org/wiki/Dynamic_modulus). The complex part here accounts for viscoelasticity.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1346,"value":"E = cscaleS([], [Math.cos(θ), Math.sin(θ)], Emag)","pinned":true,"mode":"js","data":null,"name":null},{"id":5329,"value":"md`Longitudinal wave speed, ${$`c_l`}, in ${$`m/s`}. Note that of course speeds in real world must be real-valued. The imaginary part here represents spatially decaying wave modes.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1360,"value":"cl = cscaleS([], csqrtS([], E), Math.sqrt((1 - ν) / ρ / (1 + ν) / (1 - 2 * ν)))","pinned":true,"mode":"js","data":null,"name":null},{"id":5334,"value":"md`Shear (tangential) wave speed, ${$`c_t`}, in ${$`m/s`}.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1367,"value":"ct = cscaleS([], csqrtS([], E), 1 / Math.sqrt(2 * ρ * (1 + ν)))","pinned":true,"mode":"js","data":null,"name":null},{"id":1387,"value":"ω2 = csqrS([], cscaleS([], cinvS([], cl), 2.0 * Math.PI * f * d)) ","pinned":true,"mode":"js","data":null,"name":null},{"id":1394,"value":"γ2 = csqrS([], cdivSS([], cl, ct))","pinned":true,"mode":"js","data":null,"name":null},{"id":101,"value":"fLambWave = ({\n  glsl: `\n// This function puts the branch cut on the other side of the square\n// root, away from most of the function we're looking at. It's\n// mathematically equivalent to csqrt though.\ncomplex csqrtAlt (complex a) {\n  return cmul(vec2(0, 1), csqrt(-a));\n}\n\n// We write this function generically and replace 'complex' with either\n// vec2 or vec4, depending on whether we're using automatic differentiation\ncomplex f (complex k2) {\n  k2 = csqr(k2);\n\n  complex alpha2 = csub(k2, w2);\n  complex beta2 = csub(k2, cmul(w2, gamma2));\n  complex alpha = csqrtAlt(alpha2);\n  complex beta = csqrtAlt(beta2);\n  \n  complex eb = cexp(beta);\n  complex enb = cexp(-beta);\n  complex ea = cexp(alpha);\n  complex ena = cexp(-alpha);\n\n  // Division by two cancels out, so we omit it\n  complex sinhb2 = (eb - enb);\n  complex coshb2 = (eb + enb);\n  complex sinha2 = (ea - ena);\n  complex cosha2 = (ea + ena);\n  \n  complex k2b22 = csqr(cadd(k2, beta2));\n  complex ab4k2 = 4.0 * cmul(cmul(alpha, beta), k2);\n  complex lhs = cmul(cmul(sinhb2, cosha2), ${parity === 'Symmetric' ? 'k2b22' : '-ab4k2'});\n  complex rhs = cmul(cmul(sinha2, coshb2), ${parity === 'Symmetric' ? 'ab4k2' : '-k2b22'});\n  return rhs - lhs;\n}\n`,\n  createUniforms: function (regl) {\n    return {\n      w2: regl.prop('ω2'),\n      gamma2: regl.prop('γ2'),\n    }\n  },\n  f: (function () {\n    var tmp = ccreateD()\n    var k2 = ccreateD()\n    var alpha2 = ccreateD(), beta2 = ccreateD()\n    var alpha = ccreateD(), beta = ccreateD()\n    var sinha = ccreateD(), cosha = ccreateD()\n    var sinhb = ccreateD(), coshb = ccreateD()\n    var ab4k2 = ccreateD(), k2b22 = ccreateD()\n    var lhs = ccreateD(), rhs = ccreateD()\n    var symm = parity === 'Symmetric'\n    return function fLamb (out, a, b) {\n      csetValuesD(k2, a, b, 1, 0)\n      csqrD(k2, k2)\n      csubDS(alpha2, k2, ω2)\n      csubDS(beta2, k2, cmulSS(tmp, ω2, γ2))\n      csqrtD(alpha, alpha2)\n      csqrtD(beta, beta2)\n      csinhcoshD(sinha, cosha, alpha)\n      csinhcoshD(sinhb, coshb, beta)\n      csqrD(k2b22, caddDD(tmp, k2, beta2))\n      cscaleD(ab4k2, cmulDD(tmp, cmulDD(tmp, alpha, beta), k2), 4.0)\n      cmulDD(lhs, cmulDD(tmp, sinhb, cosha), symm ? k2b22 : ab4k2)\n      cmulDD(rhs, cmulDD(tmp, sinha, coshb), symm ? ab4k2 : k2b22)\n      return csubDD(out, lhs, rhs)\n    }\n  })()\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":4406,"value":"md`## Plotting helpers`","pinned":false,"mode":"js","data":null,"name":null},{"id":3920,"value":"function createPlot (config) {\n  var xrange = config.xrange || [-10, 10]\n  var yrange = config.yrange || [-10, 10]\n  var margins = config.margins || {t: 10, r: 5, b: 20, l: 30}\n  \n  const stack = createLayerStack(width, Math.max(500, Math.min(900, width * 0.7)), Math.min(devicePixelRatio), {\n    regl: reglCanvasWithOptions({\n      attributes: {depthStencil: false, antialias: false},\n      optionalExtensions: ['OES_standard_derivatives'],\n    }),\n    svg: DOM.svg\n  })\n  \n  const viewport = createViewport(stack, margins)\n  \n  const yScale = d3.scaleLinear()\n    .domain(xrange)\n    .range([viewport.height - viewport.margin.b, viewport.margin.t])\n  \n  const xScale = constrainLinearScaleAspectRatio(\n    d3.scaleLinear()\n      .domain(yrange)\n      .range([viewport.margin.l, viewport.width - viewport.margin.r]),\n    yScale, 1)\n  \n  const configureRegl = ({\n    viewport: createReglViewportConfiguration(stack.layers.regl),\n    scale: createReglLinearScaleConfiguration(stack.layers.regl),\n    map: createReglMap(stack.layers.regl),\n  })\n\n  return Object.assign({\n    stack,\n    container: stack.container,\n    regl: stack.layers.regl,\n    viewport,\n    xScale,\n    yScale,\n    currentXScale: xScale.copy(),\n    currentYScale: yScale.copy(),\n    configureRegl,\n  }, config.extras)\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":72,"value":"function createDrawFunction(regl, func) {\n  return regl({\n    frag: `\n      ${\n        derivatives === \"OES_standard_derivatives\"\n          ? \"#extension GL_OES_standard_derivatives : enable\"\n          : \"\"\n      }\n      precision highp float;\n\n      uniform vec2 gamma2, w2;\n      uniform sampler2D colormap;\n\n      varying vec2 xy;\n      uniform vec2 inverseViewportResolution;\n      uniform mat4 inverseView;\n      uniform float n;\n\n      ${createPolarDomainColoringShader({\n        magnitudeOctaves: 4,\n        phaseOctaves: 4,\n        argumentColoring: 1.0,\n        bias1: 0.5\n      })}\n\n      ${glslComplexAutoDifferentiation}\n      ${glslRainbow}\n\n      // All of the functions are generalized to work with either vec2 (re, im)\n      // or with vec4 (the function and its derivative). We generalize the type\n      // and nail two birds with one stone.\n      ${func.glsl.replace(\n        /\\bcomplex\\b/g,\n        derivatives === \"OES_standard_derivatives\" ? \"vec2\" : \"vec4\"\n      )}\n\n      void main () {\n        ${\n          derivatives === \"OES_standard_derivatives\"\n            ? `\n          vec2 fz = f(xy);\n          vec4 fdf = vec4(fz, fwidth(fz) * 0.5);\n        `\n            : `\n          float unitsPerPixel = inverseView[0][0] * inverseViewportResolution.x * 2.0;\n          vec4 fdf = f(vec4(vec2(xy), vec2(unitsPerPixel, 0)));\n        `\n        }\n\n        gl_FragColor = mix(vec4(vec3(1.0), 1.0), domainColoring(\n          fdf,\n          vec2(6),   // steps\n          vec2(0.2), // scale\n          vec2(0.2, 0.3), // grid\n          vec2(0.5, 0.1), // shading\n          1.0, 1.0,  // line width, line feather\n          vec3(0.0), // grid color\n          1.0,        // argument coloring\n          1.0,        // contrast power\n          colormap,\n          vec2(0)\n        ), 0.9);\n      }`,\n    uniforms: Object.assign(\n      func.createUniforms ? func.createUniforms(regl) : {},\n      {\n        colormap: createRainbowTexture(regl)\n      }\n    )\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":6113,"value":"import { rainbow } from '481d4902e37d39c8'","pinned":false,"mode":"js","data":null,"name":null},{"id":6115,"value":"createRainbowTexture = regl => regl.texture(rainbow)","pinned":false,"mode":"js","data":null,"name":null},{"id":4833,"value":"function createFractalDrawFunction(regl, func) {\n  return regl({\n    frag: `\n      precision highp float;\n      varying vec2 xy;\n      uniform vec2 inverseViewportResolution;\n      uniform mat4 inverseView;\n      uniform sampler2D colormap;\n\n      ${createPolarDomainColoringShader({\n        magnitudeOctaves: 4,\n        phaseOctaves: 4,\n        argumentColoring: 0.001,\n        bias1: 0.5\n      })}\n      ${glslComplexAutoDifferentiation}\n      ${glslRainbow}\n\n      ${func.glsl.replace(/\\bcomplex\\b/g, \"vec4\")}\n\n      void main () {\n        vec2 z = xy;\n        float lightness = 1.0;\n        for (int i = 0; i < 40; i++) {\n          vec4 fdf = f(vec4(z, 1.0, 0.0));\n          z = z - cdiv(fdf.xy, fdf.zw);\n          lightness *= mix(1.0, 0.988, smoothstep(-2.5, 1.0, log(dot(fdf.xy, fdf.xy))));\n        }\n        lightness *= lightness;\n        lightness *= lightness;\n\n        float unitsPerPixel = inverseView[0][0] * inverseViewportResolution.x * 2.0;\n        vec4 fdf = f(vec4(vec2(xy), vec2(unitsPerPixel, 0)));\n\n        vec3 dc = domainColoring(\n          fdf,\n          vec2(6),   // steps\n          vec2(0.2), // scale\n          vec2(0.2, 0.2), // grid\n          vec2(0.15, 0.0), // shading\n          1.0, 1.0,  // line width, line feather\n          vec3(0.0), // grid color\n          0.0,        // argument coloring\n          1.0,        // contrast power\n          colormap,\n          vec2(0)\n        ).xyz;\n\n        gl_FragColor = vec4(rainbow(atan(z.y, z.x) * 0.5 / 3.14159 + 0.25) * lightness * dc, 1.0);\n      }`,\n    uniforms: Object.assign(\n      func.createUniforms ? func.createUniforms(regl) : {},\n      {\n        colormap: createRainbowTexture(regl)\n      }\n    )\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5489,"value":"md`These helpers in particular are highly experimental. They attempt to handle managing contexts and converting d3 scales into transformation matrices for use with WebGL, but I can't currently recommend using them!`","pinned":false,"mode":"js","data":null,"name":null},{"id":52,"value":"import {\n  reglCanvasWithOptions,\n  createLayerStack,\n  createViewport,\n  viewportAxes,\n  constrainLinearScaleAspectRatio,\n  createReglViewportConfiguration,\n  createReglLinearScaleConfiguration,\n  createReglMap,\n  persistentZoom,\n  mat3ViewportFromLinearScales, mat3fromLinearScales\n} from '@rreusser/regl-tools'","pinned":false,"mode":"js","data":null,"name":null},{"id":76,"value":"import { glslRainbow } from '@rreusser/adaptive-domain-coloring'","pinned":false,"mode":"js","data":null,"name":null},{"id":6109,"value":"import { createPolarDomainColoringShader } from '39fe342decb60c30'","pinned":false,"mode":"js","data":null,"name":null},{"id":4396,"value":"md`## Math helpers`","pinned":false,"mode":"js","data":null,"name":null},{"id":4546,"value":"md`The functions imported below implement a number of complex operations with automatic differentiation. The suffix \\`D\\` refers to dual numbers with the derivative appended while \\`S\\` refers to complex scalars. Only the functions needed so far have been implemented, so the implementation is certainly incomplete.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1232,"value":"import {\n  csqrD,\n  ccopyD,\n  csubDS,\n  ccreateD,\n  ccreateS,\n  cmulSS,\n  csqrtD,\n  csinhcoshD,\n  caddDD,\n  cscaleD,\n  cmulDD,\n  csubDD,\n  cfromValuesD,\n  csetValuesD,\n  cfromValuesS,\n  cscaleS,\n  csqrtS,\n  cinvS,\n  csqrS,\n  cdivSS,\n  cdivDD,\n} from '27199d72f00e07fc'","pinned":false,"mode":"js","data":null,"name":null},{"id":3109,"value":"import {adaptiveSimpson, vectorAdaptiveSimpson} from '@rreusser/integration'","pinned":false,"mode":"js","data":null,"name":null},{"id":1187,"value":"import { findRoots } from '@rreusser/polynomial-roots'","pinned":false,"mode":"js","data":null,"name":null},{"id":78,"value":"import {glslComplexAutoDifferentiation} from '@rreusser/glsl-complex-auto-differentiation'","pinned":false,"mode":"js","data":null,"name":null},{"id":50,"value":"d3 = require('d3@5')","pinned":false,"mode":"js","data":null,"name":null},{"id":89,"value":"import {slider, radio, checkbox} from '@jashkenas/inputs'","pinned":false,"mode":"js","data":null,"name":null},{"id":4451,"value":"$ = tex","pinned":false,"mode":"js","data":null,"name":null},{"id":4448,"value":"$$ = $.block","pinned":false,"mode":"js","data":null,"name":null},{"id":3597,"value":"html`<style>\n.observablehq .katex,\n.observablehq .katex-display>.katex { font-size: 1.08em; }\n\n.observablehq svg {\n  cursor: grab;\n}\n.observablehq svg:active {\n  cursor: grabbing;\n}\n.observablehq p > img {\n  margin-left: auto;\n  margin-right: auto;\n  display: block;\n}\n</style>`","pinned":false,"mode":"js","data":null,"name":null},{"id":3822,"value":"md`## License\n\nWith the exception of code imported from other sources as stated above, the code in this notebook is MIT Licensed.\n\nCopyright 2019 Ricky Reusser\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n`","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}