Separate uncertain density observations from plotted trends
This commit is contained in:
@@ -193,6 +193,19 @@ def write_csv(path, table, fields=None):
|
||||
writer.writerows(table)
|
||||
|
||||
|
||||
def plot_observation(row, field, negative=False, log=True):
|
||||
"""Keep unresolved sign measurements out of resolved convergence trends."""
|
||||
value = row[field]
|
||||
if value is None:
|
||||
return None, "undefined"
|
||||
finite(value, "plotted " + field, log)
|
||||
if log and value == 0:
|
||||
return None, "observed zero"
|
||||
if negative and row["negative_classification"] == "uncertain_witnesses_only":
|
||||
return None, f"uncertain-only observation {value:.6g}"
|
||||
return value, None
|
||||
|
||||
|
||||
def plot_figure(frozen, coupled, output):
|
||||
os.environ.setdefault("MPLCONFIGDIR", str(output / "matplotlib_cache"))
|
||||
import matplotlib
|
||||
@@ -205,6 +218,8 @@ def plot_figure(frozen, coupled, output):
|
||||
row_count = bool(frozen) + 2 * bool(coupled)
|
||||
require(row_count > 0, "No audited rows to plot")
|
||||
fig, axes = plt.subplots(row_count, 3, figsize=(13.8, 3.9 * row_count + .7), squeeze=False)
|
||||
model_keys = sorted({(row["n"], row["J"]) for row in [*frozen, *coupled]})
|
||||
colors = {key: plt.get_cmap("tab10")(index % 10) for index, key in enumerate(model_keys)}
|
||||
|
||||
def groups(table, frozen_case):
|
||||
result = defaultdict(list)
|
||||
@@ -215,32 +230,32 @@ def plot_figure(frozen, coupled, output):
|
||||
|
||||
def panel(ax, grouped, field, title, ylabel, negative=False, log=True, second=None, legend=False):
|
||||
annotations, positive = [], False
|
||||
for index, group in enumerate(grouped):
|
||||
color = plt.get_cmap("tab10")(index % 10)
|
||||
for group in grouped:
|
||||
color = colors[(group[0]["n"], group[0]["J"])]
|
||||
label = group[0]["case"]
|
||||
x = [r["density_degree"] for r in group]
|
||||
series = [(field, "-")] + ([(second, "--")] if second else [])
|
||||
for metric, style in series:
|
||||
values = []
|
||||
for row in group:
|
||||
value = row[metric]
|
||||
if value is None or (log and value == 0):
|
||||
annotations.append(f"{label} Q{row['density_degree']}: {'undefined' if value is None else 'observed zero'}"
|
||||
value, note = plot_observation(row, metric, negative, log)
|
||||
if note is not None:
|
||||
annotations.append(f"{label} Q{row['density_degree']}: {note}"
|
||||
+ (" (force)" if metric == second else ""))
|
||||
# NaN breaks the line, so a tiny uncertain-only number
|
||||
# cannot look like resolved orders-of-magnitude gain.
|
||||
values.append(math.nan)
|
||||
continue
|
||||
finite(value, "plotted " + metric, log)
|
||||
positive = True
|
||||
values.append(value)
|
||||
not_converged = row.get("converged") is False
|
||||
uncertain = negative and row["negative_classification"] == "uncertain_witnesses_only"
|
||||
ax.plot(row["density_degree"], value, marker="x" if not_converged else "o", markersize=5,
|
||||
color=color, markerfacecolor="none" if uncertain else color, linestyle="none")
|
||||
color=color, markerfacecolor=color, linestyle="none")
|
||||
ax.plot(x, values, style, color=color, linewidth=1.15, label=label if metric == field else None)
|
||||
if log and positive:
|
||||
ax.set_yscale("log")
|
||||
if not positive:
|
||||
ax.text(.5, .5, "No positive finite observations" if log else "No finite observations",
|
||||
ax.text(.5, .5, "No positive values in trend" if log else "No finite values in trend",
|
||||
ha="center", va="center", transform=ax.transAxes)
|
||||
ax.set_xticks([2, 3, 4], ["Q2", "Q3", "Q4"])
|
||||
ax.set_xlim(1.85, 4.15)
|
||||
@@ -273,7 +288,7 @@ def plot_figure(frozen, coupled, output):
|
||||
panel(axes[row, 1], grouped, "newton_iteration_seconds", "Newton iteration cost", "Seconds (excludes observer and setup)", log=False)
|
||||
panel(axes[row, 2], grouped, "krylov_iterations", "Total reported Krylov work", "Iterations", log=False)
|
||||
fig.suptitle("Density approximation order — audited observations", fontsize=14, y=.985)
|
||||
fig.text(.025, .018, "Open markers: uncertain-only negative witnesses; ×: nonlinear solve did not converge. Zero/undefined values are listed below panels, not placed on a log floor.\n"
|
||||
fig.text(.025, .018, "Colors identify (n, J) consistently; ×: nonlinear solve did not converge. Uncertain-only negatives, zeros and undefined values are listed below panels, never placed on a log floor.\n"
|
||||
"Negative regions use root-sliced quadrature; EOS norms use unsliced q30. P(h) virials do not certify density-EOS validity. See CSV/JSON for all outcomes and timing scope.", fontsize=8)
|
||||
fig.subplots_adjust(left=.075, right=.985, top=.92 if row_count == 1 else .95, bottom=.15 if row_count == 1 else .085,
|
||||
wspace=.35, hspace=.72)
|
||||
@@ -283,6 +298,18 @@ def plot_figure(frozen, coupled, output):
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_uncertain_plot_observation_breaks_trend(self):
|
||||
row = {"negative_mass": 1e-30, "negative_classification": "uncertain_witnesses_only"}
|
||||
plotted, note = plot_observation(row, "negative_mass", negative=True)
|
||||
self.assertIsNone(plotted)
|
||||
self.assertIn("1e-30", note)
|
||||
self.assertEqual(row["negative_mass"], 1e-30)
|
||||
self.assertEqual(plot_observation(row, "negative_mass", negative=False), (1e-30, None))
|
||||
row["negative_classification"] = "resolved_witnesses_present"
|
||||
self.assertEqual(plot_observation(row, "negative_mass", negative=True), (1e-30, None))
|
||||
row["negative_mass"] = 0.
|
||||
self.assertEqual(plot_observation(row, "negative_mass", negative=True), (None, "observed zero"))
|
||||
|
||||
def test_zero_and_undefined_are_not_floored(self):
|
||||
fine = {k: 0. for k in ("negative_mass", "negative_volume", "negative_mass_fraction", "negative_volume_fraction",
|
||||
"negative_mass_uncertainty_scale", "resolved_negative_intervals", "uncertain_negative_intervals", "uncertain_negative_mass")}
|
||||
@@ -394,6 +421,7 @@ def main():
|
||||
"Pressure-force and residual-metric rejection counts retain the recorded stage distinction; residual-metric counts include any nonfinite metric failures as well as insufficient decrease.",
|
||||
"Ratios to Q2 compare recorded endpoints and do not imply equal convergence or error tolerances were attained.",
|
||||
"Undefined values are null/blank; zero is kept exactly. No logarithmic plot floor is inserted.",
|
||||
"Uncertain-only negative observations retain exact table values and appear as separate plot annotations; resolved trends do not connect through them. Colors are stable by (n,J).",
|
||||
"P(h) virial milestones do not establish pointwise density validity; density-based virials are retained separately."],
|
||||
"frozen_projection": frozen, "coupled_outcomes": coupled, "relative_to_Q2": relative,
|
||||
"artifacts_sha256": {path.name: digest(path) for path in args.output.iterdir() if path.is_file()}}
|
||||
|
||||
Reference in New Issue
Block a user