Skip to content

Plotting Results

Overview

pySPAC integrates with matplotlib for visualization. Use generateModel() to create smooth model curves for plotting.

Basic Phase Curve Plot

Simple Plot

import numpy as np
import matplotlib.pyplot as plt

# After fitting a model
pc.fitModel(model="HG", method="trust-constr")

# Plot observational data
plt.errorbar(pc.angle, pc.magnitude, yerr=pc.magnitude_unc,
            fmt='o', capsize=5, label='Observations')

# Generate and plot model curve
model_angles = np.linspace(0, 30, 200)
model_mags = pc.generateModel(model="HG", degrees=model_angles)
plt.plot(model_angles, model_mags, 'r-', label='HG Model')

# Format plot
plt.gca().invert_yaxis()  # Astronomy convention: dimmer is up
plt.xlabel('Phase Angle (degrees)')
plt.ylabel('Reduced Magnitude')
plt.title('44 Nysa')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Simple Plot

Enhanced Plot

def plot_phase_curve(pc, model_name=None, title="Phase Curve"):
    """Create enhanced phase curve plot."""

    if model_name is None:
        model_name = pc.fitting_model

    fig, ax = plt.subplots(figsize=(8, 4))

    # Plot data with error bars
    ax.errorbar(pc.angle, pc.magnitude, yerr=pc.magnitude_unc,
               fmt='o', capsize=4, markersize=8, color='darkblue',
               label='Photometric Data', alpha=0.8)

    # Generate smooth model curve
    angle_range = np.linspace(max(0, np.min(pc.angle)-2),
                             np.max(pc.angle)+2, 300)
    model_curve = pc.generateModel(model=model_name, degrees=angle_range)

    ax.plot(angle_range, model_curve, 'r-', linewidth=2,
           label=f'{model_name} Model')

    # Add parameter text
    if pc.params:
        param_text = []
        for param, value in pc.params.items():
            if not param.lower().startswith('constraint'):
                param_text.append(f'{param} = {value:.3f}')

        textstr = '\n'.join(param_text)
        props = dict(boxstyle='round', facecolor='white', alpha=1)
        ax.text(0.03, 0.15, textstr, transform=ax.transAxes,
               verticalalignment='top', bbox=props)

    # Formatting
    ax.invert_yaxis()
    ax.set_xlabel('Phase Angle (degrees)', fontsize=12)
    ax.set_ylabel('Reduced Magnitude', fontsize=12)
    ax.set_title(title, fontsize=14)
    ax.legend(fontsize=11)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# Usage
plot_phase_curve(pc, title="44 Nysa Phase Curve")

Enhanced Plot

Residual Plots

Basic Residuals

def plot_residuals(pc):
    """Plot fit residuals."""

    if pc.fit_residual is None:
        print("No residuals available - fit a model first")
        return

    residuals = np.array(pc.fit_residual)

    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4))

    # Residuals vs phase angle
    ax1.plot(pc.angle, residuals, 'o-', markersize=6)
    ax1.axhline(y=0, color='red', linestyle='--', alpha=0.7)

    # Add RMS line
    rms = np.sqrt(np.mean(residuals**2))
    ax1.axhline(y=rms, color='orange', linestyle=':', alpha=0.7, label=f'RMS = {rms:.4f}')
    ax1.axhline(y=-rms, color='orange', linestyle=':', alpha=0.7)

    ax1.set_xlabel('Phase Angle (degrees)')
    ax1.set_ylabel('Residual (mag)')
    ax1.set_title('Residuals vs Phase Angle')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    # Residual histogram
    ax2.hist(residuals, bins=max(3, len(residuals)//3), alpha=0.7)
    ax2.axvline(x=0, color='red', linestyle='--', alpha=0.7)
    ax2.set_xlabel('Residual (mag)')
    ax2.set_ylabel('Count')
    ax2.set_title('Residual Distribution')
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# Usage
plot_residuals(pc)

Residuals Plot

Model Comparisons

Multiple Models

def plot_model_comparison(pc, models=["HG", "HG12", "LINEAR"]):
    """Compare multiple models on same data."""

    fig, ax = plt.subplots(figsize=(8, 4))

    # Plot data
    ax.errorbar(pc.angle, pc.magnitude, yerr=pc.magnitude_unc,
               fmt='o', capsize=5, markersize=8, color='black',
               label='Observations', zorder=10)

    # Generate angle range for smooth curves
    angle_range = np.linspace(max(0, np.min(pc.angle)-2),
                             np.max(pc.angle)+2, 200)

    colors = ['red', 'blue', 'green', 'orange', 'purple']

    # Fit and plot each model
    for i, model in enumerate(models):
        try:
            # Fit model
            method = "trust-constr" if model != "LINEAR" else "leastsq"
            pc.fitModel(model=model, method=method)

            # Generate curve
            model_curve = pc.generateModel(model=model, degrees=angle_range)

            # Calculate RMS for legend
            rms = np.sqrt(np.mean(np.array(pc.fit_residual)**2))

            # Plot
            color = colors[i % len(colors)]
            ax.plot(angle_range, model_curve, '-', linewidth=2, color=color,
                   label=f'{model} (RMS: {rms:.4f})')

        except Exception as e:
            print(f"Failed to fit {model}: {e}")

    ax.invert_yaxis()
    ax.set_xlabel('Phase Angle (degrees)', fontsize=12)
    ax.set_ylabel('Reduced Magnitude', fontsize=12)
    ax.set_title('Model Comparison', fontsize=14)
    ax.legend(fontsize=11)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# Usage
plot_model_comparison(pc, models=["HG", "HG12", "LINEAR"])

Multiple Models

Uncertainty Visualization

Parameter Distributions

def plot_parameter_distributions(pc):
    """Plot Monte Carlo parameter distributions."""

    if pc.montecarlo_uncertainty is None:
        print("Run Monte Carlo analysis first")
        return

    mc_data = pc.montecarlo_uncertainty
    n_params = len(mc_data)

    fig, axes = plt.subplots(1, n_params, figsize=(8,4))
    if n_params == 1:
        axes = [axes]

    for i, (param, samples) in enumerate(mc_data.items()):
        ax = axes[i]

        # Histogram
        ax.hist(samples, bins=50, alpha=0.7, density=True, color='skyblue')

        # Add percentile lines
        median = np.percentile(samples, 50)
        lower = np.percentile(samples, 15.87)
        upper = np.percentile(samples, 84.13)

        ax.axvline(median, color='red', linewidth=2, label=f'Median: {median:.4f}')
        ax.axvline(lower, color='orange', linestyle='--', alpha=0.7)
        ax.axvline(upper, color='orange', linestyle='--', alpha=0.7)

        # Shaded region
        ax.fill_betweenx([0, ax.get_ylim()[1]], lower, upper,
                        alpha=0.2, color='orange', label='68% CI')

        ax.set_xlabel(param)
        ax.set_ylabel('Probability Density')
        ax.set_title(f'{param} Distribution')
        ax.legend(fontsize="small")
        ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# Usage after Monte Carlo
pc.monteCarloUncertainty(n_simulations=50000, model="HG", method="trust-constr")
plot_parameter_distributions(pc)

Parameter Distributions

Confidence Bands

def plot_confidence_bands(pc, model_name=None, n_curves=500):
    """Plot phase curve with confidence bands."""

    if pc.montecarlo_uncertainty is None:
        print("Run Monte Carlo analysis first")
        return

    if model_name is None:
        model_name = pc.fitting_model

    # Generate angle range
    angle_range = np.linspace(max(0, np.min(pc.angle)-2),
                             np.max(pc.angle)+2, 200)

    # Generate curves from Monte Carlo samples
    mc_samples = pc.montecarlo_uncertainty
    param_names = list(mc_samples.keys())
    n_samples = len(mc_samples[param_names[0]])

    curves = []
    for i in range(min(n_curves, n_samples)):
        # Create parameter set for this sample
        temp_params = {param: samples[i] for param, samples in mc_samples.items()}

        # Generate curve
        try:
            temp_pc = PhaseCurve(angle=angle_range, params=temp_params)
            curve = temp_pc.generateModel(model=model_name, degrees=angle_range)
            curves.append(curve)
        except:
            continue

    if not curves:
        print("Could not generate confidence bands")
        return

    curves = np.array(curves)

    # Calculate percentiles
    fig, ax = plt.subplots(figsize=(10, 6))

    # 68% confidence band
    lower_68 = np.percentile(curves, 15.87, axis=0)
    upper_68 = np.percentile(curves, 84.13, axis=0)
    ax.fill_between(angle_range, lower_68, upper_68,
                   alpha=0.3, color='lightblue', label='68% Confidence')

    # 95% confidence band
    lower_95 = np.percentile(curves, 2.5, axis=0)
    upper_95 = np.percentile(curves, 97.5, axis=0)
    ax.fill_between(angle_range, lower_95, upper_95,
                   alpha=0.2, color='lightgray', label='95% Confidence')

    # Best fit curve
    best_fit = pc.generateModel(model=model_name, degrees=angle_range)
    ax.plot(angle_range, best_fit, 'r-', linewidth=2, label='Best Fit')

    # Data points
    ax.errorbar(pc.angle, pc.magnitude, yerr=pc.magnitude_unc,
               fmt='o', capsize=5, markersize=8, color='darkblue',
               label='Data', zorder=10)

    ax.invert_yaxis()
    ax.set_xlabel('Phase Angle (degrees)')
    ax.set_ylabel('Reduced Magnitude')
    ax.set_title('Phase Curve with Confidence Bands')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# Usage
plot_confidence_bands(pc)

Confidence Bands

Next Steps