mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
127 lines
5.7 KiB
Python
127 lines
5.7 KiB
Python
"""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
|