Add star-branch instance: branch-point data family alongside the log-charge family
11 changed files+207−46
docs/problems/laplace-dirichlet-2d.mdmodified+50−16View file
@@ -21,28 +21,52 @@ set of evaluation points.
2121
2222 ## The exact solution
2323
24-The data g is manufactured from an exact harmonic function, a sum of
25-three logarithmic point sources placed outside the domain:
24+The data g is manufactured from an exact harmonic function u*, and
25+g = u* restricted to ∂Ω. Since u* is harmonic in Ω, it is the unique
26+solution, and errors are measured against it directly rather than
27+against a reference computation. Two data families are used.
28+
29+**log-sources.** A sum of three logarithmic point sources placed outside
30+the domain:
2631
2732 u*(x) = Σ_j c_j log |x − s_j|, c = (1.0, −0.6, 0.8).
2833
2934 Source s_j is the boundary point at parameter φ_j = 2π(j−1)/3 + 0.4
30-pushed a distance d along the outward unit normal, and g = u* restricted
31-to ∂Ω. Since u* is harmonic in Ω, it is the unique solution, and errors
32-are measured against it directly rather than against a reference
33-computation. The distance d controls difficulty: u* continues
34-harmonically only up to the sources, so the smaller d, the shorter the
35-distance the data continues past the boundary, and methods whose
35+pushed a distance d along the outward unit normal.
36+
37+**branch-point.** The real part of a complex square root:
38+
39+ u*(z) = Re √(w), w = −(z − z₀) e^{−iθ₀},
40+
41+where z₀ is the boundary point at parameter 0.4 pushed a distance d
42+along the outward unit normal and θ₀ is its polar angle. The principal
43+branch cut w ≤ 0 maps to the radial ray from z₀ away from the origin,
44+which cannot meet a domain star-shaped about the origin, so u* is
45+harmonic on the closed domain; its nearest singularity is the branch
46+point z₀ itself. In real arithmetic, Re √w = √((|w| + Re w)/2).
47+
48+In both families the distance d controls difficulty: u* continues
49+harmonically only up to its singularities, so the smaller d, the shorter
50+the distance the data continues past the boundary, and methods whose
3651 representations assume a generous continuation lose it. A solver must
37-not use knowledge of the sources; they exist only to manufacture g.
52+not use knowledge of the sources or branch point; they exist only to
53+manufacture g.
3854
3955 ## Official instances
4056
41-| id | a | k | d | character |
42-|---|---|---|---|---|
43-| disk-easy | 0 | 0 | 0.5 | the unit disk, distant sources |
44-| star-medium | 0.2 | 3 | 0.4 | mild geometry, comfortable continuation |
45-| star-hard | 0.3 | 5 | 0.08 | wavy boundary, data barely continues |
57+| id | a | k | d | family | character |
58+|---|---|---|---|---|---|
59+| disk-easy | 0 | 0 | 0.5 | log-sources | the unit disk, distant sources |
60+| star-medium | 0.2 | 3 | 0.4 | log-sources | mild geometry, comfortable continuation |
61+| star-hard | 0.3 | 5 | 0.08 | log-sources | wavy boundary, data barely continues |
62+| star-branch | 0.2 | 3 | 0.4 | branch-point | star-medium's geometry with the other data family |
63+
64+star-branch repeats star-medium's geometry and singularity distance with
65+the branch-point family. The pair separates the data family from its
66+singularity structure: a method whose accuracy depended on a coincidence
67+between its representation and the data-generating family would score
68+differently on the two, while a method governed by the continuation
69+distance scores alike.
4670
4771 Committed results exist only at these instances, so every solver is
4872 compared on identical inputs. The parameters may be varied freely in the
@@ -116,5 +140,15 @@ masks them; grid values are never scored.
116140 The exact solution is smooth and free of boundary singularities, so this
117141 problem does not test corner handling, nonsmooth data, or interior
118142 sources (a nonzero right-hand side would exclude plain boundary-integral
119-methods; that belongs to a different problem). The domain family is
120-star-shaped by construction, which some methods can exploit.
143+methods; that belongs to a different problem). In particular, every
144+instance's solution is real-analytic in a neighborhood of the closed
145+domain, which favors methods that exploit analyticity (fundamental
146+solutions, boundary integral equations, spectral collocation) over
147+low-order discretizations; this is a property of manufactured smooth
148+benchmarks in general, not of the particular families chosen. The
149+star-branch instance exists because most instances manufacture the data
150+from exterior log charges, the same function class the MFS
151+representation draws on; comparing it with star-medium checks that
152+scores follow the continuation distance rather than the family. The
153+domain family is star-shaped by construction, which some methods can
154+exploit.
src/app/components/DomainView.tsxmodified+26−5View file
@@ -5,8 +5,9 @@ import { useEffect, useRef } from "react";
55 import type { Laplace2dInstance } from "../../problems/laplace2d/spec";
66 import {
77 boundaryPoint,
8+ branchPoint,
89 evalPoints,
9- sources,
10+ singularities,
1011 } from "../../problems/laplace2d/exact";
1112
1213 function token(name: string): string {
@@ -59,10 +60,10 @@ export function DomainView({ inst }: { inst: Laplace2dInstance }) {
5960 ctx.fill();
6061 }
6162
62- // sources
63+ // the exact solution's singularities
6364 ctx.strokeStyle = token("--series-2");
6465 ctx.lineWidth = 2;
65- for (const src of sources(inst)) {
66+ for (const src of singularities(inst)) {
6667 const cx = X(src.x);
6768 const cy = Y(src.y);
6869 ctx.beginPath();
@@ -72,6 +73,22 @@ export function DomainView({ inst }: { inst: Laplace2dInstance }) {
7273 ctx.lineTo(cx + 5, cy - 5);
7374 ctx.stroke();
7475 }
76+
77+ // branch family: dashed segment along the cut (radially outward)
78+ if (inst.family === "branch-point") {
79+ const b = branchPoint(inst);
80+ ctx.strokeStyle = token("--text-3");
81+ ctx.lineWidth = 1.5;
82+ ctx.setLineDash([4, 3]);
83+ ctx.beginPath();
84+ ctx.moveTo(X(b.x), Y(b.y));
85+ ctx.lineTo(
86+ X(b.x + 0.3 * Math.cos(b.theta0)),
87+ Y(b.y + 0.3 * Math.sin(b.theta0))
88+ );
89+ ctx.stroke();
90+ ctx.setLineDash([]);
91+ }
7592 };
7693 draw();
7794 const mq = window.matchMedia("(prefers-color-scheme: dark)");
@@ -84,8 +101,12 @@ export function DomainView({ inst }: { inst: Laplace2dInstance }) {
84101 <canvas ref={canvasRef} />
85102 <figcaption className="field-caption" style={{ maxWidth: 360 }}>
86103 The domain, the evaluation points where solutions are scored
87- (dots), and the exact solution's sources a distance {inst.d} outside
88- the boundary (crosses).
104+ (dots), and the exact solution's{" "}
105+ {inst.family === "branch-point"
106+ ? "branch point a distance " + inst.d + " outside the boundary " +
107+ "(cross), with its cut running radially outward (dashed)"
108+ : "sources a distance " + inst.d + " outside the boundary (crosses)"}
109+ .
89110 </figcaption>
90111 </figure>
91112 );
src/app/matlabSources.tsmodified+1−0View file
@@ -17,6 +17,7 @@ export function matlabBase() {
1717 return {
1818 buildProblem: get("../problems/laplace2d/matlab/build_problem.m"),
1919 bdata: get("../problems/laplace2d/matlab/laplace2d_bdata.m"),
20+ bdataBranch: get("../problems/laplace2d/matlab/laplace2d_bdata_branch.m"),
2021 };
2122 }
2223
src/app/pages/ProblemPage.tsxmodified+2−0View file
@@ -236,6 +236,8 @@ export function ProblemPage({ problemId }: { problemId: string }) {
236236 <td>{inst.k}</td>
237237 <th className="left">d</th>
238238 <td>{inst.d}</td>
239+ <th className="left">data</th>
240+ <td className="left">{inst.family}</td>
239241 </tr>
240242 </tbody>
241243 </table>
src/cli/main.tsmodified+1−0View file
@@ -110,6 +110,7 @@ async function runCommand(flags: Record<string, string>) {
110110 const base = {
111111 buildProblem: readSrc("problems/laplace2d/matlab/build_problem.m"),
112112 bdata: readSrc("problems/laplace2d/matlab/laplace2d_bdata.m"),
113+ bdataBranch: readSrc("problems/laplace2d/matlab/laplace2d_bdata_branch.m"),
113114 };
114115
115116 let solverList: { manifest: SolverManifest; sources: MatlabSources; source: string }[];
src/harness/runner.tsmodified+5−1View file
@@ -14,6 +14,8 @@ export interface MatlabSources {
1414 buildProblem: string;
1515 /** laplace2d_bdata.m source */
1616 bdata: string;
17+ /** laplace2d_bdata_branch.m source */
18+ bdataBranch: string;
1719 /** solver.m source of the solver under test */
1820 solver: string;
1921 }
@@ -48,9 +50,10 @@ export function runPoint(req: RunPointRequest): RunPoint {
4850 const { instance, n } = req;
4951 const repeats = req.repeats ?? 3;
5052 const wantGrid = req.wantGrid ?? false;
53+ const family = instance.family === "branch-point" ? 1 : 0;
5154 const main = [
5255 "% generated by the fastandaccurate harness",
53- `prob = build_problem(${numLiteral(instance.a)}, ${numLiteral(instance.k)}, ${numLiteral(instance.d)}, ${wantGrid ? 1 : 0});`,
56+ `prob = build_problem(${numLiteral(instance.a)}, ${numLiteral(instance.k)}, ${numLiteral(instance.d)}, ${family}, ${wantGrid ? 1 : 0});`,
5457 `tic; out = solver(prob, ${n}); res_cold = toc;`,
5558 `res_times = zeros(${repeats}, 1);`,
5659 `for irep = 1:${repeats}`,
@@ -65,6 +68,7 @@ export function runPoint(req: RunPointRequest): RunPoint {
6568 {
6669 "build_problem.m": req.sources.buildProblem,
6770 "laplace2d_bdata.m": req.sources.bdata,
71+ "laplace2d_bdata_branch.m": req.sources.bdataBranch,
6872 "solver.m": req.sources.solver,
6973 },
7074 ["res_cold", "res_times", "res_ueval", "res_ugrid"]
src/problems/laplace2d/exact.tsmodified+42−12View file
@@ -22,26 +22,56 @@ export function boundaryPoint(inst: Laplace2dInstance, t: number) {
2222 return { x: r * Math.cos(t), y: r * Math.sin(t) };
2323 }
2424
25-/** The three exact-solution sources: boundary points at phi_j pushed a
25+/** Boundary point at parameter phi pushed a distance d along the outward
26+ * unit normal. */
27+function pushedPoint(inst: Laplace2dInstance, phi: number, d: number) {
28+ const { a, k } = inst;
29+ const r = 1 + a * Math.cos(k * phi);
30+ const dr = -a * k * Math.sin(k * phi);
31+ const bx = r * Math.cos(phi);
32+ const by = r * Math.sin(phi);
33+ const dx = dr * Math.cos(phi) - r * Math.sin(phi);
34+ const dy = dr * Math.sin(phi) + r * Math.cos(phi);
35+ const sp = Math.hypot(dx, dy);
36+ return { x: bx + (d * dy) / sp, y: by - (d * dx) / sp };
37+}
38+
39+/** The three log-family sources: boundary points at phi_j pushed a
2640 * distance d along the outward normal, strengths [1.0, -0.6, 0.8]. */
2741 export function sources(inst: Laplace2dInstance): Source[] {
28- const { a, k, d } = inst;
2942 const strengths = [1.0, -0.6, 0.8];
3043 return strengths.map((c, j) => {
31- const phi = (2 * Math.PI * j) / 3 + 0.4;
32- const r = 1 + a * Math.cos(k * phi);
33- const dr = -a * k * Math.sin(k * phi);
34- const bx = r * Math.cos(phi);
35- const by = r * Math.sin(phi);
36- const dx = dr * Math.cos(phi) - r * Math.sin(phi);
37- const dy = dr * Math.sin(phi) + r * Math.cos(phi);
38- const sp = Math.hypot(dx, dy);
39- return { x: bx + (d * dy) / sp, y: by - (d * dx) / sp, c };
44+ const p = pushedPoint(inst, (2 * Math.PI * j) / 3 + 0.4, inst.d);
45+ return { ...p, c };
4046 });
4147 }
4248
43-/** Exact solution u(x, y) = sum_j c_j log|x - s_j|. */
49+/** The branch-point family's singularity: the boundary point at
50+ * parameter 0.4 pushed d along the outward normal, with its polar
51+ * angle (the branch cut runs radially outward from it). */
52+export function branchPoint(inst: Laplace2dInstance) {
53+ const p = pushedPoint(inst, 0.4, inst.d);
54+ return { ...p, theta0: Math.atan2(p.y, p.x) };
55+}
56+
57+/** The exact solution's singular points, for display. */
58+export function singularities(inst: Laplace2dInstance): { x: number; y: number }[] {
59+ return inst.family === "branch-point" ? [branchPoint(inst)] : sources(inst);
60+}
61+
62+/** Exact solution. Log family: u = sum_j c_j log|x - s_j|. Branch
63+ * family: u = Re sqrt(w), w = -(z - z0) e^{-i theta0}, evaluated in
64+ * real arithmetic as sqrt((|w| + Re w)/2); the branch cut is the radial
65+ * ray from z0 away from the origin, outside the domain. */
4466 export function exactU(inst: Laplace2dInstance, x: number, y: number): number {
67+ if (inst.family === "branch-point") {
68+ const b = branchPoint(inst);
69+ const dx = x - b.x;
70+ const dy = y - b.y;
71+ const wre = -(dx * Math.cos(b.theta0) + dy * Math.sin(b.theta0));
72+ const wim = -(dy * Math.cos(b.theta0) - dx * Math.sin(b.theta0));
73+ return Math.sqrt((Math.hypot(wre, wim) + wre) / 2);
74+ }
4575 let u = 0;
4676 for (const s of sources(inst)) {
4777 u += s.c * 0.5 * Math.log((x - s.x) ** 2 + (y - s.y) ** 2);
src/problems/laplace2d/matlab/build_problem.mmodified+24−9View file
@@ -1,17 +1,23 @@
1-function prob = build_problem(a, k, d, wantGrid)
1+function prob = build_problem(a, k, d, family, wantGrid)
22 % BUILD_PROBLEM Assemble the problem struct for laplace-dirichlet-2d.
33 %
44 % The domain is the star-shaped region bounded by
55 % x(t) = r(t) [cos t; sin t], r(t) = 1 + a cos(k t), t in [0, 2 pi).
6-% The exact solution is u(x) = sum_j c_j log|x - s_j|, with three point
7-% sources s_j outside the domain: s_j is the boundary point at parameter
8-% phi_j = 2 pi (j-1)/3 + 0.4 pushed a distance d along the outward unit
9-% normal, with strengths c = [1.0; -0.6; 0.8].
6+% The exact solution depends on the data family:
7+% family 0 (log-sources): u(x) = sum_j c_j log|x - s_j|, with three
8+% point sources s_j outside the domain: s_j is the boundary point at
9+% parameter phi_j = 2 pi (j-1)/3 + 0.4 pushed a distance d along the
10+% outward unit normal, with strengths c = [1.0; -0.6; 0.8].
11+% family 1 (branch-point): u(z) = Re sqrt(-(z - z0) e^{-i th0}), with
12+% z0 the boundary point at parameter 0.4 pushed a distance d along
13+% the outward unit normal and th0 its polar angle (see
14+% laplace2d_bdata_branch.m).
1015 %
1116 % The solver receives only the curve (with derivatives), the Dirichlet
1217 % data g as a function of the boundary parameter, and the points where
13-% the solution is requested. The sources exist here only to manufacture
14-% the data; a submitted solver must not use knowledge of them.
18+% the solution is requested. The singularities exist here only to
19+% manufacture the data; a submitted solver must not use knowledge of
20+% them.
1521 %
1622 % Fields of prob:
1723 % curve @(t) -> [x y] boundary point, t column vector
@@ -22,7 +28,11 @@ function prob = build_problem(a, k, d, wantGrid)
2228 % vizXY m x 2 grid points where uGrid is requested
2329 % (m = 0 when no visualization is wanted)
2430
25-phi = 2*pi*[0; 1; 2]/3 + 0.4;
31+if family == 1
32+ phi = 0.4;
33+else
34+ phi = 2*pi*[0; 1; 2]/3 + 0.4;
35+end
2636 c = [1.0; -0.6; 0.8];
2737 rphi = 1 + a*cos(k*phi);
2838 bx = rphi.*cos(phi);
@@ -39,7 +49,12 @@ prob.curveD = @(t) [-a*k*sin(k*t).*cos(t) - (1 + a*cos(k*t)).*sin(t), ...
3949 -a*k*sin(k*t).*sin(t) + (1 + a*cos(k*t)).*cos(t)];
4050 prob.curveDD = @(t) [(-a*k*k*cos(k*t) - 1 - a*cos(k*t)).*cos(t) + 2*a*k*sin(k*t).*sin(t), ...
4151 (-a*k*k*cos(k*t) - 1 - a*cos(k*t)).*sin(t) - 2*a*k*sin(k*t).*cos(t)];
42-prob.g = @(t) laplace2d_bdata(t, a, k, sx, sy, c);
52+if family == 1
53+ th0 = atan2(sy(1), sx(1));
54+ prob.g = @(t) laplace2d_bdata_branch(t, a, k, sx(1), sy(1), th0);
55+else
56+ prob.g = @(t) laplace2d_bdata(t, a, k, sx, sy, c);
57+end
4358
4459 % Evaluation points: 32 rays, radial fractions 0.1..0.9, plus the origin
4560 % (289 points). The rule must match evalPoints() in
src/problems/laplace2d/matlab/laplace2d_bdata_branch.madded+20−0View file
@@ -0,0 +1,20 @@
1+function g = laplace2d_bdata_branch(t, a, k, x0, y0, th0)
2+% LAPLACE2D_BDATA_BRANCH Dirichlet data for the branch-point family.
3+% The exact solution is u*(z) = Re sqrt(w), w = -(z - z0) e^{-i th0},
4+% with z0 = x0 + i y0 the branch point and th0 its polar angle. The
5+% principal branch cut w <= 0 maps to the radial ray from z0 away from
6+% the origin, which never meets a domain star-shaped about the origin,
7+% so u* is harmonic on the closed domain. Evaluated in real arithmetic:
8+% Re sqrt(w) = sqrt((|w| + Re w)/2).
9+% Called through the prob.g handle built in build_problem; solvers see
10+% only that handle. Must match the branch-point case of exactU in
11+% src/problems/laplace2d/exact.ts.
12+r = 1 + a*cos(k*t);
13+x = r.*cos(t);
14+y = r.*sin(t);
15+dx = x - x0;
16+dy = y - y0;
17+wre = -(dx*cos(th0) + dy*sin(th0));
18+wim = -(dy*cos(th0) - dx*sin(th0));
19+g = sqrt((sqrt(wre.^2 + wim.^2) + wre)/2);
20+end
src/problems/laplace2d/spec.tsmodified+23−1View file
@@ -7,6 +7,9 @@
77 export const PROBLEM_ID = "laplace-dirichlet-2d";
88 export const PROBLEM_VERSION = 1;
99
10+/** The family the exact solution is manufactured from. */
11+export type DataFamily = "log-sources" | "branch-point";
12+
1013 export interface Laplace2dInstance {
1114 /** Short stable identifier used in results and URLs. */
1215 id: string;
@@ -14,8 +17,9 @@ export interface Laplace2dInstance {
1417 /** Boundary r(t) = 1 + a cos(k t). */
1518 a: number;
1619 k: number;
17- /** Distance of the exact solution's sources beyond the boundary. */
20+ /** Distance of the exact solution's singularities beyond the boundary. */
1821 d: number;
22+ family: DataFamily;
1923 description: string;
2024 }
2125
@@ -26,6 +30,7 @@ export const INSTANCES: Laplace2dInstance[] = [
2630 a: 0,
2731 k: 0,
2832 d: 0.5,
33+ family: "log-sources",
2934 description:
3035 "The unit disk with sources half a radius beyond the boundary. " +
3136 "Every reasonable method should reach high accuracy quickly.",
@@ -36,6 +41,7 @@ export const INSTANCES: Laplace2dInstance[] = [
3641 a: 0.2,
3742 k: 3,
3843 d: 0.4,
44+ family: "log-sources",
3945 description:
4046 "A gently star-shaped domain; the data continues comfortably past " +
4147 "the boundary, so geometric convergence is attainable but the " +
@@ -47,11 +53,26 @@ export const INSTANCES: Laplace2dInstance[] = [
4753 a: 0.3,
4854 k: 5,
4955 d: 0.08,
56+ family: "log-sources",
5057 description:
5158 "A wavier domain with sources only 0.08 beyond the boundary. The " +
5259 "data barely continues past the boundary, which defeats methods " +
5360 "whose representation assumes it does.",
5461 },
62+ {
63+ id: "star-branch",
64+ label: "3-lobe star, branch-point data",
65+ a: 0.2,
66+ k: 3,
67+ d: 0.4,
68+ family: "branch-point",
69+ description:
70+ "Identical geometry and singularity distance to star-medium, but " +
71+ "the data comes from a branch-point singularity (the real part of " +
72+ "a complex square root) rather than log point charges, so the pair " +
73+ "tests whether behavior depends on the data family or only on its " +
74+ "singularity distance.",
75+ },
5576 ];
5677
5778 export function getInstance(id: string): Laplace2dInstance {
@@ -69,6 +90,7 @@ export function canonicalSpec(inst: Laplace2dInstance) {
6990 return {
7091 a: inst.a,
7192 d: inst.d,
93+ family: inst.family,
7294 instance: inst.id,
7395 k: inst.k,
7496 problem: PROBLEM_ID,
test/solver-test.tsmodified+13−2View file
@@ -16,6 +16,7 @@ const read = (p: string) => readFileSync(join(root, p), "utf-8");
1616 const base = {
1717 buildProblem: read("src/problems/laplace2d/matlab/build_problem.m"),
1818 bdata: read("src/problems/laplace2d/matlab/laplace2d_bdata.m"),
19+ bdataBranch: read("src/problems/laplace2d/matlab/laplace2d_bdata_branch.m"),
1920 };
2021 const solverSources: Record<string, MatlabSources> = {
2122 mfs: { ...base, solver: read("src/solvers/mfs/solver.m") },
@@ -27,8 +28,18 @@ const solverSources: Record<string, MatlabSources> = {
2728 // beyond the data's singularities there, and if it suddenly reached high
2829 // accuracy the instance would no longer be testing what the spec says.
2930 const mustReach: Record<string, Record<string, number>> = {
30- mfs: { "disk-easy": 1e-12, "star-medium": 1e-12, "star-hard": 1e-2 },
31- "nystrom-dlp": { "disk-easy": 1e-10, "star-medium": 1e-10, "star-hard": 1e-8 },
31+ mfs: {
32+ "disk-easy": 1e-12,
33+ "star-medium": 1e-12,
34+ "star-hard": 1e-2,
35+ "star-branch": 1e-10,
36+ },
37+ "nystrom-dlp": {
38+ "disk-easy": 1e-10,
39+ "star-medium": 1e-10,
40+ "star-hard": 1e-8,
41+ "star-branch": 1e-10,
42+ },
3243 };
3344 const mustNotReach: Record<string, Record<string, number>> = {
3445 mfs: { "star-hard": 1e-8 },