"""rendering logic for PHANTOM environment dashboard""" import numpy as np import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec def style_axis(ax, title: str = None, xlabel: str = None, ylabel: str = None): ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) if title: ax.set_title(title, fontsize=11, fontweight="bold", pad=8) if xlabel: ax.set_xlabel(xlabel, fontsize=9) if ylabel: ax.set_ylabel(ylabel, fontsize=9) class DashboardRenderer: """stateful renderer for PHANTOM market dynamics visualization""" def __init__(self): self.fig = None self.gs = None def render(self, env) -> None: if self.fig is None: plt.ion() self.fig = plt.figure(figsize=(14, 10)) self.gs = GridSpec( 3, 3, figure=self.fig, hspace=0.35, wspace=0.3, left=0.07, right=0.95, top=0.92, bottom=0.08, ) plt.show(block=False) self.fig.clear() self.fig.suptitle( f"PHANTOM Market Dynamics [t={env._step_count}, a={env.alpha:.2f}]", fontsize=14, fontweight="bold", ) demand_mat = np.array(env._demand_history).T price_mat = np.array(env._price_history).T elasticity = env._compute_elasticity() self._render_scatter(env) self._render_elasticity_bar(env, elasticity) self._render_session_pie(env) self._render_price_heatmap(price_mat) self._render_demand_heatmap(demand_mat) self._render_correlation(env.n_products, price_mat, demand_mat) self._render_revenue(env) self.fig.canvas.draw_idle() self.fig.canvas.flush_events() def _render_scatter(self, env): ax = self.fig.add_subplot(self.gs[0, 0]) prices_flat = np.array(env._price_history).flatten() demands_flat = np.array(env._demand_history).flatten() product_ids = np.tile(np.arange(env.n_products), len(env._price_history)) ax.scatter( prices_flat, demands_flat, c=product_ids, cmap="plasma", alpha=0.6, s=15, edgecolors="none", ) if len(prices_flat) > 1: z = np.polyfit(prices_flat, demands_flat, 1) p_line = np.linspace(prices_flat.min(), prices_flat.max(), 50) ax.plot(p_line, np.polyval(z, p_line), "--", lw=1.5, alpha=0.8) style_axis(ax, "Price-Demand Relationship", "Price ($)", "Demand") def _render_elasticity_bar(self, env, elasticity): ax = self.fig.add_subplot(self.gs[0, 1]) ax.barh(range(env.n_products), elasticity, alpha=0.8) ax.axvline(0, lw=0.8, alpha=0.5) ax.axvline(-1, lw=1, ls="--", alpha=0.5) ax.set_yticks(range(env.n_products)) ax.set_yticklabels([f"P{i}" for i in range(env.n_products)], fontsize=7) style_axis(ax, "Price Elasticity", "(dQ/dP)(P/Q)", None) def _render_session_pie(self, env): ax = self.fig.add_subplot(self.gs[0, 2]) n_h, n_a = env.market.Nhumans, env.market.Nagents wedges, _ = ax.pie( [n_h, n_a], startangle=90, wedgeprops={"linewidth": 2, "edgecolor": "white"} ) ax.legend( wedges, [f"H ({n_h})", f"A ({n_a})"], loc="lower center", fontsize=8, frameon=False, bbox_to_anchor=(0.5, -0.05), ) ax.set_title("Session Mix", fontsize=11, fontweight="bold") def _render_price_heatmap(self, price_mat): ax = self.fig.add_subplot(self.gs[1, :2]) im = ax.imshow(price_mat, aspect="auto", cmap="viridis", origin="lower") style_axis(ax, "Price Heatmap P(product, t)", "Step", "Product") cbar = self.fig.colorbar(im, ax=ax, fraction=0.03, pad=0.02) cbar.set_label("$", fontsize=8) def _render_demand_heatmap(self, demand_mat): ax = self.fig.add_subplot(self.gs[1, 2]) im = ax.imshow(demand_mat, aspect="auto", cmap="Blues", origin="lower") style_axis(ax, "Demand Q(product, t)", "Step", None) self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02) def _render_correlation(self, n_products, price_mat, demand_mat): ax = self.fig.add_subplot(self.gs[2, 0]) if price_mat.shape[1] > 2: corr = np.corrcoef(price_mat, demand_mat)[:n_products, n_products:] im = ax.imshow(corr, cmap="RdBu", vmin=-1, vmax=1, aspect="auto") ax.set_xticks(range(n_products)) ax.set_yticks(range(n_products)) ax.set_xticklabels([f"Q{i}" for i in range(n_products)], fontsize=6) ax.set_yticklabels([f"P{i}" for i in range(n_products)], fontsize=6) self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02) style_axis(ax, "Price-Demand Correlation", None, None) def _render_revenue(self, env): ax = self.fig.add_subplot(self.gs[2, 1:]) n_steps = len(env._revenue_history) demand_std = [np.std(d) for d in env._demand_history] ax.fill_between(range(n_steps), env._revenue_history, alpha=0.3) ax.plot(env._revenue_history, linewidth=2, label="Revenue") ax.set_xlim(0, max(n_steps, 1)) ax.set_ylim(0, max(env._revenue_history) * 1.1 if env._revenue_history else 1) ax2 = ax.twinx() ax2.plot( range(n_steps), demand_std, linewidth=2, ls="-", alpha=0.9, label="sigma(Demand)", ) d_min, d_max = min(demand_std), max(demand_std) margin = (d_max - d_min) * 0.2 if d_max > d_min else 0.5 ax2.set_ylim(max(0, d_min - margin), d_max + margin) ax2.set_ylabel("Demand sigma", fontsize=9) style_axis(ax, "Revenue & Demand Dispersion", "Step", "Revenue ($)") ax.legend(loc="upper left", fontsize=7, frameon=False) ax2.legend(loc="upper right", fontsize=7, frameon=False) def close(self): if self.fig: plt.close(self.fig) self.fig = None