#!/usr/bin/env catnip
# Riemann et Lebesgue à la frontière : Cantor ordinaire contre Cantor gras.
#
# Deux suites décroissantes d'ensembles fermés partent de [0, 1]. À chaque étape,
# leur indicatrice est Riemann-intégrable : elle n'a qu'un nombre fini de points
# de discontinuité, et son intégrale est la longueur totale des intervalles.
#
# Les limites ont pourtant des destins différents. Le Cantor ternaire a une
# mesure nulle ; les sommes de Darboux inférieure et supérieure de son indicatrice
# valent toutes deux zéro. Le Smith–Volterra–Cantor a une mesure 1/2 et un
# intérieur vide ; son indicatrice a une somme inférieure nulle et une somme
# supérieure 1/2. Elle est Lebesgue-intégrable, mais pas Riemann-intégrable.
#
# DEPS: matplotlib
mpl = import('matplotlib')
mpl.use('Agg')
plt = import('matplotlib.pyplot')
sys = import('sys')
import('pathlib', 'Path')
script_dir = Path(META.file).parent
output_dir = script_dir / 'output'
output_dir.mkdir(exist_ok=True)
STAGES = 7
struct Interval {
left: float; right: float;
length(self): float => { self.right - self.left }
}
struct StageReport {
name: str; stage: int; intervals: list[Interval];
cover_measure: float; expected: float; error: float;
display(self): None => {
print(
f" étape={self.stage} intervalles={len(self.intervals):<3} " +
f"couverture={self.cover_measure:.9f} formule={self.expected:.9f}"
)
}
}
union Verdict {
riemann_and_lebesgue
lebesgue_only
label(self): str => {
match self {
Verdict.riemann_and_lebesgue => { "Riemann et Lebesgue" }
Verdict.lebesgue_only => { "Lebesgue seulement" }
}
}
}
struct LimitReport {
name: str; lower_darboux: float; upper_darboux: float;
lebesgue_integral: float; verdict: Verdict;
display(self): None => {
print(f" {self.name}")
print(
f" Darboux : inférieure={self.lower_darboux}, " +
f"supérieure={self.upper_darboux}"
)
print(
f" intégrale de Lebesgue={self.lebesgue_integral}, " +
f"verdict={self.verdict.label()}"
)
}
}
interval_measure = (intervals: list[Interval]): float => {
total = 0.0
for interval in intervals {
total = total + interval.length()
}
total
}
# Cantor ternaire : chaque intervalle conserve ses tiers gauche et droit.
# Après n étapes, 2^n intervalles de longueur 3^-n couvrent la limite.
next_ternary_stage = (intervals: list[Interval]): list[Interval] => {
result = list()
for interval in intervals {
third = interval.length() / 3.0
result.append(Interval(interval.left, interval.left + third))
result.append(Interval(interval.right - third, interval.right))
}
result
}
# Smith–Volterra–Cantor : à l'étape n, on retire au centre de chacun des
# 2^(n-1) intervalles un trou de longueur 4^-n. La longueur retirée tend vers 1/2.
next_fat_stage = (intervals: list[Interval], stage: int): list[Interval] => {
hole = 1.0 / (4.0 ** stage)
result = list()
for interval in intervals {
center = (interval.left + interval.right) / 2.0
result.append(Interval(interval.left, center - hole / 2.0))
result.append(Interval(center + hole / 2.0, interval.right))
}
result
}
make_stage_report = (name: str, stage: int, intervals: list[Interval], expected: float): StageReport => {
measured = interval_measure(intervals)
StageReport(name, stage, intervals, measured, expected, abs(measured - expected))
}
ternary_intervals = list(Interval(0.0, 1.0))
fat_intervals = list(Interval(0.0, 1.0))
ternary = list(make_stage_report('Cantor ternaire', 0, ternary_intervals, 1.0))
fat = list(make_stage_report('Smith–Volterra–Cantor', 0, fat_intervals, 1.0))
stage = 1
while stage <= STAGES {
ternary_intervals = next_ternary_stage(ternary_intervals)
fat_intervals = next_fat_stage(fat_intervals, stage)
ternary_expected = (2.0 / 3.0) ** stage
fat_expected = 0.5 + 1.0 / (2.0 ** (stage + 1))
ternary.append(make_stage_report('Cantor ternaire', stage, ternary_intervals, ternary_expected))
fat.append(make_stage_report('Smith–Volterra–Cantor', stage, fat_intervals, fat_expected))
stage = stage + 1
}
# Les deux limites sont fermées et d'intérieur vide. Toute somme inférieure de
# leur indicatrice vaut donc zéro. Les couvertures finies calculées ci-dessus
# convergent vers leurs sommes supérieures : zéro pour le Cantor ternaire, 1/2
# pour le Smith–Volterra–Cantor.
limits = list(
LimitReport('Cantor ternaire', 0.0, 0.0, 0.0, Verdict.riemann_and_lebesgue),
LimitReport('Smith–Volterra–Cantor', 0.0, 0.5, 0.5, Verdict.lebesgue_only),
)
# Oracles : cardinal 2^n, formules exactes des couvertures et décroissance stricte.
oracle_ok = True
families = list(ternary, fat)
for reports in families {
i = 0
while i < len(reports) {
report = reports[i]
if len(report.intervals) != 2 ** report.stage or report.error > 1.0e-12 {
oracle_ok = False
}
if i > 0 and report.cover_measure >= reports[i - 1].cover_measure {
oracle_ok = False
}
i = i + 1
}
}
if ternary[len(ternary) - 1].cover_measure > 6.0e-2 {
oracle_ok = False
}
if abs(fat[len(fat) - 1].cover_measure - 0.5) > 4.0e-3 {
oracle_ok = False
}
draw_stages = (axis, reports: list[StageReport], color: str, title: str): None => {
for report in reports {
for interval in report.intervals {
axis.plot(
list(interval.left, interval.right),
list(report.stage, report.stage),
color=color,
linewidth=5.0,
solid_capstyle='butt',
)
}
}
axis.invert_yaxis()
axis.set_xlim(0.0, 1.0)
axis.set_yticks(range(STAGES + 1))
axis.set_title(title)
axis.set_xlabel("x")
axis.set_ylabel("étape")
}
# Planche : mêmes propriétés topologiques visibles, mesures différentes, puis
# conséquence directe sur l'écart entre sommes de Darboux à la limite.
fig, axes = plt.subplots(2, 2, figsize=tuple(13, 9))
axes = axes.ravel()
draw_stages(axes[0], ternary, '#4c78a8', "Cantor ternaire : mesure → 0")
draw_stages(axes[1], fat, '#54a24b', "Smith–Volterra–Cantor : mesure → 1/2")
stage_numbers = ternary.[(r) => { r.stage }]
ternary_measures = ternary.[(r) => { r.cover_measure }]
fat_measures = fat.[(r) => { r.cover_measure }]
axes[2].plot(stage_numbers, ternary_measures, marker='o', label='Cantor ternaire')
axes[2].plot(stage_numbers, fat_measures, marker='s', label='Smith–Volterra–Cantor')
axes[2].axhline(0.5, color='#777777', linewidth=1.0, linestyle='--', label='limite 1/2')
axes[2].axhline(0.0, color='#111111', linewidth=1.0)
axes[2].set_ylim(-0.03, 1.03)
axes[2].set_title("Intégrales des indicatrices finies")
axes[2].set_xlabel("étape n")
axes[2].set_ylabel("longueur de la couverture")
axes[2].grid(True, alpha=0.25)
axes[2].legend()
xpos = list(0, 1)
upper = limits.[(r) => { r.upper_darboux }]
lower = limits.[(r) => { r.lower_darboux }]
axes[3].bar(xpos, upper, width=0.55, color=list('#4c78a8', '#54a24b'), alpha=0.7, label='somme supérieure')
axes[3].scatter(xpos, lower, color='#111111', marker='_', s=500, linewidths=3.0, label='somme inférieure')
axes[3].set_xticks(xpos)
axes[3].set_xticklabels(list('Cantor\nordinaire', 'Cantor\ngras'))
axes[3].set_ylim(-0.04, 0.62)
axes[3].set_title("Écart de Darboux à la limite")
axes[3].set_ylabel("valeur")
axes[3].text(0, 0.05, "Riemann : oui", ha='center')
axes[3].text(1, 0.54, "Riemann : non", ha='center')
axes[3].legend(loc='upper left')
fig.suptitle("Même topologie, mesure différente, intégrabilité différente", fontsize=15)
fig.tight_layout()
output_path = output_dir / 'riemann_lebesgue.png'
fig.savefig(str(output_path), dpi=130, bbox_inches='tight')
print(f"⇒ Figure → {output_path}")
if not oracle_ok {
raise RuntimeError("un invariant des constructions de Cantor a échoué")
}
# Aperçu one-shot par défaut : le PNG reste généré au runtime, puis embarqué
# dans une page autonome. `--no-browser` conserve un chemin headless pour les
# validations et les environnements sans interface graphique.
if '--no-browser' not in sys.argv {
base64 = import('base64')
http = import('http')
image_data = base64.b64encode(output_path.read_bytes()).decode('ascii')
page_start = '<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Riemann et Lebesgue — Catnip</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: #0d1117; color: #c9d1d9; font-family: system-ui, sans-serif; }
main { width: min(1440px, 100%); margin: 0 auto; padding: 32px 20px 48px; }
h1 { margin: 0 0 8px; color: #f0f6fc; font-size: clamp(1.5rem, 3vw, 2.4rem); }
p { margin: 0 0 24px; color: #8b949e; }
figure { margin: 0; padding: 16px; background: #ffffff; border: 1px solid #30363d; border-radius: 12px; }
img { display: block; width: 100%; height: auto; }
.verdicts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 16px; }
.verdict { padding: 14px 16px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; }
.verdict strong { display: block; color: #f0f6fc; margin-bottom: 4px; }
@media (max-width: 680px) { .verdicts { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<main>
<h1>Riemann et Lebesgue à la frontière</h1>
<p>Même topologie, mesure différente, intégrabilité différente.</p>
<figure><img alt="Comparaison des ensembles de Cantor ordinaire et gras" src="data:image/png;base64,'
page_end = '"></figure>
<section class="verdicts">
<div class="verdict">
<strong>Cantor ternaire</strong>
Darboux inférieur = supérieur = 0. Riemann et Lebesgue.
</div>
<div class="verdict">
<strong>Smith–Volterra–Cantor</strong>
Darboux inférieur = 0, supérieur = 1/2. Lebesgue seulement.
</div>
</section>
</main>
</body>
</html>'
http.serve(page_start + image_data + page_end, 0, 'text/html; charset=utf-8', True)
}