Download services/plotting.py from lopezels/pdf_plots: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/spaces/lopezels/pdf_plots/resolve/main/services/plotting.py
- Command line
-
hf download hf://spaces/lopezels/pdf_plots/services/plotting.py
-
curl -L -o plotting.py https://huggingface.co/spaces/lopezels/pdf_plots/resolve/main/services/plotting.py
16.8 kB
| """ | |
| Plotly figure builders using the "standard" PDF-uncertainty style the | |
| user prefers: a central curve + uncertainty band computed via LHAPDF's | |
| own `PDFSet.uncertainty()`, and a Hessian-style correlation contour | |
| between two flavors. | |
| IMPORTANT: `PDFSet.uncertainty()` is only mathematically valid for | |
| error types "hessian", "symmhessian", and "replicas". Every function | |
| here assumes the caller already validated that with | |
| `services.pdf_utils.load_validated_pdf_set` — these functions do not | |
| re-check it themselves, to keep the validation in exactly one place. | |
| Nothing in this module talks to Hugging Face or FastAPI directly; each | |
| function takes an already-loaded `pdf_set` and returns a | |
| `plotly.graph_objects.Figure`. | |
| """ | |
| import logging | |
| import time | |
| from typing import Optional | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| from services.colors import get_main_plot_label, get_parton_color, get_parton_name, get_parton_symbol | |
| logger = logging.getLogger(__name__) | |
| def build_standard_plot( | |
| pdf_set, | |
| pdfs, | |
| grid_name: str, | |
| q_scale: float, | |
| parton_id: int, | |
| plot_color: Optional[str] = None, | |
| x_vals=None, | |
| custom_title: Optional[str] = None, | |
| ) -> go.Figure: | |
| """ | |
| Central value + uncertainty band for x*f(x, Q) of a single flavor. | |
| `pdfs` is `pdf_set.mkPDFs()`, passed in already-built (see | |
| services.pdf_utils.get_pdf_members) so repeat requests for the same | |
| PDF set don't reload every member's grid file from disk each time. | |
| """ | |
| plot_color = plot_color or get_parton_color(parton_id) | |
| if x_vals is None: | |
| x_vals = np.logspace(-5, -0.01, 150) | |
| logger.info( | |
| "build_standard_plot: grid=%s parton_id=%s q_scale=%s n_points=%d members=%d", | |
| grid_name, parton_id, q_scale, len(x_vals), len(pdfs), | |
| ) | |
| t0 = time.monotonic() | |
| central_values, upper_band, lower_band = [], [], [] | |
| for x in x_vals: | |
| vals = [pdf.xfxQ(parton_id, x, q_scale) for pdf in pdfs] | |
| uncertainty = pdf_set.uncertainty(vals) | |
| central = uncertainty.central | |
| central_values.append(round(central, 6)) | |
| upper_band.append(round(central + uncertainty.errplus, 6)) | |
| lower_band.append(round(central - uncertainty.errminus, 6)) | |
| logger.info("build_standard_plot: evaluated %d points in %.2fs", len(x_vals), time.monotonic() - t0) | |
| fig = go.Figure() | |
| x_vals_rounded = np.round(x_vals, 6) | |
| x_band = np.concatenate([x_vals_rounded, x_vals_rounded[::-1]]) | |
| y_band = np.concatenate([upper_band, lower_band[::-1]]) | |
| fig.add_trace(go.Scatter( | |
| x=x_band, | |
| y=y_band, | |
| fill="toself", | |
| fillcolor=plot_color, | |
| opacity=0.3, | |
| line=dict(color="rgba(255,255,255,0)"), | |
| name="Uncertainty", | |
| hoverinfo="skip", | |
| )) | |
| hover_custom_data = np.stack((upper_band, lower_band), axis=-1) | |
| fig.add_trace(go.Scatter( | |
| x=x_vals, | |
| y=central_values, | |
| mode="lines", | |
| line=dict(color=plot_color, width=2), | |
| name=f"{grid_name} Central <br>Q={q_scale} GeV", | |
| customdata=hover_custom_data, | |
| hovertemplate=( | |
| "<b>x:</b> %{x:.4e}<br>" | |
| "<b>Central:</b> %{y:.4f}<br>" | |
| "<b>Max (Upper):</b> %{customdata[0]:.4f}<br>" | |
| "<b>Min (Lower):</b> %{customdata[1]:.4f}" | |
| "<extra></extra>" | |
| ), | |
| )) | |
| default_title = " " #f"Parton Distribution: {get_parton_name(parton_id)} at Q = {q_scale} GeV" | |
| fig.update_layout( | |
| title=custom_title if custom_title else default_title, | |
| xaxis_title="$x$", | |
| yaxis_title=f"$x {get_parton_symbol(parton_id)}(x, Q)$", | |
| template="plotly_white", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| autosize=True, | |
| legend=dict(x=0.02, y=0.98, xanchor="left", yanchor="top", bgcolor="rgba(255,255,255,0.8)"), | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes( | |
| type="log", | |
| exponentformat="power", | |
| showline=True, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| y_upper_limit = max(upper_band) * 1.30 | |
| y_lower_limit = min(lower_band) * (1 - 0.20) | |
| fig.update_yaxes( | |
| range=[y_lower_limit, y_upper_limit], | |
| showline=True, | |
| zeroline=False, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| return fig | |
| def build_standard_ratio_plot( | |
| pdf_set, | |
| pdfs, | |
| grid_name: str, | |
| q_scale: float, | |
| parton_id: int, | |
| plot_color: Optional[str] = None, | |
| x_vals=None, | |
| custom_title: Optional[str] = None, | |
| ) -> go.Figure: | |
| """ | |
| Same as build_standard_plot, but normalized to the central value at | |
| each x (i.e. central line sits at 1.0, band shows relative uncertainty). | |
| `pdfs` is `pdf_set.mkPDFs()`, passed in already-built — see | |
| services.pdf_utils.get_pdf_members. | |
| """ | |
| plot_color = plot_color or get_parton_color(parton_id) | |
| if x_vals is None: | |
| x_vals = np.logspace(-5, -0.01, 150) | |
| logger.info( | |
| "build_standard_ratio_plot: grid=%s parton_id=%s q_scale=%s n_points=%d members=%d", | |
| grid_name, parton_id, q_scale, len(x_vals), len(pdfs), | |
| ) | |
| t0 = time.monotonic() | |
| central_values, upper_band, lower_band = [], [], [] | |
| for x in x_vals: | |
| vals = [pdf.xfxQ(parton_id, x, q_scale) for pdf in pdfs] | |
| uncertainty = pdf_set.uncertainty(vals) | |
| central1 = uncertainty.central | |
| if abs(central1) < 1e-10: | |
| # Treat a (numerically) zero central value as a flat, uninformative ratio. | |
| central_values.append(1.0) | |
| upper_band.append(1.0) | |
| lower_band.append(1.0) | |
| else: | |
| central_values.append(round(central1 / central1, 6)) | |
| upper_band.append(round(1.0 + uncertainty.errplus / central1, 6)) | |
| lower_band.append(round(1.0 - uncertainty.errminus / central1, 6)) | |
| logger.info("build_standard_ratio_plot: evaluated %d points in %.2fs", len(x_vals), time.monotonic() - t0) | |
| fig = go.Figure() | |
| x_vals_rounded = np.round(x_vals, 6) | |
| x_band = np.concatenate([x_vals_rounded, x_vals_rounded[::-1]]) | |
| y_band = np.concatenate([upper_band, lower_band[::-1]]) | |
| fig.add_trace(go.Scatter( | |
| x=x_band, | |
| y=y_band, | |
| fill="toself", | |
| fillcolor=plot_color, | |
| opacity=0.3, | |
| line=dict(color="rgba(255,255,255,0)"), | |
| name="Uncertainty", | |
| hoverinfo="skip", | |
| )) | |
| hover_custom_data = np.stack((upper_band, lower_band), axis=-1) | |
| fig.add_trace(go.Scatter( | |
| x=x_vals, | |
| y=central_values, | |
| mode="lines", | |
| line=dict(color=plot_color, width=2), | |
| name=f"{grid_name} Central <br>Q={q_scale} GeV", | |
| customdata=hover_custom_data, | |
| hovertemplate=( | |
| "<b>x:</b> %{x:.4e}<br>" | |
| "<b>Central:</b> %{y:.4f}<br>" | |
| "<b>Max (Upper):</b> %{customdata[0]:.4f}<br>" | |
| "<b>Min (Lower):</b> %{customdata[1]:.4f}" | |
| "<extra></extra>" | |
| ), | |
| )) | |
| default_title = " " #f"Parton Distribution: {get_parton_name(parton_id)} at Q = {q_scale} GeV" | |
| fig.update_layout( | |
| title=custom_title if custom_title else default_title, | |
| xaxis_title="$x$", | |
| yaxis_title=f"$x {get_parton_symbol(parton_id)}(x, Q)/central$", | |
| template="plotly_white", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| autosize=True, | |
| legend=dict(x=0.02, y=0.98, xanchor="left", yanchor="top", bgcolor="rgba(255,255,255,0.8)"), | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes( | |
| type="log", | |
| exponentformat="power", | |
| showline=True, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| y_upper_limit = max(upper_band) * 1.30 | |
| ymin_data = min(lower_band) | |
| y_lower_limit = ymin_data * (1 - 0.30) if ymin_data >= 0 else ymin_data * (1 + 0.30) | |
| fig.update_yaxes( | |
| range=[y_lower_limit, y_upper_limit], | |
| showline=True, | |
| zeroline=False, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| return fig | |
| def build_correlation_plot( | |
| pdfs, | |
| q_scale: float, | |
| parton_id_1: int, | |
| parton_id_2: int, | |
| x_vals=None, | |
| color_correlated: Optional[str] = None, | |
| color_anti_correlated: str = "black", | |
| ) -> go.Figure: | |
| """ | |
| Hessian-style correlation contour between two flavors across an | |
| x-grid, at a fixed Q scale. | |
| `pdfs` is `pdf_set.mkPDFs()`, passed in already-built — see | |
| services.pdf_utils.get_pdf_members. | |
| """ | |
| color_correlated = color_correlated or get_parton_color(parton_id_1) | |
| num_members = len(pdfs) | |
| if x_vals is None: | |
| x_vals = np.logspace(-4, -0.01, 35) | |
| x_vals_rounded = np.round(x_vals, 6) | |
| num_x = len(x_vals_rounded) | |
| logger.info( | |
| "build_correlation_plot: parton_id_1=%s parton_id_2=%s q_scale=%s n_points=%d members=%d", | |
| parton_id_1, parton_id_2, q_scale, num_x, num_members, | |
| ) | |
| t0 = time.monotonic() | |
| matrix_p1 = np.zeros((num_members, num_x)) | |
| matrix_p2 = np.zeros((num_members, num_x)) | |
| for m_idx, pdf in enumerate(pdfs): | |
| for x_idx, x in enumerate(x_vals_rounded): | |
| matrix_p1[m_idx, x_idx] = pdf.xfxQ(parton_id_1, x, q_scale) | |
| matrix_p2[m_idx, x_idx] = pdf.xfxQ(parton_id_2, x, q_scale) | |
| logger.info("build_correlation_plot: evaluated %d members x %d points in %.2fs", num_members, num_x, time.monotonic() - t0) | |
| correlation_matrix = np.zeros((num_x, num_x)) | |
| p1_central = matrix_p1[0, :] | |
| p2_central = matrix_p2[0, :] | |
| p1_dev = matrix_p1[1:, :] - p1_central | |
| p2_dev = matrix_p2[1:, :] - p2_central | |
| for i in range(num_x): | |
| for j in range(num_x): | |
| delta_x = p1_dev[:, i] | |
| delta_y = p2_dev[:, j] | |
| numerator = np.sum(delta_x * delta_y) | |
| sum_x_sq = np.sum(delta_x ** 2) | |
| sum_y_sq = np.sum(delta_y ** 2) | |
| denominator = np.sqrt(sum_x_sq * sum_y_sq) | |
| if denominator > 1e-10: | |
| corr_value = np.clip(numerator / denominator, -1.0, 1.0) | |
| correlation_matrix[j, i] = round(corr_value, 4) | |
| else: | |
| correlation_matrix[j, i] = 0.0 | |
| name_1 = get_parton_symbol(parton_id_1) | |
| name_2 = get_parton_symbol(parton_id_2) | |
| fig = go.Figure(data=go.Contour( | |
| z=correlation_matrix, | |
| x=x_vals_rounded, | |
| y=x_vals_rounded, | |
| colorscale=[ | |
| [0.0, color_anti_correlated], | |
| [0.5, "rgb(245, 245, 245)"], | |
| [1.0, color_correlated], | |
| ], | |
| zmin=-1.0, | |
| zmax=1.0, | |
| line=dict(width=1, color="rgba(0, 0, 0, 0.2)"), | |
| contours=dict(coloring="heatmap", showlines=False), | |
| ncontours=15, | |
| colorbar=dict( | |
| title=dict(text="Corr. Coefficient", side="right"), | |
| thickness=15, | |
| len=1.0, | |
| ), | |
| hovertemplate=( | |
| f"<b>x₁ ({name_1}):</b> %{{x:.4e}}<br>" | |
| f"<b>x₂ ({name_2}):</b> %{{y:.4e}}<br>" | |
| "<b>Correlation:</b> %{z:.4f}<extra></extra>" | |
| ), | |
| )) | |
| fig.update_layout( | |
| title=" ", | |
| template="plotly_white", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| margin=dict(l=70, r=20, t=70, b=70), | |
| autosize=True, | |
| ) | |
| fig.update_xaxes( | |
| title_text=f"$x_1 \\text{{ in }} {name_1}(x_1, Q)$", | |
| type="log", | |
| exponentformat="power", | |
| showline=True, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| fig.update_yaxes( | |
| title_text=f"$x_2 \\text{{ in }} {name_2}(x_2, Q)$", | |
| type="log", | |
| exponentformat="power", | |
| showline=True, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| return fig | |
| def build_main_plot( | |
| pdf_set, | |
| pdfs, | |
| grid_name: str, | |
| q_scale: float, | |
| parton_ids: list, | |
| x_vals=None, | |
| custom_title: Optional[str] = None, | |
| ) -> go.Figure: | |
| """ | |
| The classic multi-flavor "overview" plot (CT18 Fig. 2 style, see | |
| arXiv:1912.10053): every flavor in parton_ids overlaid on one figure as | |
| central value + 90% C.L. uncertainty band, log-x / linear-y. The gluon | |
| (parton_id 21) is always scaled down by a fixed factor of 5 so it fits | |
| on the same y-scale as the quark flavors, following that same | |
| convention -- its legend entry is labelled "g/5" accordingly (see | |
| services.colors.get_main_plot_label). | |
| `pdfs` is `pdf_set.mkPDFs()`, passed in already-built (see | |
| services.pdf_utils.get_pdf_members). | |
| """ | |
| if x_vals is None: | |
| x_vals = np.logspace(-6, np.log10(0.9), 500) | |
| logger.info( | |
| "build_main_plot: grid=%s parton_ids=%s q_scale=%s n_points=%d members=%d", | |
| grid_name, parton_ids, q_scale, len(x_vals), len(pdfs), | |
| ) | |
| t0 = time.monotonic() | |
| fig = go.Figure() | |
| x_vals_rounded = np.round(x_vals, 6) | |
| x_band = np.concatenate([x_vals_rounded, x_vals_rounded[::-1]]) | |
| all_upper_bands = [] | |
| for parton_id in parton_ids: | |
| plot_color = get_parton_color(parton_id) | |
| label = get_main_plot_label(parton_id) | |
| scale = 1.0 / 5.0 if parton_id == 21 else 1.0 | |
| central_values, upper_band, lower_band = [], [], [] | |
| for x in x_vals: | |
| vals = [pdf.xfxQ(parton_id, x, q_scale) for pdf in pdfs] | |
| uncertainty = pdf_set.uncertainty(vals) | |
| central = uncertainty.central * scale | |
| central_values.append(round(central, 6)) | |
| upper_band.append(round(central + uncertainty.errplus * scale, 6)) | |
| lower_band.append(round(central - uncertainty.errminus * scale, 6)) | |
| all_upper_bands.extend(upper_band) | |
| y_band = np.concatenate([upper_band, lower_band[::-1]]) | |
| fig.add_trace(go.Scatter( | |
| x=x_band, | |
| y=y_band, | |
| fill="toself", | |
| fillcolor=plot_color, | |
| opacity=0.3, | |
| line=dict(color="rgba(255,255,255,0)"), | |
| name=label + " (unc.)", | |
| legendgroup=label, | |
| showlegend=False, | |
| hoverinfo="skip", | |
| )) | |
| hover_custom_data = np.stack((upper_band, lower_band), axis=-1) | |
| fig.add_trace(go.Scatter( | |
| x=x_vals, | |
| y=central_values, | |
| mode="lines", | |
| line=dict(color=plot_color, width=2), | |
| name=f"${label}$", | |
| legendgroup=label, | |
| customdata=hover_custom_data, | |
| hovertemplate=( | |
| f"<b>Flavor:</b> {label}<br>" | |
| "<b>x:</b> %{x:.4e}<br>" | |
| "<b>Central:</b> %{y:.4f}<br>" | |
| "<b>Max (Upper):</b> %{customdata[0]:.4f}<br>" | |
| "<b>Min (Lower):</b> %{customdata[1]:.4f}" | |
| "<extra></extra>" | |
| ), | |
| )) | |
| logger.info( | |
| "build_main_plot: evaluated %d flavors x %d points in %.2fs", | |
| len(parton_ids), len(x_vals), time.monotonic() - t0, | |
| ) | |
| default_title = " " | |
| fig.update_layout( | |
| title=custom_title if custom_title else default_title, | |
| xaxis_title="$x$", | |
| yaxis_title="$x f(x, Q)$", | |
| template="plotly_white", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| autosize=True, | |
| legend=dict(x=0.98, y=0.98, xanchor="right", yanchor="top", bgcolor="rgba(255,255,255,0.8)"), | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes( | |
| type="log", | |
| exponentformat="power", | |
| showline=True, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| y_upper_limit = max(all_upper_bands) * 1.15 if all_upper_bands else 1.0 | |
| fig.update_yaxes( | |
| range=[0, y_upper_limit], | |
| showline=True, | |
| zeroline=False, | |
| linewidth=1.5, | |
| linecolor="black", | |
| mirror="allticks", | |
| ticks="inside", | |
| tickwidth=1.5, | |
| tickcolor="black", | |
| showgrid=False, | |
| ) | |
| return fig | |