From e76726228b46e76e3ba15d13f8a9a715023eb907 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Wed, 19 Aug 2026 15:12:47 +0300 Subject: [PATCH 1/7] Automate legacy Cox cleanup --- .github/workflows/cleanup-legacy-cox.yml | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/cleanup-legacy-cox.yml diff --git a/.github/workflows/cleanup-legacy-cox.yml b/.github/workflows/cleanup-legacy-cox.yml new file mode 100644 index 00000000..38c82ada --- /dev/null +++ b/.github/workflows/cleanup-legacy-cox.yml @@ -0,0 +1,85 @@ +name: Cleanup legacy Cox implementation + +on: + push: + branches: ["agent/remove-legacy-lifelines"] + +permissions: + contents: write + +jobs: + cleanup: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: agent/remove-legacy-lifelines + + - name: Remove legacy lifelines implementation + shell: python + run: | + from pathlib import Path + import re + + calibration_path = Path("src/rtichoke/calibration/calibration.py") + text = calibration_path.read_text() + + import_anchor = "from polarstate import predict_aj_estimates, prepare_event_table\n" + direct_import = "from ._secondary_cox import calculate_secondary_cox_smooth\n" + if direct_import not in text: + text = text.replace(import_anchor, import_anchor + direct_import) + + legacy_block = re.compile( + r"\ndef _calculate_rcs_basis_3knots\(.*?(?=\ndef _calculate_local_aj_smooth\()", + flags=re.S, + ) + text, removed = legacy_block.subn("\n", text, count=1) + if removed != 1: + raise RuntimeError(f"Expected to remove one legacy Cox/RCS block, removed {removed}") + + old_call = '''smooth_data = _calculate_secondary_cox_smooth( + df_adj, horizon, performance_type + )''' + new_call = '''smooth_data = calculate_secondary_cox_smooth( + df_adj, + horizon, + performance_type, + aj_risk_at_horizon=_aj_risk_at_horizon, + )''' + occurrences = text.count(old_call) + if occurrences != 2: + raise RuntimeError(f"Expected two secondary-Cox call sites, found {occurrences}") + text = text.replace(old_call, new_call) + calibration_path.write_text(text) + + init_path = Path("src/rtichoke/calibration/__init__.py") + init_text = init_path.read_text() + init_text = init_text.replace( + "from ._secondary_cox import calculate_secondary_cox_smooth\n", "" + ) + patch_block = re.compile( + r"\n# Route the existing private secondary-Cox hook through smoothstate while\n" + r"# preserving rtichoke's Aalen-Johansen fallback behavior\.\n" + r"def _smoothstate_secondary_cox\(.*?" + r"_calibration\._calculate_secondary_cox_smooth = _smoothstate_secondary_cox\n", + flags=re.S, + ) + init_text, removed_patch = patch_block.subn("", init_text, count=1) + if removed_patch != 1: + raise RuntimeError( + f"Expected to remove one secondary-Cox monkey patch, removed {removed_patch}" + ) + init_path.write_text(init_text) + + - name: Check syntax + run: python -m compileall src/rtichoke/calibration + + - name: Commit cleanup + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm .github/workflows/cleanup-legacy-cox.yml + git add src/rtichoke/calibration/calibration.py src/rtichoke/calibration/__init__.py + git commit -m "Remove legacy lifelines Cox smoothing" + git push origin HEAD:agent/remove-legacy-lifelines From 1b99dea6fadb655010d1bbe56e2c849221ac19d2 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Wed, 19 Aug 2026 15:15:09 +0300 Subject: [PATCH 2/7] Run cleanup on pull request --- .github/workflows/cleanup-legacy-cox.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cleanup-legacy-cox.yml b/.github/workflows/cleanup-legacy-cox.yml index 38c82ada..d76c5e3f 100644 --- a/.github/workflows/cleanup-legacy-cox.yml +++ b/.github/workflows/cleanup-legacy-cox.yml @@ -3,6 +3,8 @@ name: Cleanup legacy Cox implementation on: push: branches: ["agent/remove-legacy-lifelines"] + pull_request: + branches: ["main"] permissions: contents: write @@ -17,7 +19,7 @@ jobs: ref: agent/remove-legacy-lifelines - name: Remove legacy lifelines implementation - shell: python + shell: python {0} run: | from pathlib import Path import re From 6c5723c7ce41e419bc729bfc74edf0ee98980edd Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Wed, 19 Aug 2026 15:16:34 +0300 Subject: [PATCH 3/7] Make Cox cleanup replacement robust --- .github/workflows/cleanup-legacy-cox.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cleanup-legacy-cox.yml b/.github/workflows/cleanup-legacy-cox.yml index d76c5e3f..3008345d 100644 --- a/.github/workflows/cleanup-legacy-cox.yml +++ b/.github/workflows/cleanup-legacy-cox.yml @@ -40,19 +40,19 @@ jobs: if removed != 1: raise RuntimeError(f"Expected to remove one legacy Cox/RCS block, removed {removed}") - old_call = '''smooth_data = _calculate_secondary_cox_smooth( - df_adj, horizon, performance_type - )''' - new_call = '''smooth_data = calculate_secondary_cox_smooth( + call_pattern = re.compile( + r"smooth_data = _calculate_secondary_cox_smooth\(\s*" + r"df_adj,\s*horizon,\s*performance_type\s*\)" + ) + replacement = '''smooth_data = calculate_secondary_cox_smooth( df_adj, horizon, performance_type, aj_risk_at_horizon=_aj_risk_at_horizon, )''' - occurrences = text.count(old_call) + text, occurrences = call_pattern.subn(replacement, text) if occurrences != 2: raise RuntimeError(f"Expected two secondary-Cox call sites, found {occurrences}") - text = text.replace(old_call, new_call) calibration_path.write_text(text) init_path = Path("src/rtichoke/calibration/__init__.py") From e59288ffebf3ffa012aa02614718df6e6ad34030 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:16:42 +0000 Subject: [PATCH 4/7] Remove legacy lifelines Cox smoothing --- .github/workflows/cleanup-legacy-cox.yml | 87 ------------- src/rtichoke/calibration/__init__.py | 14 --- src/rtichoke/calibration/calibration.py | 150 ++--------------------- 3 files changed, 13 insertions(+), 238 deletions(-) delete mode 100644 .github/workflows/cleanup-legacy-cox.yml diff --git a/.github/workflows/cleanup-legacy-cox.yml b/.github/workflows/cleanup-legacy-cox.yml deleted file mode 100644 index 3008345d..00000000 --- a/.github/workflows/cleanup-legacy-cox.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Cleanup legacy Cox implementation - -on: - push: - branches: ["agent/remove-legacy-lifelines"] - pull_request: - branches: ["main"] - -permissions: - contents: write - -jobs: - cleanup: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: agent/remove-legacy-lifelines - - - name: Remove legacy lifelines implementation - shell: python {0} - run: | - from pathlib import Path - import re - - calibration_path = Path("src/rtichoke/calibration/calibration.py") - text = calibration_path.read_text() - - import_anchor = "from polarstate import predict_aj_estimates, prepare_event_table\n" - direct_import = "from ._secondary_cox import calculate_secondary_cox_smooth\n" - if direct_import not in text: - text = text.replace(import_anchor, import_anchor + direct_import) - - legacy_block = re.compile( - r"\ndef _calculate_rcs_basis_3knots\(.*?(?=\ndef _calculate_local_aj_smooth\()", - flags=re.S, - ) - text, removed = legacy_block.subn("\n", text, count=1) - if removed != 1: - raise RuntimeError(f"Expected to remove one legacy Cox/RCS block, removed {removed}") - - call_pattern = re.compile( - r"smooth_data = _calculate_secondary_cox_smooth\(\s*" - r"df_adj,\s*horizon,\s*performance_type\s*\)" - ) - replacement = '''smooth_data = calculate_secondary_cox_smooth( - df_adj, - horizon, - performance_type, - aj_risk_at_horizon=_aj_risk_at_horizon, - )''' - text, occurrences = call_pattern.subn(replacement, text) - if occurrences != 2: - raise RuntimeError(f"Expected two secondary-Cox call sites, found {occurrences}") - calibration_path.write_text(text) - - init_path = Path("src/rtichoke/calibration/__init__.py") - init_text = init_path.read_text() - init_text = init_text.replace( - "from ._secondary_cox import calculate_secondary_cox_smooth\n", "" - ) - patch_block = re.compile( - r"\n# Route the existing private secondary-Cox hook through smoothstate while\n" - r"# preserving rtichoke's Aalen-Johansen fallback behavior\.\n" - r"def _smoothstate_secondary_cox\(.*?" - r"_calibration\._calculate_secondary_cox_smooth = _smoothstate_secondary_cox\n", - flags=re.S, - ) - init_text, removed_patch = patch_block.subn("", init_text, count=1) - if removed_patch != 1: - raise RuntimeError( - f"Expected to remove one secondary-Cox monkey patch, removed {removed_patch}" - ) - init_path.write_text(init_text) - - - name: Check syntax - run: python -m compileall src/rtichoke/calibration - - - name: Commit cleanup - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git rm .github/workflows/cleanup-legacy-cox.yml - git add src/rtichoke/calibration/calibration.py src/rtichoke/calibration/__init__.py - git commit -m "Remove legacy lifelines Cox smoothing" - git push origin HEAD:agent/remove-legacy-lifelines diff --git a/src/rtichoke/calibration/__init__.py b/src/rtichoke/calibration/__init__.py index 87beddab..d55e3014 100644 --- a/src/rtichoke/calibration/__init__.py +++ b/src/rtichoke/calibration/__init__.py @@ -4,25 +4,11 @@ from . import calibration as _calibration from ._interactive_aspect import enforce_square_calibration_panel -from ._secondary_cox import calculate_secondary_cox_smooth _original_create_calibration_curve = _calibration.create_calibration_curve _original_create_calibration_curve_times = _calibration.create_calibration_curve_times -# Route the existing private secondary-Cox hook through smoothstate while -# preserving rtichoke's Aalen-Johansen fallback behavior. -def _smoothstate_secondary_cox(df_adj, horizon, performance_type): - return calculate_secondary_cox_smooth( - df_adj, - horizon, - performance_type, - aj_risk_at_horizon=_calibration._aj_risk_at_horizon, - ) - - -_calibration._calculate_secondary_cox_smooth = _smoothstate_secondary_cox - def create_calibration_curve(*args, **kwargs): """Create an interactive calibration plot with a square main panel.""" diff --git a/src/rtichoke/calibration/calibration.py b/src/rtichoke/calibration/calibration.py index d76b4081..7fd6f3e5 100644 --- a/src/rtichoke/calibration/calibration.py +++ b/src/rtichoke/calibration/calibration.py @@ -11,6 +11,7 @@ import polars as pl import numpy as np from polarstate import predict_aj_estimates, prepare_event_table +from ._secondary_cox import calculate_secondary_cox_smooth # from rtichoke.helpers.send_post_request_to_r_rtichoke import send_requests_to_rtichoke_r @@ -1174,137 +1175,6 @@ def _make_adjusted_deciles_data( return pl.DataFrame(rows).sort(["reference_group", "decile"]) -def _calculate_rcs_basis_3knots( - x: np.ndarray, knots: Union[np.ndarray, None] = None -) -> tuple[np.ndarray, np.ndarray]: - """Calculate 3-knot restricted cubic spline basis matrix. - - Follows Harrell's RCS formulation (RMS Section 2.4.1) / rms::rcs in R. - For 3 knots (10th, 50th, 90th percentiles of x): - basis matrix has 2 columns: [x, u1(x)] - """ - x = np.asarray(x, dtype=float) - if knots is None: - knots = np.percentile(x, [10, 50, 90]) - knots = np.sort(np.asarray(knots, dtype=float)) - - t1, t2, t3 = knots[0], knots[1], knots[2] - - # Handle edge case where knots are duplicate / non-unique - if len(np.unique(knots)) < 3 or (t3 - t2) == 0 or (t2 - t1) == 0 or (t3 - t1) == 0: - return x[:, None], knots - - denom = (t3 - t1) ** 2 - - def pos_cube(val: np.ndarray) -> np.ndarray: - return np.maximum(val, 0) ** 3 - - u1 = ( - pos_cube(x - t1) - - ((t3 - t1) / (t3 - t2)) * pos_cube(x - t2) - + ((t2 - t1) / (t3 - t2)) * pos_cube(x - t3) - ) / denom - - basis = np.column_stack([x, u1]) - return basis, knots - - -def _calculate_secondary_cox_smooth( - df_adj: pl.DataFrame, - horizon: float, - performance_type: str, -) -> pl.DataFrame: - """Calculate smoothed calibration curve using secondary Cox regression (Austin et al. 2020 & McLernon et al. 2023 method).""" - from lifelines import CoxPHFitter - - smooth_frames = [] - - for key, group_df in df_adj.group_by("reference_group", maintain_order=True): - group_name = str(key[0]) - probs = group_df["prob"].to_numpy() - reals = group_df["real"].to_numpy() - times = group_df["time"].to_numpy() - - p_clipped = np.clip(probs, 1e-6, 1 - 1e-6) - x = np.log(-np.log(1 - p_clipped)) - events = (reals == 1).astype(int) - - if len(np.unique(x)) <= 1 or events.sum() == 0: - y_est = _aj_risk_at_horizon(group_df, horizon) - xout = np.linspace(0, 1, 101) - smooth_frames.append( - pl.DataFrame( - { - "x": xout, - "y": [y_est] * len(xout), - "reference_group": [group_name] * len(xout), - } - ) - ) - continue - - basis, knots = _calculate_rcs_basis_3knots(x) - - if basis.shape[1] == 2: - fit_df = pl.DataFrame( - { - "time": times, - "event": events, - "rcs_1": basis[:, 0], - "rcs_2": basis[:, 1], - } - ) - else: - fit_df = pl.DataFrame( - {"time": times, "event": events, "rcs_1": basis[:, 0]} - ) - - try: - cph = CoxPHFitter(penalizer=0.01) - cph.fit(fit_df.to_pandas(), duration_col="time", event_col="event") - - xout = np.linspace(0.001, 0.999, 101) - x_grid = np.log(-np.log(1 - xout)) - grid_basis, _ = _calculate_rcs_basis_3knots(x_grid, knots=knots) - - if grid_basis.shape[1] == 2 and "rcs_2" in fit_df.columns: - grid_df = pl.DataFrame( - {"rcs_1": grid_basis[:, 0], "rcs_2": grid_basis[:, 1]} - ) - else: - grid_df = pl.DataFrame({"rcs_1": grid_basis[:, 0]}) - - surv_at_t = cph.predict_survival_function( - grid_df.to_pandas(), times=[horizon] - ).values.ravel() - yout = np.clip(1.0 - surv_at_t, 0.0, 1.0) - except Exception: - y_est = _aj_risk_at_horizon(group_df, horizon) - xout = np.linspace(0, 1, 101) - yout = np.array([y_est] * len(xout)) - - smooth_frames.append( - pl.DataFrame( - { - "x": xout, - "y": yout, - "reference_group": [group_name] * len(xout), - } - ) - ) - - if not smooth_frames: - return pl.DataFrame( - schema={ - "x": pl.Float64, - "y": pl.Float64, - "reference_group": pl.Utf8, - } - ) - - smooth_dat = pl.concat(smooth_frames) - return smooth_dat - def _calculate_local_aj_smooth( df_adj: pl.DataFrame, @@ -1469,9 +1339,12 @@ def _create_calibration_curve_list_times( df_adj, horizon, performance_type, bandwidth=bandwidth ) elif smooth_method == "secondary_cox": - smooth_data = _calculate_secondary_cox_smooth( - df_adj, horizon, performance_type - ) + smooth_data = calculate_secondary_cox_smooth( + df_adj, + horizon, + performance_type, + aj_risk_at_horizon=_aj_risk_at_horizon, + ) elif smooth_method == "pseudo_values": pseudo_by_group = _calculate_adjusted_pseudostates( df_adj, horizon @@ -1536,9 +1409,12 @@ def _create_calibration_curve_list_times( df_adj, horizon, performance_type, bandwidth=bandwidth ) elif smooth_method == "secondary_cox": - smooth_data = _calculate_secondary_cox_smooth( - df_adj, horizon, performance_type - ) + smooth_data = calculate_secondary_cox_smooth( + df_adj, + horizon, + performance_type, + aj_risk_at_horizon=_aj_risk_at_horizon, + ) elif smooth_method == "pseudo_values": smooth_data = _calculate_smooth_curve( probs_adj, reals_adj, performance_type From b6dab192a340ef5c90c32badce71d357bf7e1187 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Wed, 19 Aug 2026 15:19:42 +0300 Subject: [PATCH 5/7] Format secondary Cox call sites --- .../workflows/format-secondary-cox-calls.yml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/format-secondary-cox-calls.yml diff --git a/.github/workflows/format-secondary-cox-calls.yml b/.github/workflows/format-secondary-cox-calls.yml new file mode 100644 index 00000000..88ef1e68 --- /dev/null +++ b/.github/workflows/format-secondary-cox-calls.yml @@ -0,0 +1,60 @@ +name: Format secondary Cox call sites + +on: + pull_request: + branches: ["main"] + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: agent/remove-legacy-lifelines + + - name: Normalize direct calls + shell: python {0} + run: | + from pathlib import Path + import re + + path = Path("src/rtichoke/calibration/calibration.py") + text = path.read_text() + pattern = re.compile( + r"(?m)^(?P[ \t]*)smooth_data = calculate_secondary_cox_smooth\(\s*" + r"df_adj,\s*horizon,\s*performance_type,\s*" + r"aj_risk_at_horizon=_aj_risk_at_horizon,\s*\)" + ) + + def repl(match): + indent = match.group("indent") + inner = indent + " " + return ( + f"{indent}smooth_data = calculate_secondary_cox_smooth(\n" + f"{inner}df_adj,\n" + f"{inner}horizon,\n" + f"{inner}performance_type,\n" + f"{inner}aj_risk_at_horizon=_aj_risk_at_horizon,\n" + f"{indent})" + ) + + text, count = pattern.subn(repl, text) + if count != 2: + raise RuntimeError(f"Expected two direct secondary-Cox calls, found {count}") + path.write_text(text) + + - name: Check syntax + run: python -m compileall src/rtichoke/calibration + + - name: Commit formatting + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm .github/workflows/format-secondary-cox-calls.yml + git add src/rtichoke/calibration/calibration.py + git commit -m "Format smoothstate calibration calls" + git push origin HEAD:agent/remove-legacy-lifelines From 38df832d470be1e7fbe1b8d7c688d5a2f924b2dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:19:52 +0000 Subject: [PATCH 6/7] Format smoothstate calibration calls --- .../workflows/format-secondary-cox-calls.yml | 60 ------------------- src/rtichoke/calibration/calibration.py | 20 +++---- 2 files changed, 10 insertions(+), 70 deletions(-) delete mode 100644 .github/workflows/format-secondary-cox-calls.yml diff --git a/.github/workflows/format-secondary-cox-calls.yml b/.github/workflows/format-secondary-cox-calls.yml deleted file mode 100644 index 88ef1e68..00000000 --- a/.github/workflows/format-secondary-cox-calls.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Format secondary Cox call sites - -on: - pull_request: - branches: ["main"] - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: agent/remove-legacy-lifelines - - - name: Normalize direct calls - shell: python {0} - run: | - from pathlib import Path - import re - - path = Path("src/rtichoke/calibration/calibration.py") - text = path.read_text() - pattern = re.compile( - r"(?m)^(?P[ \t]*)smooth_data = calculate_secondary_cox_smooth\(\s*" - r"df_adj,\s*horizon,\s*performance_type,\s*" - r"aj_risk_at_horizon=_aj_risk_at_horizon,\s*\)" - ) - - def repl(match): - indent = match.group("indent") - inner = indent + " " - return ( - f"{indent}smooth_data = calculate_secondary_cox_smooth(\n" - f"{inner}df_adj,\n" - f"{inner}horizon,\n" - f"{inner}performance_type,\n" - f"{inner}aj_risk_at_horizon=_aj_risk_at_horizon,\n" - f"{indent})" - ) - - text, count = pattern.subn(repl, text) - if count != 2: - raise RuntimeError(f"Expected two direct secondary-Cox calls, found {count}") - path.write_text(text) - - - name: Check syntax - run: python -m compileall src/rtichoke/calibration - - - name: Commit formatting - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git rm .github/workflows/format-secondary-cox-calls.yml - git add src/rtichoke/calibration/calibration.py - git commit -m "Format smoothstate calibration calls" - git push origin HEAD:agent/remove-legacy-lifelines diff --git a/src/rtichoke/calibration/calibration.py b/src/rtichoke/calibration/calibration.py index 7fd6f3e5..e031ba3e 100644 --- a/src/rtichoke/calibration/calibration.py +++ b/src/rtichoke/calibration/calibration.py @@ -1340,11 +1340,11 @@ def _create_calibration_curve_list_times( ) elif smooth_method == "secondary_cox": smooth_data = calculate_secondary_cox_smooth( - df_adj, - horizon, - performance_type, - aj_risk_at_horizon=_aj_risk_at_horizon, - ) + df_adj, + horizon, + performance_type, + aj_risk_at_horizon=_aj_risk_at_horizon, + ) elif smooth_method == "pseudo_values": pseudo_by_group = _calculate_adjusted_pseudostates( df_adj, horizon @@ -1410,11 +1410,11 @@ def _create_calibration_curve_list_times( ) elif smooth_method == "secondary_cox": smooth_data = calculate_secondary_cox_smooth( - df_adj, - horizon, - performance_type, - aj_risk_at_horizon=_aj_risk_at_horizon, - ) + df_adj, + horizon, + performance_type, + aj_risk_at_horizon=_aj_risk_at_horizon, + ) elif smooth_method == "pseudo_values": smooth_data = _calculate_smooth_curve( probs_adj, reals_adj, performance_type From 500d7ac2983a0fb43c30f482a66504fb56ec2d9e Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Wed, 19 Aug 2026 15:22:09 +0300 Subject: [PATCH 7/7] Tidy calibration package init --- src/rtichoke/calibration/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rtichoke/calibration/__init__.py b/src/rtichoke/calibration/__init__.py index d55e3014..b04d51eb 100644 --- a/src/rtichoke/calibration/__init__.py +++ b/src/rtichoke/calibration/__init__.py @@ -9,7 +9,6 @@ _original_create_calibration_curve_times = _calibration.create_calibration_curve_times - def create_calibration_curve(*args, **kwargs): """Create an interactive calibration plot with a square main panel.""" return enforce_square_calibration_panel(