571 lines
18 KiB
Python
Executable File
571 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Reader-facing computations for growing-radius domination.
|
|
|
|
This script accompanies the essay version of the preprint
|
|
|
|
Near-Critical First-Moment Lower Bounds for
|
|
Growing-Radius Domination in Random Regular Graphs.
|
|
|
|
It deliberately keeps the mathematical structure visible. The core steps are:
|
|
|
|
1. compute the tree-ball volume B_h;
|
|
2. evaluate the conditioned local entropy s_d(a);
|
|
3. evaluate the exact compact microcanonical functional;
|
|
4. solve the unique stationary orbit by reverse transfer;
|
|
5. reconstruct density, activity, and free energy at the root;
|
|
6. target the near-critical coordinate C = alpha B_h;
|
|
7. gate every reported row by independent residual checks.
|
|
|
|
Only ``mpmath`` is required for the main calculations. ``matplotlib`` is
|
|
optional and is used only by ``--plot``.
|
|
|
|
The code is expository, not optimized for maximum throughput. Numerical
|
|
calculations illustrate the theorem and audit the identities; they are not
|
|
used in its proof.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import hashlib
|
|
import math
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Iterable, Sequence
|
|
|
|
import mpmath as mp
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Geometry and the expected scale
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def tree_ball_volume(d: int, h: int) -> mp.mpf:
|
|
"""Radius-h volume of the infinite d-regular tree."""
|
|
if d < 3:
|
|
raise ValueError("d must be at least 3")
|
|
if h < 0:
|
|
raise ValueError("h must be nonnegative")
|
|
b = d - 1
|
|
return mp.mpf(1) + d * (mp.power(b, h) - 1) / (d - 2)
|
|
|
|
|
|
def near_critical_coordinate(d: int, h: int, W: mp.mpf | None = None) -> mp.mpf:
|
|
"""Return C = log B_h - 2 log log B_h - W.
|
|
|
|
The demonstration default W=log log B_h stays a diverging distance below
|
|
the bounded critical window while remaining close enough to show the
|
|
predicted second-order term.
|
|
"""
|
|
B = tree_ball_volume(d, h)
|
|
L = mp.log(B)
|
|
if W is None:
|
|
W = mp.log(L)
|
|
return L - 2 * mp.log(L) - W
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Exact local entropy and compact functional
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _conditioned_marginal(d: int, lam: mp.mpf) -> mp.mpf:
|
|
"""Marginal of one coordinate in a nonempty tilted subset of [d]."""
|
|
log1p_lam = mp.log1p(lam)
|
|
numerator = lam * mp.exp((d - 1) * log1p_lam)
|
|
denominator = mp.expm1(d * log1p_lam)
|
|
return numerator / denominator
|
|
|
|
|
|
def lambda_from_marginal(d: int, a: mp.mpf) -> mp.mpf:
|
|
"""Solve a = lambda(1+lambda)^(d-1)/((1+lambda)^d-1).
|
|
|
|
A precision-scaled lower endpoint is important near the Moore corner,
|
|
where a-1/d can be exponentially small.
|
|
"""
|
|
a = mp.mpf(a)
|
|
lower = mp.mpf(1) / d
|
|
if a < lower or a > 1:
|
|
raise ValueError("a must lie in [1/d, 1]")
|
|
tol = mp.power(10, -(mp.mp.dps - 12))
|
|
if abs(a - lower) <= tol:
|
|
return mp.mpf(0)
|
|
if abs(a - 1) <= tol:
|
|
return mp.inf
|
|
|
|
# Bisection in log lambda avoids an enormous dynamic range.
|
|
lo = -mp.mpf(2) * mp.mp.dps * mp.log(10)
|
|
hi = mp.mpf(2) * mp.mp.dps * mp.log(10)
|
|
for _ in range(max(160, 3 * mp.mp.dps)):
|
|
mid = (lo + hi) / 2
|
|
lam = mp.exp(mid)
|
|
if _conditioned_marginal(d, lam) < a:
|
|
lo = mid
|
|
else:
|
|
hi = mid
|
|
return mp.exp((lo + hi) / 2)
|
|
|
|
|
|
def conditioned_subset_entropy(d: int, a: mp.mpf) -> mp.mpf:
|
|
r"""Maximum entropy s_d(a) on nonempty subsets with marginal a.
|
|
|
|
s_d(a) = inf_{lambda>0} [log((1+lambda)^d-1)-da log lambda].
|
|
"""
|
|
a = mp.mpf(a)
|
|
lower = mp.mpf(1) / d
|
|
tol = mp.power(10, -(mp.mp.dps - 12))
|
|
if abs(a - lower) <= tol:
|
|
return mp.log(d)
|
|
if abs(a - 1) <= tol:
|
|
return mp.mpf(0)
|
|
lam = lambda_from_marginal(d, a)
|
|
log_partition = mp.log(mp.expm1(d * mp.log1p(lam)))
|
|
return log_partition - d * a * mp.log(lam)
|
|
|
|
|
|
def _xlogx(x: mp.mpf) -> mp.mpf:
|
|
return mp.mpf(0) if x == 0 else x * mp.log(x)
|
|
|
|
|
|
def compact_microcanonical_value(
|
|
d: int,
|
|
h: int,
|
|
x: Sequence[mp.mpf],
|
|
ell: Sequence[mp.mpf],
|
|
) -> tuple[mp.mpf, mp.mpf]:
|
|
r"""Evaluate the exact compact functional.
|
|
|
|
Coordinates:
|
|
x_i = q_{i-1,i}, i=1,...,h,
|
|
ell_i = q_{i,i}, i=0,...,h.
|
|
|
|
The layer masses are p_i = ell_i + x_i + x_{i+1}, with x_0=x_{h+1}=0.
|
|
Returns (F, alpha=p_0).
|
|
"""
|
|
if len(x) != h or len(ell) != h + 1:
|
|
raise ValueError("wrong coordinate lengths")
|
|
xx = [mp.mpf(0)] + [mp.mpf(v) for v in x] + [mp.mpf(0)]
|
|
ll = [mp.mpf(v) for v in ell]
|
|
if any(v < 0 for v in xx) or any(v < 0 for v in ll):
|
|
raise ValueError("coordinates must be nonnegative")
|
|
|
|
p = [ll[i] + xx[i] + xx[i + 1] for i in range(h + 1)]
|
|
if abs(sum(p) - 1) > mp.power(10, -(mp.mp.dps // 2)):
|
|
raise ValueError("profile is not normalized")
|
|
|
|
alpha = p[0]
|
|
value = (d - 1) * _xlogx(alpha)
|
|
for i in range(1, h + 1):
|
|
if p[i] == 0:
|
|
continue
|
|
a_i = xx[i] / p[i]
|
|
lower = mp.mpf(1) / d
|
|
numerical_tol = mp.power(10, -(mp.mp.dps // 3))
|
|
if a_i < lower - numerical_tol or a_i > 1 + numerical_tol:
|
|
raise ValueError("descent marginal is infeasible")
|
|
a_i = min(mp.mpf(1), max(lower, a_i))
|
|
y_i = p[i] - xx[i]
|
|
value += -_xlogx(p[i])
|
|
value += p[i] * conditioned_subset_entropy(d, a_i)
|
|
value += d * _xlogx(y_i)
|
|
value -= mp.mpf(d) / 2 * sum(_xlogx(v) for v in ll)
|
|
return value, alpha
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Reverse transfer and stationary reconstruction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class StationaryPoint:
|
|
d: int
|
|
h: int
|
|
terminal_t: mp.mpf
|
|
terminal: mp.mpf
|
|
z: mp.mpf
|
|
alpha: mp.mpf
|
|
phi_direct: mp.mpf
|
|
psi_direct: mp.mpf
|
|
phi_root: mp.mpf
|
|
psi_root: mp.mpf
|
|
kappa: mp.mpf
|
|
r0: mp.mpf
|
|
Zv: mp.mpf
|
|
Ze: mp.mpf
|
|
telescoping_residual: mp.mpf
|
|
root_pressure_residual: mp.mpf
|
|
root_micro_residual: mp.mpf
|
|
stationarity_residual: mp.mpf
|
|
compact_residual: mp.mpf
|
|
rho: list[mp.mpf]
|
|
u: list[mp.mpf]
|
|
A: list[mp.mpf]
|
|
Bmsg: list[mp.mpf]
|
|
|
|
|
|
def _w_e(rho: mp.mpf, b: int) -> tuple[mp.mpf, mp.mpf]:
|
|
"""Return w=(1-rho)^(1/b) and e=1-w without cancellation."""
|
|
log_w = mp.log1p(-rho) / b
|
|
return mp.exp(log_w), -mp.expm1(log_w)
|
|
|
|
|
|
def reverse_step(rho_next: mp.mpf, v_next: mp.mpf, b: int) -> tuple[mp.mpf, mp.mpf]:
|
|
r"""Invert one stationary transfer step.
|
|
|
|
Input lies in D={(rho,v): 0<rho<=v<=1}. The output is the unique
|
|
predecessor in D.
|
|
"""
|
|
w, e = _w_e(rho_next, b)
|
|
R = v_next * e / w
|
|
M = mp.power(e + w / v_next, b)
|
|
denominator = R + M
|
|
rho = R / denominator
|
|
v = (1 + R) / denominator
|
|
return rho, v
|
|
|
|
|
|
def _stable_power_difference(S: mp.mpf, B: mp.mpf, power: int) -> mp.mpf:
|
|
"""Compute S^power-(S-B)^power stably when B/S is tiny."""
|
|
ratio = B / S
|
|
return mp.power(S, power) * (-mp.expm1(power * mp.log1p(-ratio)))
|
|
|
|
|
|
def stationary_from_terminal_t(
|
|
d: int, h: int, terminal_t: mp.mpf, *, validate_compact: bool = True
|
|
) -> StationaryPoint:
|
|
r"""Construct the unique positive stationary orbit.
|
|
|
|
We use t=-log(1-s) rather than s directly. At high density the terminal
|
|
parameter s is extraordinarily close to one, while t remains numerically
|
|
well scaled.
|
|
"""
|
|
if d < 3 or h < 1:
|
|
raise ValueError("require d>=3 and h>=1")
|
|
b = d - 1
|
|
t = mp.mpf(terminal_t)
|
|
if t <= 0:
|
|
raise ValueError("terminal_t must be positive")
|
|
terminal = -mp.expm1(-t)
|
|
|
|
rho = [mp.mpf(0)] * (h + 1)
|
|
v = [mp.mpf(0)] * (h + 1)
|
|
u = [mp.mpf(0)] * (h + 1)
|
|
rho[h] = v[h] = terminal
|
|
for i in range(h - 1, 0, -1):
|
|
rho[i], v[i] = reverse_step(rho[i + 1], v[i + 1], b)
|
|
u[i] = v[i] - rho[i]
|
|
u[h] = mp.mpf(0)
|
|
|
|
w1, e1 = _w_e(rho[1], b)
|
|
if e1 == 0:
|
|
raise ArithmeticError("root coordinate under-resolved; increase mp.dps")
|
|
r0 = v[1] * e1 / w1
|
|
kappa = mp.power(v[1] / w1, b)
|
|
z = r0 * kappa / mp.power(1 + r0, b)
|
|
|
|
# Normalize A_1=1 and reconstruct the messages.
|
|
A = [mp.mpf(0)] * (h + 2)
|
|
Bmsg = [mp.mpf(0)] * (h + 1)
|
|
A[1] = mp.mpf(1)
|
|
for i in range(1, h):
|
|
A[i + 1] = u[i] * A[i]
|
|
Bmsg[0] = r0
|
|
for i in range(1, h + 1):
|
|
Bmsg[i] = rho[i] * A[i]
|
|
|
|
S = [mp.mpf(0)] * (h + 1)
|
|
S[0] = Bmsg[0] + A[1]
|
|
for i in range(1, h):
|
|
S[i] = Bmsg[i - 1] + Bmsg[i] + A[i + 1]
|
|
S[h] = Bmsg[h - 1] + Bmsg[h]
|
|
|
|
vertex_terms = [z * mp.power(S[0], d)]
|
|
for i in range(1, h + 1):
|
|
vertex_terms.append(_stable_power_difference(S[i], Bmsg[i - 1], d))
|
|
Zv = mp.fsum(vertex_terms)
|
|
Ze = mp.fsum(v * v for v in Bmsg) + 2 * mp.fsum(
|
|
Bmsg[i] * A[i + 1] for i in range(h)
|
|
)
|
|
|
|
alpha = Bmsg[0] * (Bmsg[0] + A[1]) / Ze
|
|
phi_direct = mp.log(Zv) - mp.mpf(d) / 2 * mp.log(Ze)
|
|
psi_direct = phi_direct - alpha * mp.log(z)
|
|
|
|
# Correct, well-conditioned root-only formulas.
|
|
phi_root = (
|
|
mp.log(z)
|
|
+ mp.mpf(d) / 2 * mp.log((1 + r0) / r0)
|
|
+ mp.mpf(d - 2) / 2 * mp.log(alpha)
|
|
)
|
|
psi_root = (
|
|
(1 - alpha) * mp.log(kappa)
|
|
- (mp.mpf(d - 2) / 2 + alpha) * mp.log(r0)
|
|
+ mp.mpf(d - 2) / 2 * mp.log(alpha)
|
|
+ (mp.mpf(d - 1) * alpha - mp.mpf(d - 2) / 2) * mp.log(1 + r0)
|
|
)
|
|
|
|
stationarity = [abs(kappa * Bmsg[0] - z * mp.power(S[0], b))]
|
|
for i in range(1, h + 1):
|
|
stationarity.append(abs(kappa * A[i] - mp.power(S[i], b)))
|
|
stationarity.append(
|
|
abs(
|
|
kappa * Bmsg[i]
|
|
- _stable_power_difference(S[i], Bmsg[i - 1], b)
|
|
)
|
|
)
|
|
|
|
# Reconstruct the compact profile only for final diagnostic points.
|
|
if validate_compact:
|
|
x = [Bmsg[i - 1] * A[i] / Ze for i in range(1, h + 1)]
|
|
ell = [Bmsg[i] * Bmsg[i] / Ze for i in range(h + 1)]
|
|
compact_value, compact_alpha = compact_microcanonical_value(d, h, x, ell)
|
|
compact_residual = max(abs(compact_value - psi_root), abs(compact_alpha - alpha))
|
|
else:
|
|
compact_residual = mp.nan
|
|
|
|
return StationaryPoint(
|
|
d=d,
|
|
h=h,
|
|
terminal_t=t,
|
|
terminal=terminal,
|
|
z=z,
|
|
alpha=alpha,
|
|
phi_direct=phi_direct,
|
|
psi_direct=psi_direct,
|
|
phi_root=phi_root,
|
|
psi_root=psi_root,
|
|
kappa=kappa,
|
|
r0=r0,
|
|
Zv=Zv,
|
|
Ze=Ze,
|
|
telescoping_residual=Zv - kappa * Ze,
|
|
root_pressure_residual=phi_direct - phi_root,
|
|
root_micro_residual=psi_direct - psi_root,
|
|
stationarity_residual=max(stationarity),
|
|
compact_residual=compact_residual,
|
|
rho=rho,
|
|
u=u,
|
|
A=A,
|
|
Bmsg=Bmsg,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Target-density solving and residual gates
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def solve_for_alpha_B(
|
|
d: int,
|
|
h: int,
|
|
C: mp.mpf,
|
|
*,
|
|
iterations: int | None = None,
|
|
) -> StationaryPoint:
|
|
"""Solve alpha B_h = C by bisection in terminal_t."""
|
|
B = tree_ball_volume(d, h)
|
|
target_alpha = mp.mpf(C) / B
|
|
if not 0 < target_alpha < 1:
|
|
raise ValueError("target density must lie in (0,1)")
|
|
if iterations is None:
|
|
iterations = max(140, 2 * mp.mp.dps)
|
|
|
|
D = mp.mpf(d) / (d - 2)
|
|
lo = mp.mpf("0.05") * C / D
|
|
hi = mp.mpf("3.0") * C / D + 1
|
|
while stationary_from_terminal_t(d, h, lo, validate_compact=False).alpha > target_alpha:
|
|
lo /= 2
|
|
while stationary_from_terminal_t(d, h, hi, validate_compact=False).alpha < target_alpha:
|
|
hi *= 2
|
|
|
|
for _ in range(iterations):
|
|
mid = (lo + hi) / 2
|
|
point = stationary_from_terminal_t(d, h, mid, validate_compact=False)
|
|
if point.alpha < target_alpha:
|
|
lo = mid
|
|
else:
|
|
hi = mid
|
|
return stationary_from_terminal_t(d, h, (lo + hi) / 2, validate_compact=True)
|
|
|
|
|
|
def relative_residual(residual: mp.mpf, reference: mp.mpf) -> mp.mpf:
|
|
return abs(residual) / max(abs(reference), mp.mpf(1))
|
|
|
|
|
|
def gate_point(point: StationaryPoint, route_tolerance: str = "1e-6") -> None:
|
|
"""Reject a row if an internal cross-check is too large to ignore.
|
|
|
|
The direct partition-function route can be ill conditioned near criticality.
|
|
The root-only formula is the reported value, but the direct route must still
|
|
agree to the requested relative tolerance at the working precision.
|
|
"""
|
|
tol = mp.mpf(route_tolerance)
|
|
if point.psi_root == 0:
|
|
raise RuntimeError("zero root-formula microcanonical exponent")
|
|
route_disagreement = abs(point.root_micro_residual / point.psi_root)
|
|
if route_disagreement > tol:
|
|
raise RuntimeError(
|
|
"microcanonical routes disagree: "
|
|
f"relative discrepancy={route_disagreement}; increase mp.dps"
|
|
)
|
|
if point.compact_residual > mp.sqrt(tol):
|
|
raise RuntimeError(
|
|
f"compact-functional reconstruction failed: {point.compact_residual}"
|
|
)
|
|
|
|
|
|
def diagnostic_row(d: int, h: int, W: mp.mpf | None = None) -> dict[str, str | int]:
|
|
"""Solve one near-critical case and return an audit-friendly record."""
|
|
B = tree_ball_volume(d, h)
|
|
L = mp.log(B)
|
|
if W is None:
|
|
W = mp.log(L)
|
|
C = near_critical_coordinate(d, h, W)
|
|
point = solve_for_alpha_B(d, h, C)
|
|
gate_point(point)
|
|
|
|
coupon = B * mp.power(1 - point.alpha, B)
|
|
activity_ratio = (-mp.log(point.z) - mp.log(1 / point.alpha)) / coupon
|
|
scale = mp.exp(-C)
|
|
route_disagreement = abs(point.root_micro_residual / point.psi_root)
|
|
|
|
return {
|
|
"d": d,
|
|
"h": h,
|
|
"dps": mp.mp.dps,
|
|
"B_h": mp.nstr(B, 30),
|
|
"C": mp.nstr(C, 24),
|
|
"alpha_B_h": mp.nstr(point.alpha * B, 24),
|
|
"terminal_t": mp.nstr(point.terminal_t, 30),
|
|
"activity_ratio": mp.nstr(activity_ratio, 20),
|
|
"minus_psi_over_exp_minus_C": mp.nstr(-point.psi_root / scale, 20),
|
|
"psi_root": mp.nstr(point.psi_root, 24),
|
|
"psi_direct": mp.nstr(point.psi_direct, 24),
|
|
"route_relative_disagreement": mp.nstr(route_disagreement, 12),
|
|
"stationarity_residual": mp.nstr(point.stationarity_residual, 12),
|
|
"telescoping_residual": mp.nstr(point.telescoping_residual, 12),
|
|
"compact_residual": mp.nstr(point.compact_residual, 12),
|
|
"source_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Tables and optional plotting
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def default_cases() -> list[tuple[int, int, int]]:
|
|
"""(d,h,dps) cases chosen to keep a demonstration run manageable."""
|
|
return [
|
|
(3, 12, 90),
|
|
(3, 20, 110),
|
|
(3, 30, 130),
|
|
(3, 40, 160),
|
|
(4, 12, 100),
|
|
(4, 20, 130),
|
|
(4, 28, 160),
|
|
]
|
|
|
|
|
|
def make_table(
|
|
cases: Iterable[tuple[int, int, int]],
|
|
*,
|
|
csv_path: str | None = None,
|
|
) -> list[dict[str, str | int]]:
|
|
rows: list[dict[str, str | int]] = []
|
|
for d, h, dps in cases:
|
|
mp.mp.dps = dps
|
|
row = diagnostic_row(d, h)
|
|
rows.append(row)
|
|
print(
|
|
f"d={d} h={h:>2} C={row['C']} "
|
|
f"activity={row['activity_ratio']} "
|
|
f"-Psi/e^-C={row['minus_psi_over_exp_minus_C']}"
|
|
)
|
|
if csv_path:
|
|
with open(csv_path, "w", newline="") as handle:
|
|
writer = csv.DictWriter(handle, fieldnames=list(rows[0].keys()))
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
return rows
|
|
|
|
|
|
def plot_rows(rows: Sequence[dict[str, str | int]], output: str) -> None:
|
|
"""Plot the two diagnostic ratios; matplotlib is optional."""
|
|
try:
|
|
import matplotlib.pyplot as plt
|
|
except ImportError as exc:
|
|
raise RuntimeError("install matplotlib to use --plot") from exc
|
|
|
|
by_d: dict[int, list[dict[str, str | int]]] = {}
|
|
for row in rows:
|
|
by_d.setdefault(int(row["d"]), []).append(row)
|
|
|
|
# Separate figures keep the diagnostics readable.
|
|
for field, ylabel, suffix in [
|
|
("activity_ratio", "activity-law ratio", "activity"),
|
|
("minus_psi_over_exp_minus_C", r"$-\Psi/e^{-C}$", "free_energy"),
|
|
]:
|
|
plt.figure()
|
|
for d, group in sorted(by_d.items()):
|
|
group = sorted(group, key=lambda r: int(r["h"]))
|
|
plt.plot(
|
|
[int(r["h"]) for r in group],
|
|
[float(r[field]) for r in group],
|
|
marker="o",
|
|
label=f"d={d}",
|
|
)
|
|
plt.axhline(1.0, linestyle="--")
|
|
plt.xlabel("radius h")
|
|
plt.ylabel(ylabel)
|
|
plt.legend()
|
|
plt.tight_layout()
|
|
path = str(Path(output).with_name(Path(output).stem + f"_{suffix}.png"))
|
|
plt.savefig(path, dpi=180)
|
|
plt.close()
|
|
print(path)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Command line interface
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--d", type=int, default=3)
|
|
parser.add_argument("--h", type=int, default=20)
|
|
parser.add_argument("--dps", type=int, default=110)
|
|
parser.add_argument(
|
|
"--W",
|
|
type=str,
|
|
default=None,
|
|
help="additive gap W; default is log log B_h",
|
|
)
|
|
parser.add_argument("--table", action="store_true", help="run the curated table")
|
|
parser.add_argument("--csv", type=str, default=None, help="optional CSV output")
|
|
parser.add_argument("--plot", type=str, default=None, help="plot prefix for table output")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
if args.table:
|
|
rows = make_table(default_cases(), csv_path=args.csv)
|
|
if args.plot:
|
|
plot_rows(rows, args.plot)
|
|
return
|
|
|
|
mp.mp.dps = args.dps
|
|
W = None if args.W is None else mp.mpf(args.W)
|
|
row = diagnostic_row(args.d, args.h, W)
|
|
for key, value in row.items():
|
|
print(f"{key}: {value}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|