Skip to content

Commit 7f48904

Browse files
committed
make a dft computation demo too
1 parent a9d77c7 commit 7f48904

File tree

9 files changed

+2020
-60
lines changed

9 files changed

+2020
-60
lines changed

content/applied-math/misc/dft.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,22 @@ $$ c_n = \frac{1}{N} \sum_{k=0}^{N-1} z_k e^{-2 \pi i n k / N} $$
2222
* phase angle = $\Arg(c_n)$
2323

2424
* rotation speed = $n$
25+
26+
27+
Seems to be measuring how well the sample points move around the unit circle at the same rate as the rotation vector?
28+
29+
Let's take $N = 9$ and talk about what happens when our curve is a unit circle.
30+
31+
With frequency $n=0:$ we have no rotations, so, for each sample point, we add the sample point directly to the summation vector. The result is the average of all the sample points, the center of gravity, or DC component. For the unit circle this is $0.$
32+
33+
With frequency $n=-1:$
34+
35+
Starting with $k=0, $ we have $z_0,$ which is at $1.$ Our first rotation doesn't rotate at all, so our summation is now pointing at $1.$
36+
37+
Now at $k=1$, our second sample point $z_1$ is pointing at $\frac{2 \pi}{9}.$ The imaginary component of our exponential $e^{-2 \pi i n k / N}$ gives us
38+
39+
$$ -2 \pi i (-1) (1) / 9} = \frac{2 \pi}{9}. $$
40+
41+
So, our sample point and our rotation are the same. When we multiply them in exponential form, we add the angles, and get $\frac{4 \pi}{9}.$ We take the exponential from our first sample and add it to this exponential, which gives us their average, and we get $\frac{2 \pi}{9}$ as our summation vector so far.
42+
43+
Next at $k=2,$ our sample point is at $\frac{4 \pi}{9}$ and our rotation vector is too. The resulting angle is $\frac{8 \pi}{9}.$ Our summation vector now points at $\frac{5 \pi}{9},$ midway between its previous value and the value of this sample.

demos-framework/src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ const demoRegistry: Record<string, () => Promise<DemoModule>> = {
4747
'cross-product': () => import('@demos/linear-algebra/cross-product'),
4848
'binary-network': () => import('@demos/network/binary-network'),
4949
'cauchy-riemann': () => import('@demos/complex-analysis/cauchy-riemann'),
50-
'contour-drawing': () => import('@demos/complex-analysis/contour-drawing')
50+
'contour-drawing': () => import('@demos/complex-analysis/contour-drawing'),
51+
'dft-computation': () => import('@demos/complex-analysis/dft-computation')
5152
};
5253

5354
// Store for loaded metadata

demos/complex-analysis/cauchy-riemann.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import { DemoInstance, DemoConfig, DemoMetadata } from '@framework/types';
33
// @ts-ignore
44
import Plotly from 'plotly.js-dist-min';
55
import * as math from 'mathjs';
6+
import {
7+
pi as PI, abs, hypot, max as mathMax, min as mathMin, acos, sqrt, ceil, floor, number, Complex
8+
} from 'mathjs';
9+
10+
// Type for numeric mathjs results we know won't be BigNumber/Fraction/Matrix
11+
type NumericResult = number | bigint | Complex;
12+
13+
// Helper to extract real part from a mathjs result (number, bigint, or Complex)
14+
function toReal(val: NumericResult): number {
15+
if (typeof val === 'number') return val;
16+
if (typeof val === 'bigint') return Number(val);
17+
return val.re;
18+
}
619

720
interface Point2D {
821
x: number;
@@ -53,7 +66,7 @@ class CauchyRiemannDemo implements DemoInstance {
5366
// Adaptive sampling parameters
5467
private minSamples: number = 64; // Base samples along each line
5568
private maxRefineLevel: number = 6; // Maximum refinement depth
56-
private angleThreshold: number = Math.PI / 180 * 2; // Refine when turn angle > 2 degrees
69+
private angleThreshold: number = number(PI) / 180 * 2; // Refine when turn angle > 2 degrees
5770

5871
async init(container: HTMLElement, config: DemoConfig): Promise<void> {
5972
this.container = container;
@@ -369,8 +382,8 @@ class CauchyRiemannDemo implements DemoInstance {
369382

370383
private checkCauchyRiemannEquations(partials: PartialDerivatives): boolean {
371384
const tolerance = 0.001;
372-
const eq1_satisfied = Math.abs(partials.dudx - partials.dvdy) < tolerance;
373-
const eq2_satisfied = Math.abs(partials.dvdx + partials.dudy) < tolerance;
385+
const eq1_satisfied = number(abs(partials.dudx - partials.dvdy)) < tolerance;
386+
const eq2_satisfied = number(abs(partials.dvdx + partials.dudy)) < tolerance;
374387
return eq1_satisfied && eq2_satisfied;
375388
}
376389

@@ -433,13 +446,13 @@ class CauchyRiemannDemo implements DemoInstance {
433446
const v0 = { x: pm.x - p0.x, y: pm.y - p0.y };
434447
const v1 = { x: p1.x - pm.x, y: p1.y - pm.y };
435448
const dot = v0.x * v1.x + v0.y * v1.y;
436-
const n0 = Math.hypot(v0.x, v0.y);
437-
const n1 = Math.hypot(v1.x, v1.y);
449+
const n0 = number(hypot(v0.x, v0.y));
450+
const n1 = number(hypot(v1.x, v1.y));
438451

439452
let angle = 0;
440453
if (n0 > 0 && n1 > 0) {
441-
const cosAngle = Math.max(-1, Math.min(1, dot / (n0 * n1)));
442-
angle = Math.acos(cosAngle);
454+
const cosAngle = number(mathMax(-1, mathMin(1, dot / (n0 * n1))));
455+
angle = toReal(acos(cosAngle));
443456
}
444457

445458
if (angle > this.angleThreshold) {
@@ -469,30 +482,30 @@ class CauchyRiemannDemo implements DemoInstance {
469482
horizontalLines: Point2D[][] // Lines with constant y (horizontal in preimage)
470483
} {
471484
const gridStep = 1.0;
472-
const min = -Math.ceil(this.gridRange);
473-
const max = Math.ceil(this.gridRange);
474-
const numLines = Math.floor((max - min) / gridStep) + 1;
485+
const minVal = -number(ceil(this.gridRange));
486+
const maxVal = number(ceil(this.gridRange));
487+
const numLines = number(floor((maxVal - minVal) / gridStep)) + 1;
475488

476489
const verticalLines: Point2D[][] = [];
477490
const horizontalLines: Point2D[][] = [];
478491

479492
// Generate vertical lines (constant x)
480493
for (let i = 0; i < numLines; i++) {
481-
const xConst = min + i * gridStep;
494+
const xConst = minVal + i * gridStep;
482495
const line: Point2D[] = [];
483496
for (let j = 0; j < numLines; j++) {
484-
const y = min + j * gridStep;
497+
const y = minVal + j * gridStep;
485498
line.push({ x: xConst, y: y });
486499
}
487500
verticalLines.push(line);
488501
}
489502

490503
// Generate horizontal lines (constant y)
491504
for (let j = 0; j < numLines; j++) {
492-
const yConst = min + j * gridStep;
505+
const yConst = minVal + j * gridStep;
493506
const line: Point2D[] = [];
494507
for (let i = 0; i < numLines; i++) {
495-
const x = min + i * gridStep;
508+
const x = minVal + i * gridStep;
496509
line.push({ x: x, y: yConst });
497510
}
498511
horizontalLines.push(line);
@@ -577,7 +590,7 @@ class CauchyRiemannDemo implements DemoInstance {
577590
angleref: 'previous',
578591
color: color
579592
},
580-
hovertemplate: `${name}<br>Δ = (${dx.toFixed(3)}, ${dy.toFixed(3)})<br>|Δ| = ${Math.sqrt(dx*dx + dy*dy).toFixed(3)}<extra></extra>`
593+
hovertemplate: `${name}<br>Δ = (${dx.toFixed(3)}, ${dy.toFixed(3)})<br>|Δ| = ${toReal(sqrt(dx*dx + dy*dy)).toFixed(3)}<extra></extra>`
581594
};
582595
}
583596

@@ -634,10 +647,10 @@ class CauchyRiemannDemo implements DemoInstance {
634647
const diff_dx = { x: f_z0_dx.x - f_z0.x, y: f_z0_dx.y - f_z0.y };
635648
const diff_dy = { x: f_z0_dy.x - f_z0.x, y: f_z0_dy.y - f_z0.y };
636649

637-
const mag_dx = Math.sqrt(diff_dx.x * diff_dx.x + diff_dx.y * diff_dx.y);
638-
const mag_dy = Math.sqrt(diff_dy.x * diff_dy.x + diff_dy.y * diff_dy.y);
639-
const angle = Math.acos((diff_dx.x * diff_dy.x + diff_dx.y * diff_dy.y) /
640-
(mag_dx * mag_dy || 1)) * 180 / Math.PI;
650+
const mag_dx = toReal(sqrt(diff_dx.x * diff_dx.x + diff_dx.y * diff_dx.y));
651+
const mag_dy = toReal(sqrt(diff_dy.x * diff_dy.x + diff_dy.y * diff_dy.y));
652+
const angle = toReal(acos((diff_dx.x * diff_dy.x + diff_dx.y * diff_dy.y) /
653+
(mag_dx * mag_dy || 1))) * 180 / number(PI);
641654

642655
// Update validation display
643656
const crIcon = crSatisfied ? '✓' : '✗';

demos/complex-analysis/contour-drawing.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,19 @@ import {
88
createCheckbox,
99
InfoDisplay
1010
} from '@framework/ui-components';
11-
import { complex, Complex, multiply, add, divide } from 'mathjs';
11+
import { complex, Complex, multiply, add, divide, exp, pi as PI, hypot, number, min, max, round, ceil } from 'mathjs';
1212
import { encodeCoeffs, decodeCoeffs, COEFF_COUNT, SAMPLE_COUNT_FOR_ENCODING } from './fourier-encoding';
1313

14+
// Type for numeric mathjs results we know won't be BigNumber/Fraction/Matrix
15+
type NumericResult = number | bigint | Complex;
16+
17+
// Helper to extract real part from a mathjs result (number, bigint, or Complex)
18+
function toReal(val: NumericResult): number {
19+
if (typeof val === 'number') return val;
20+
if (typeof val === 'bigint') return Number(val);
21+
return val.re;
22+
}
23+
1424
interface Point2D {
1525
x: number;
1626
y: number;
@@ -370,7 +380,7 @@ class ContourDrawingDemo implements DemoInstance {
370380
for (const sp of samplePoints) {
371381
const pt = this.plotToCanvas(sp);
372382
ctx.beginPath();
373-
ctx.arc(pt.x, pt.y, 4, 0, Math.PI * 2);
383+
ctx.arc(pt.x, pt.y, 4, 0, number(multiply(PI, 2)));
374384
ctx.fill();
375385
ctx.stroke();
376386
}
@@ -383,7 +393,7 @@ class ContourDrawingDemo implements DemoInstance {
383393
ctx.strokeStyle = this.isDark ? '#fff' : '#000';
384394
ctx.lineWidth = 1;
385395
ctx.beginPath();
386-
ctx.arc(startPt.x, startPt.y, 7, 0, Math.PI * 2);
396+
ctx.arc(startPt.x, startPt.y, 7, 0, number(multiply(PI, 2)));
387397
ctx.fill();
388398
ctx.stroke();
389399
}
@@ -431,7 +441,7 @@ class ContourDrawingDemo implements DemoInstance {
431441
// Tip marker
432442
ctx.fillStyle = color;
433443
ctx.beginPath();
434-
ctx.arc(end.x, end.y, 3, 0, Math.PI * 2);
444+
ctx.arc(end.x, end.y, 3, 0, number(multiply(PI, 2)));
435445
ctx.fill();
436446

437447
currentX = nextX;
@@ -488,13 +498,13 @@ class ContourDrawingDemo implements DemoInstance {
488498
for (let i = 1; i < totalPoints; i++) {
489499
const dx = this.points[i].x - this.points[i - 1].x;
490500
const dy = this.points[i].y - this.points[i - 1].y;
491-
cumLength.push(cumLength[i - 1] + Math.hypot(dx, dy));
501+
cumLength.push(cumLength[i - 1] + number(hypot(dx, dy)));
492502
}
493503
const totalLength = cumLength[totalPoints - 1];
494504

495505
if (totalLength === 0) return [];
496506

497-
const sampleCount = Math.min(N, totalPoints);
507+
const sampleCount = number(min(N, totalPoints));
498508
const samples: Point2D[] = [];
499509

500510
// Sample at even arc length intervals
@@ -599,9 +609,9 @@ class ContourDrawingDemo implements DemoInstance {
599609
const lastPoint = this.points[this.points.length - 1];
600610
const dx = point.x - lastPoint.x;
601611
const dy = point.y - lastPoint.y;
602-
const dist = Math.hypot(dx, dy);
612+
const dist = number(hypot(dx, dy));
603613
const pixelSize = this.pixelsToPlotUnits(1);
604-
const steps = Math.max(1, Math.round(dist / pixelSize));
614+
const steps = number(max(1, round(dist / pixelSize)));
605615

606616
// Fill in all pixel-sized steps from last point to new point
607617
for (let i = 1; i <= steps; i++) {
@@ -626,7 +636,7 @@ class ContourDrawingDemo implements DemoInstance {
626636
// Check if close enough to start point to auto-close
627637
const lastPoint = this.points[this.points.length - 1];
628638
const startPoint = this.points[0];
629-
const dist = Math.hypot(lastPoint.x - startPoint.x, lastPoint.y - startPoint.y);
639+
const dist = number(hypot(lastPoint.x - startPoint.x, lastPoint.y - startPoint.y));
630640
const threshold = this.pixelsToPlotUnits(this.closeThresholdPixels);
631641

632642
if (dist <= threshold) {
@@ -767,7 +777,7 @@ class ContourDrawingDemo implements DemoInstance {
767777
// For N=11: index 0,1,2,3,4,5,6,7,8,9,10 → freq 0,-1,+1,-2,+2,-3,+3,-4,+4,-5,+5
768778
private indexToFrequency(index: number): number {
769779
if (index === 0) return 0;
770-
const magnitude = Math.ceil(index / 2);
780+
const magnitude = number(ceil(index / 2));
771781
const sign = index % 2 === 1 ? -1 : 1;
772782
return sign * magnitude;
773783
}
@@ -782,8 +792,8 @@ class ContourDrawingDemo implements DemoInstance {
782792
const freq = this.indexToFrequency(n);
783793
let sum: Complex = complex(0, 0);
784794
for (let k = 0; k < N; k++) {
785-
const angle = -2 * Math.PI * freq * k / N;
786-
const expTerm = complex(Math.cos(angle), Math.sin(angle));
795+
const angle = toReal(multiply(-2, PI, freq, k, 1/N) as NumericResult);
796+
const expTerm = exp(complex(0, angle)) as Complex;
787797
sum = add(sum, multiply(z[k], expTerm)) as Complex;
788798
}
789799
coefficients.push(divide(sum, N) as Complex);
@@ -817,18 +827,18 @@ class ContourDrawingDemo implements DemoInstance {
817827
this.trailY = [];
818828

819829
for (let j = 0; j < M; j++) {
820-
const t_j = (2 * Math.PI * j) / M;
830+
const t_j = multiply(2, PI, j, 1/M);
821831
const frameVectors: Complex[] = [];
822832
let tipX = 0;
823833
let tipY = 0;
824834

825835
// Use only kCoefficients (low-pass filter effect)
826-
const coeffsToUse = Math.min(this.kCoefficients, N);
836+
const coeffsToUse = number(min(this.kCoefficients, N));
827837
for (let n = 0; n < coeffsToUse; n++) {
828838
// v_f(t_j) = c_f * e^(i*f*t_j) where f is the actual frequency
829839
const freq = this.indexToFrequency(n);
830-
const angle = freq * t_j;
831-
const expTerm = complex(Math.cos(angle), Math.sin(angle));
840+
const angle = toReal(multiply(freq, t_j) as NumericResult);
841+
const expTerm = exp(complex(0, angle)) as Complex;
832842
const v = multiply(this.fourierCoefficients[n], expTerm) as Complex;
833843
frameVectors.push(v);
834844
tipX += v.re;

0 commit comments

Comments
 (0)