demos¶
Exact Symbolic Poincaré Conformal Hyperbolic Disk Demonstrations (SymPy + geometor.model)
In strict accordance with GEOMETOR’s Content / Code Separation, executable Python constructions live outside the historical :layout: book pages of La Science et l’Hypothèse and operate in geometor.poincare.disk and demos/ with zero ``float`` or ``sp.Float`` coordinates.
Because Poincaré’s conformal dictionary represents the hyperbolic plane inside the Euclidean unit disk $\mathbb{D} = \{(x, y) : x^2 + y^2 < 1\}$ bounded by the ideal horizon $\Omega: x^2 + y^2 = 1$, every hyperbolic geodesic is either a Euclidean diameter through $O(0,0)$ or a Euclidean circle cutting $\Omega$ orthogonally ($|OC|^2 - r^2 = 1$). Straightedge-and-compass constructions in geometor.model therefore execute 100% exact hyperbolic geometry over $\mathbb{Q}$ and quadratic radical extensions.
1. Horizon Inversion, Orthogonal Geodesic Circles & Rational Quadrances¶
Constructs Euclidean inversion $P \mapsto P^* = P / \|P\|^2$ in $\Omega$, circumscribes the unique orthogonal geodesic circle through $(P, Q, P^*)$, and verifies the exact polynomial bridge between Poincaré’s 4-point cross-ratio $\chi(P, Q; U, V)$ and Norman Wildberger’s Rational Hyperbolic Quadrance $q_{\mathrm{UHG}}(P, Q) \in \mathbb{Q}$:
1r"""Demo 1: Horizon Inversion, Orthogonal Geodesic Circles, and Rational Quadrances.
2
3Constructs Henri Poincaré's conformal disk dictionary (*La Science et l'Hypothèse*,
4Part II, Ch. III, 1902) inside ``geometor.model`` with 100% exact SymPy algebra:
5
61. Horizon circle :math:`\Omega: x^2 + y^2 = 1` centered at :math:`O = (0, 0)`.
72. Pure straightedge-and-compass Euclidean inversion :math:`P \mapsto P^* = P / \|P\|^2`
8 satisfying :math:`Q(O, P) \cdot Q(O, P^*) = 1` and involution :math:`(P^*)^* = P`.
93. Unique orthogonal hyperbolic geodesic circle through :math:`(P, Q, P^*)` cutting
10 :math:`\Omega` orthogonally (:math:`|OC|^2 - r^2 = 1`) at ideal boundary points :math:`U, V`.
114. Exact equivalence between Poincaré's 4-point cross-ratio distance
12 :math:`d_{\mathbb{H}}(P, Q) = \ln \chi(P, Q; U, V)` and Norman Wildberger's
13 transcendental-free Rational Hyperbolic Quadrance :math:`q_{\text{UHG}}(P, Q) \in \mathbb{Q}`.
14"""
15
16from __future__ import annotations
17
18import argparse
19import sys
20from pathlib import Path
21from typing import Sequence
22
23_REPO_ROOT = Path(__file__).resolve().parents[1]
24for _candidate in (_REPO_ROOT / "src", _REPO_ROOT.parent / "model" / "src"):
25 if str(_candidate) not in sys.path:
26 sys.path.insert(0, str(_candidate))
27
28import sympy as sp # noqa: E402
29from geometor.model import Model, quadrance, zero_test # noqa: E402
30
31from geometor.poincare import ( # noqa: E402
32 construct_hyperbolic_geodesic,
33 construct_inversion_in_horizon,
34 construct_unit_horizon,
35 hyperbolic_cross_ratio_and_quadrance,
36 render_figure_svg,
37)
38
39
40def assert_zero_floats(model: Model) -> None:
41 """Verify that every element in ``model`` is 100% free of ``sp.Float``."""
42 for el in model:
43 if el.has(sp.Float):
44 raise AssertionError(f"Floating-point contamination detected in {el!r}")
45
46
47def run_demo(svg_out: Path | None = None) -> dict[str, sp.Expr]:
48 """Execute the exact horizon-inversion and geodesic construction."""
49 model = Model("demo_inversion_and_geodesics", logger=False, auto_intersect=False)
50 origin, _ex, _omega = construct_unit_horizon(model)
51
52 # Interior rational points P and Q
53 p_pt = model.set_point(sp.Rational(1, 3), sp.Rational(1, 4), classes=["given"], label="P")
54 q_pt = model.set_point(sp.Rational(-2, 5), sp.Rational(1, 5), classes=["given"], label="Q")
55
56 # 1. Exact compass-and-straightedge inversion in the unit horizon
57 p_star = construct_inversion_in_horizon(model, p_pt, label="P*")
58 p_roundtrip = construct_inversion_in_horizon(model, p_star, label="P**")
59 assert zero_test(quadrance(origin, p_pt) * quadrance(origin, p_star) - 1)
60 assert zero_test(p_roundtrip.x - p_pt.x) and zero_test(p_roundtrip.y - p_pt.y)
61
62 # 2. Orthogonal hyperbolic geodesic circle through P and Q
63 geo = construct_hyperbolic_geodesic(model, p_pt, q_pt, label="g_PQ")
64 carrier, u_ideal, v_ideal = geo
65 assert not geo.is_diameter
66 assert geo.center is not None and geo.radius_squared is not None
67 assert zero_test(geo.orthogonality_residual)
68 assert zero_test(quadrance(origin, u_ideal) - 1)
69 assert zero_test(quadrance(origin, v_ideal) - 1)
70
71 # 3. Diameter case when collinear with origin O
72 r_pt = model.set_point(sp.Rational(2, 3), sp.Rational(1, 2), classes=["given"], label="R")
73 diam_geo = construct_hyperbolic_geodesic(model, p_pt, r_pt, label="diam_PR")
74 assert diam_geo.is_diameter
75
76 # 4. Cross-ratio and Rational Hyperbolic Quadrance invariants
77 metrics = hyperbolic_cross_ratio_and_quadrance(p_pt, q_pt, u_ideal, v_ideal)
78 assert_zero_floats(model)
79
80 svg_markup = render_figure_svg("fig2_poincare_dictionary_inversion.svg")
81 if svg_out is not None:
82 svg_out.parent.mkdir(parents=True, exist_ok=True)
83 svg_out.write_text(svg_markup, encoding="utf-8")
84
85 _ = carrier
86 return {
87 "P_star_x": p_star.x,
88 "P_star_y": p_star.y,
89 "center_x": geo.center.x,
90 "center_y": geo.center.y,
91 "radius_squared": geo.radius_squared,
92 "conformal_quadrance": metrics.conformal_quadrance,
93 "wildberger_uhg_quadrance": metrics.wildberger_uhg_quadrance,
94 "cayley_klein_quadrance": metrics.cayley_klein_quadrance,
95 "cross_ratio": metrics.cross_ratio,
96 "poincare_distance": metrics.poincare_distance,
97 }
98
99
100def main(argv: Sequence[str] | None = None) -> int:
101 """CLI entry point for Demo 1."""
102 parser = argparse.ArgumentParser(description=__doc__)
103 parser.add_argument("--svg", type=Path, default=None, help="Optional output SVG path.")
104 args = parser.parse_args(argv)
105
106 summary = run_demo(svg_out=args.svg)
107 print("=== Demo 1: Horizon Inversion & Hyperbolic Geodesic Dictionary ===")
108 print(f"P = (1/3, 1/4) -> P* = ({summary['P_star_x']}, {summary['P_star_y']})")
109 print(
110 f"Geodesic : center C = ({summary['center_x']}, {summary['center_y']}), "
111 f"r^2 = {summary['radius_squared']} (|OC|^2 - r^2 = 1)"
112 )
113 print(f"tau(P, Q) = {summary['conformal_quadrance']}")
114 print(f"q_UHG = {summary['wildberger_uhg_quadrance']}")
115 print(f"q_CK = {summary['cayley_klein_quadrance']}")
116 print(f"chi = {summary['cross_ratio']}")
117 print(f"d_H(P, Q) = {summary['poincare_distance']}")
118 return 0
119
120
121if __name__ == "__main__":
122 raise SystemExit(main())
2. Limiting Horoparallels, Ultraparallels & Independence of Postulate V¶
Constructs two asymptotic horoparallels $\ell_1, \ell_2$ and two ultraparallels $m_1, m_2$ through $P(\frac{1}{6}, \frac{2}{5})$ relative to the rational geodesic $L_{\mathrm{hyp}}$ joining $(\frac{4}{5}, -\frac{3}{5})$ and $(-\frac{4}{5}, -\frac{3}{5})$, proving algebraically via intersect_2d that Euclid’s Fifth Postulate fails inside $\mathbb{D}$ while Postulates I–IV hold identically.
1r"""Demo 2: Limiting Horoparallels, Ultraparallels, and Independence of Postulate V.
2
3Constructs Henri Poincaré's relative consistency proof for Lobachevskian geometry
4(*La Science et l'Hypothèse*, Part II, Ch. III, 1902):
5
6Given a hyperbolic geodesic :math:`L_{\text{hyp}}` with rational ideal endpoints
7:math:`U_L = (\frac{4}{5}, -\frac{3}{5})` and :math:`V_L = (-\frac{4}{5}, -\frac{3}{5})`
8and an exterior point :math:`P = (\frac{1}{6}, \frac{2}{5}) \in \mathbb{D} \setminus L_{\text{hyp}}`:
9
10- Constructs the two limiting horoparallels :math:`\ell_1 = PU_L` and :math:`\ell_2 = PV_L`
11 sharing ideal boundary vertices with :math:`L_{\text{hyp}}` on :math:`\partial\Omega`
12 while remaining disjoint inside the open disk :math:`\mathbb{D}`.
13- Constructs two distinct ultraparallel geodesics :math:`m_1, m_2` through :math:`P`
14 whose closures in :math:`\overline{\mathbb{D}}` are 100% disjoint from :math:`L_{\text{hyp}}`.
15- Demonstrates that any contradiction in Lobachevsky's geometry would translate via
16 Poincaré's Dictionary into a contradiction in Euclidean circle geometry.
17"""
18
19from __future__ import annotations
20
21import argparse
22import sys
23from pathlib import Path
24from typing import Sequence
25
26_REPO_ROOT = Path(__file__).resolve().parents[1]
27for _candidate in (_REPO_ROOT / "src", _REPO_ROOT.parent / "model" / "src"):
28 if str(_candidate) not in sys.path:
29 sys.path.insert(0, str(_candidate))
30
31import sympy as sp # noqa: E402
32import sympy.geometry as spg # noqa: E402
33from geometor.model import exact_sign, intersect_2d, zero_test # noqa: E402
34
35from geometor.poincare import ( # noqa: E402
36 UltraparallelPencil,
37 construct_ultraparallels_through_point,
38 render_figure_svg,
39)
40
41
42def run_demo(svg_out: Path | None = None) -> UltraparallelPencil:
43 """Construct and verify the Lobachevskian parallel pencil through P."""
44 pencil = construct_ultraparallels_through_point()
45 for el in pencil.model:
46 if el.has(sp.Float):
47 raise AssertionError(f"Floating-point atom found in {el!r}")
48
49 base_carrier = pencil.base_geodesic.carrier
50 # Verify limiting horoparallels meet base_geodesic ONLY on the ideal horizon x^2 + y^2 == 1
51 for lim in pencil.limiting_parallels:
52 hits = intersect_2d(base_carrier, lim.carrier)
53 interior_hits = [
54 pt
55 for pt in hits
56 if isinstance(pt, spg.Point2D) and exact_sign(pt.x**2 + pt.y**2 - 1) < 0
57 ]
58 bdry_hits = [
59 pt
60 for pt in hits
61 if isinstance(pt, spg.Point2D) and zero_test(pt.x**2 + pt.y**2 - 1)
62 ]
63 assert len(interior_hits) == 0
64 assert len(bdry_hits) == 1
65
66 # Verify ultraparallels have ZERO intersections in the closed unit disk x^2 + y^2 <= 1
67 for ultra in pencil.ultraparallels:
68 hits = intersect_2d(base_carrier, ultra.carrier)
69 closed_disk_hits = [
70 pt
71 for pt in hits
72 if isinstance(pt, spg.Point2D) and exact_sign(pt.x**2 + pt.y**2 - 1) <= 0
73 ]
74 assert len(closed_disk_hits) == 0
75
76 svg_markup = render_figure_svg("fig3_lobachevsky_parallels.svg")
77 if svg_out is not None:
78 svg_out.parent.mkdir(parents=True, exist_ok=True)
79 svg_out.write_text(svg_markup, encoding="utf-8")
80
81 return pencil
82
83
84def main(argv: Sequence[str] | None = None) -> int:
85 """CLI entry point for Demo 2."""
86 parser = argparse.ArgumentParser(description=__doc__)
87 parser.add_argument("--svg", type=Path, default=None, help="Optional output SVG path.")
88 args = parser.parse_args(argv)
89
90 pencil = run_demo(svg_out=args.svg)
91 print("=== Demo 2: Lobachevskian Horoparallels & Ultraparallels (Postulate V) ===")
92 print(f"Base geodesic L_hyp ideal endpoints : {pencil.base_geodesic.ideal_points}")
93 print(f"Exterior point P : ({pencil.point.x}, {pencil.point.y})")
94 for idx, lim in enumerate(pencil.limiting_parallels, start=1):
95 print(f"Horoparallel l_{idx} center & r^2 : {lim.center}, r^2 = {lim.radius_squared}")
96 for idx, ultra in enumerate(pencil.ultraparallels, start=1):
97 print(
98 f"Ultraparallel m_{idx} center & r^2 : {ultra.center}, r^2 = {ultra.radius_squared} "
99 "(0 intersections in closed disk)"
100 )
101 return 0
102
103
104if __name__ == "__main__":
105 raise SystemExit(main())
3. Pythagorean Ideal Triangle & Saccheri Quadrilateral Angular Defect¶
Constructs the ideal triangle $\triangle U_1 U_2 U_3$ on $U_1(1,0), U_2(-\frac{3}{5},\frac{4}{5}), U_3(-\frac{3}{5},-\frac{4}{5})$ with rational side centers $(1,2), (1,-2), (-\frac{5}{3},0)$, rational radii $2, 2, \frac{4}{3}$, and interior spreads $(0, 0, 0)$, alongside the rational Saccheri quadrilateral $A_1 A_2 B_2 B_1$ with equal leg UHG quadrances $-\frac{225}{64}$ and strictly acute summit spreads $\frac{64}{145} < 1$.
1r"""Demo 3: Ideal Hyperbolic Triangle & Saccheri Quadrilateral Angular Defect.
2
3Proves two celebrated theorems of Bolyai–Lobachevsky–Saccheri geometry using 100%
4rational points, rational circle centers, and Rational Trigonometry spreads inside
5``geometor.model``:
6
71. **Pythagorean Ideal Triangle** :math:`\triangle U_1 U_2 U_3` on
8 :math:`U_1 = (1, 0),\; U_2 = (-\frac{3}{5}, \frac{4}{5}),\; U_3 = (-\frac{3}{5}, -\frac{4}{5})`:
9 - Orthogonal side circles centered at :math:`(1, 2),\; (1, -2),\; (-\frac{5}{3}, 0)`
10 with exact rational radii :math:`2,\; 2,\; \frac{4}{3}`.
11 - Interior tangent spreads :math:`(0, 0, 0)` and maximal Gauss–Bonnet defect :math:`\pi`.
122. **Hyperbolic Right Triangle** :math:`\triangle OAB` & **Saccheri Quadrilateral**
13 :math:`A_1 A_2 B_2 B_1`:
14 - Right triangle on :math:`O(0,0),\; A(\frac{1}{2},0),\; B(0,\frac{1}{2})` has base spread
15 :math:`1` and acute vertex spreads :math:`\frac{9}{34} < \frac{1}{2}`.
16 - Saccheri quadrilateral with base :math:`A_1(-\frac{1}{3}, 0),\; A_2(\frac{1}{3}, 0)` and
17 summit :math:`B_1(-\frac{17}{39}, \frac{20}{39}),\; B_2(\frac{17}{39}, \frac{20}{39})` has
18 equal leg UHG quadrances :math:`-\frac{225}{64}`, right base spreads :math:`(1, 1)`,
19 and strictly acute summit spreads :math:`\frac{64}{145} < 1` (refuting Saccheri's
20 Euclidean right-angle hypothesis).
21"""
22
23from __future__ import annotations
24
25import argparse
26import sys
27from pathlib import Path
28from typing import Sequence
29
30_REPO_ROOT = Path(__file__).resolve().parents[1]
31for _candidate in (_REPO_ROOT / "src", _REPO_ROOT.parent / "model" / "src"):
32 if str(_candidate) not in sys.path:
33 sys.path.insert(0, str(_candidate))
34
35import sympy as sp # noqa: E402
36from geometor.model import zero_test # noqa: E402
37
38from geometor.poincare import ( # noqa: E402
39 IdealTriangleResult,
40 SaccheriDefectResult,
41 construct_ideal_hyperbolic_triangle,
42 construct_saccheri_triangleDefect,
43 render_figure_svg,
44)
45
46
47def run_demo(svg_dir: Path | None = None) -> tuple[IdealTriangleResult, SaccheriDefectResult]:
48 """Construct and verify the ideal hyperbolic triangle and Saccheri quadrilateral."""
49 ideal = construct_ideal_hyperbolic_triangle()
50 sac = construct_saccheri_triangleDefect()
51
52 for m in (ideal.model, sac.model):
53 for el in m:
54 if el.has(sp.Float):
55 raise AssertionError(f"Floating-point atom detected in {el!r}")
56
57 assert ideal.radii == (sp.Integer(2), sp.Integer(2), sp.Rational(4, 3))
58 assert all(zero_test(s) for s in ideal.vertex_spreads)
59
60 assert sac.triangle_spreads == (sp.Integer(1), sp.Rational(9, 34), sp.Rational(9, 34))
61 assert sac.saccheri_base_spreads == (sp.Integer(1), sp.Integer(1))
62 assert sac.saccheri_summit_spreads == (sp.Rational(64, 145), sp.Rational(64, 145))
63 assert sac.saccheri_leg_uhg_quadrances == (
64 sp.Rational(-225, 64),
65 sp.Rational(-225, 64),
66 )
67
68 if svg_dir is not None:
69 svg_dir.mkdir(parents=True, exist_ok=True)
70 (svg_dir / "fig4_ideal_hyperbolic_triangle.svg").write_text(
71 render_figure_svg("fig4_ideal_hyperbolic_triangle.svg"), encoding="utf-8"
72 )
73 (svg_dir / "fig5_saccheri_angular_defect.svg").write_text(
74 render_figure_svg("fig5_saccheri_angular_defect.svg"), encoding="utf-8"
75 )
76
77 return ideal, sac
78
79
80def main(argv: Sequence[str] | None = None) -> int:
81 """CLI entry point for Demo 3."""
82 parser = argparse.ArgumentParser(description=__doc__)
83 parser.add_argument(
84 "--svg-dir",
85 type=Path,
86 default=None,
87 help="Optional directory to write fig4 and fig5 SVGs.",
88 )
89 args = parser.parse_args(argv)
90
91 ideal, sac = run_demo(svg_dir=args.svg_dir)
92 print("=== Demo 3: Ideal Hyperbolic Triangle & Saccheri Angular Defect ===")
93 print(f"Ideal vertices : {ideal.vertices}")
94 print(f"Ideal circle centers : {ideal.centers}")
95 print(f"Ideal circle radii : {ideal.radii}")
96 print(f"Ideal vertex spreads : {ideal.vertex_spreads} -> defect = {ideal.angular_defect}")
97 print("-" * 68)
98 print(
99 f"Right triangle OAB : spreads = {sac.triangle_spreads} (< (1, 1/2, 1/2)), "
100 f"defect = {sac.triangle_defect}"
101 )
102 print(f"Saccheri leg q_UHG : {sac.saccheri_leg_uhg_quadrances}")
103 print(f"Saccheri base spreads: {sac.saccheri_base_spreads}")
104 print(
105 f"Saccheri summit : spreads = {sac.saccheri_summit_spreads} (< 1), "
106 f"defect = {sac.saccheri_defect}"
107 )
108 return 0
109
110
111if __name__ == "__main__":
112 raise SystemExit(main())
4. Heated-Disk Optical Rays, Beltrami–Klein Bridge & Stellar Parallax¶
Realizes Poincaré’s physical thermal parable ($T \propto 1 - r^2$, $n \propto (1 - r^2)^{-1}$), proves by pure straightedge radial projection $K(P) = \frac{2P}{1 + \|P\|^2}$ that Poincaré conformal arcs map onto Beltrami–Klein straight chords with identical Cayley–Klein quadrance $q_{\mathrm{CK}}$, and models Poincaré’s astronomical parallax argument for geometric conventionalism.
1r"""Demo 4: Poincaré's Heated Disk, Beltrami–Klein Projection, & Stellar Parallax.
2
3Synthesizes the physical and projective models of *La Science et l'Hypothèse*
4(Part II, Chs. IV–V, 1902):
5
61. **Poincaré's Heated-Disk Parable (Ch. IV)**:
7 - Temperature :math:`T(r) \propto R^2 - r^2` and refractive index :math:`n(r) \propto (R^2 - r^2)^{-1}`
8 bend optical Fermat geodesics and thermally contracted rulers along orthogonal circles,
9 making the finite disk physically indistinguishable from an infinite Lobachevskian plane.
102. **Beltrami–Klein Projective Chord Bridge**:
11 - Radial straightedge projection from :math:`O` onto the straight chord :math:`UV`
12 realizes the exact Weierstrass map :math:`K(P) = \frac{2P}{1 + \|P\|^2}`, turning
13 Poincaré's conformal arcs into Beltrami–Klein projective segments while preserving
14 :math:`q_{\text{CK}}(P_1, P_2) = \tanh^2 d_{\mathbb{H}}(P_1, P_2)`.
153. **Stellar Parallax & Geometric Conventionalism (Ch. V § 3)**:
16 - Any astronomical triangle :math:`\triangle S_1 S_2 S_3` with angular sum differing
17 from :math:`\pi` admits two empirically equivalent descriptions: Lobachevskian space
18 with straight rays vs. Euclidean space with refracted rays.
19"""
20
21from __future__ import annotations
22
23import argparse
24import sys
25from pathlib import Path
26from typing import Sequence
27
28_REPO_ROOT = Path(__file__).resolve().parents[1]
29for _candidate in (_REPO_ROOT / "src", _REPO_ROOT.parent / "model" / "src"):
30 if str(_candidate) not in sys.path:
31 sys.path.insert(0, str(_candidate))
32
33import sympy as sp # noqa: E402
34from geometor.model import zero_test # noqa: E402
35
36from geometor.poincare import ( # noqa: E402
37 BeltramiKleinBridgeResult,
38 HeatedDiskResult,
39 StellarParallaxResult,
40 construct_beltrami_klein_bridge,
41 construct_heated_disk_optical_rays,
42 construct_stellar_parallax_conventionalism,
43 hyperbolic_cross_ratio_and_quadrance,
44 render_figure_svg,
45)
46
47
48def _klein_hilbert_quadrance(
49 k1: sp.geometry.Point2D,
50 k2: sp.geometry.Point2D,
51) -> sp.Expr:
52 r"""Compute the Beltrami–Klein metric invariant directly from Klein coordinates.
53
54 For interior Klein points :math:`K_1, K_2`, the Cayley–Klein quadrance satisfies
55 :math:`1 - q_{\text{CK}}(K_1, K_2) = \frac{(1 - \|K_1\|^2)(1 - \|K_2\|^2)}{(1 - K_1 \cdot K_2)^2}`.
56 """
57 n1 = k1.x**2 + k1.y**2
58 n2 = k2.x**2 + k2.y**2
59 dot = k1.x * k2.x + k1.y * k2.y
60 return sp.simplify(1 - ((1 - n1) * (1 - n2)) / (1 - dot) ** 2)
61
62
63def run_demo(
64 svg_dir: Path | None = None,
65) -> tuple[HeatedDiskResult, BeltramiKleinBridgeResult, StellarParallaxResult]:
66 """Construct and verify the Heated Disk, Beltrami–Klein bridge, and Parallax models."""
67 heated = construct_heated_disk_optical_rays()
68 bridge = construct_beltrami_klein_bridge()
69 parallax = construct_stellar_parallax_conventionalism()
70
71 for m in (heated.model, bridge.model, parallax.model):
72 for el in m:
73 if el.has(sp.Float):
74 raise AssertionError(f"Floating-point atom detected in {el!r}")
75
76 p1, p2 = bridge.poincare_points
77 k1, k2 = bridge.klein_points
78 poincare_metrics = hyperbolic_cross_ratio_and_quadrance(
79 p1, p2, bridge.poincare_geodesic.U, bridge.poincare_geodesic.V
80 )
81 q_ck_from_klein = _klein_hilbert_quadrance(k1, k2)
82 assert zero_test(poincare_metrics.cayley_klein_quadrance - q_ck_from_klein)
83
84 if svg_dir is not None:
85 svg_dir.mkdir(parents=True, exist_ok=True)
86 for fname in (
87 "fig6_heated_disk_refraction.svg",
88 "fig7_cayley_klein_beltrami_bridge.svg",
89 "fig8_stellar_parallax_conventionalism.svg",
90 ):
91 (svg_dir / fname).write_text(render_figure_svg(fname), encoding="utf-8")
92
93 return heated, bridge, parallax
94
95
96def main(argv: Sequence[str] | None = None) -> int:
97 """CLI entry point for Demo 4."""
98 parser = argparse.ArgumentParser(description=__doc__)
99 parser.add_argument(
100 "--svg-dir",
101 type=Path,
102 default=None,
103 help="Optional directory to write fig6, fig7, and fig8 SVGs.",
104 )
105 args = parser.parse_args(argv)
106
107 heated, bridge, parallax = run_demo(svg_dir=args.svg_dir)
108 p1, p2 = bridge.poincare_points
109 k1, k2 = bridge.klein_points
110 metrics = hyperbolic_cross_ratio_and_quadrance(
111 p1, p2, bridge.poincare_geodesic.U, bridge.poincare_geodesic.V
112 )
113
114 print("=== Demo 4: Heated Disk, Beltrami–Klein Bridge, & Stellar Parallax ===")
115 print(
116 f"Heated disk source S = ({heated.source.x}, {heated.source.y}) with "
117 f"{len(heated.isothermals)} isothermals and {len(heated.light_rays)} Fermat rays"
118 )
119 print(f"Poincaré points : P1 = ({p1.x}, {p1.y}), P2 = ({p2.x}, {p2.y})")
120 print(f"Klein chord images : K1 = ({k1.x}, {k1.y}), K2 = ({k2.x}, {k2.y})")
121 print(
122 f"Invariant match : q_CK(Poincaré) = {metrics.cayley_klein_quadrance} == "
123 f"q_CK(Beltrami–Klein) = {_klein_hilbert_quadrance(k1, k2)}"
124 )
125 print(
126 f"Parallax triangle : {len(parallax.stars)} stars, "
127 f"{len(parallax.lobachevsky_rays)} hyperbolic arcs vs. "
128 f"{len(parallax.euclidean_chords)} Euclidean refracted chords"
129 )
130 return 0
131
132
133if __name__ == "__main__":
134 raise SystemExit(main())