waterrocketpy.visualization.parameter_explorer¶
Water Rocket Parameter Exploration Tool
This script provides comprehensive parameter exploration capabilities for water rocket simulations. Features: - Parameter sweeping with configurable ranges - Multi-parameter analysis with 2D plotting - Sensitivity analysis (derivatives) - Extensible design for adding new parameters and targets - Robust error handling and progress tracking
ExplorationResult
dataclass
¶
Results from parameter exploration.
Source code in waterrocketpy/visualization/parameter_explorer.py
@dataclass
class ExplorationResult:
"""Results from parameter exploration."""
parameter_names: List[str]
parameter_values: Dict[str, np.ndarray]
target_values: np.ndarray
target_name: str
target_unit: str
base_target_value: float
sensitivity_analysis: Dict[str, float]
ParameterConfig
dataclass
¶
Configuration for a parameter to explore.
Source code in waterrocketpy/visualization/parameter_explorer.py
@dataclass
class ParameterConfig:
"""Configuration for a parameter to explore."""
name: str
base_value: float
min_factor: float = 0.5 # minimum as factor of base value
max_factor: float = 2.0 # maximum as factor of base value
num_points: int = 10 # number of points to sample
unit: str = "" # unit for display
@property
def min_value(self) -> float:
return self.base_value * self.min_factor
@property
def max_value(self) -> float:
return self.base_value * self.max_factor
@property
def values(self) -> np.ndarray:
return np.linspace(self.min_value, self.max_value, self.num_points)
ParameterExplorer
¶
Main class for water rocket parameter exploration.
Source code in waterrocketpy/visualization/parameter_explorer.py
class ParameterExplorer:
"""Main class for water rocket parameter exploration."""
def __init__(self,verbose: bool = False):
self.verbose = verbose # Enable verbose output for debugging
self.simulator = WaterRocketSimulator(verbose=self.verbose)
# Define available target extractors
self.target_extractors = {
"apogee": ("max_altitude", "m", "Maximum Altitude"),
"max_velocity": ("max_velocity", "m/s", "Maximum Velocity"),
"flight_time": ("flight_time", "s", "Flight Time"),
"water_depletion_time": (
"water_depletion_time",
"s",
"Water Depletion Time",
),
"air_depletion_time": (
"air_depletion_time",
"s",
"Air Depletion Time",
),
}
# Define parameter mappings - maps explorer parameter names to simulation parameter keys
# Format: 'explorer_name': ('sim_param_key', 'display_name', 'unit',
# default_range)
self.parameter_mappings = {
# Pressure and initial conditions
"initial_pressure": (
"P0",
"Initial Pressure",
"Pa",
{"min_factor": 0.3, "max_factor": 3.0},
),
"pressure": (
"P0",
"Initial Pressure",
"Pa",
{"min_factor": 0.3, "max_factor": 3.0},
), # alias
# Water and mass parameters
"water_fraction": (
"water_fraction",
"Water Fraction",
"-",
{"min_factor": 0.3, "max_factor": 2.5},
),
# Nozzle parameters
"nozzle_area": (
"A_nozzle",
"Nozzle Area",
"m²",
{"min_factor": 0.4, "max_factor": 3.0},
),
"nozzle_diameter": (
"A_nozzle",
"Nozzle Diameter",
"m",
{
"min_factor": 0.5,
"max_factor": 2.5,
"convert_func": self._diameter_to_area,
},
),
"nozzle_discharge_coefficient": (
"C_d",
"Nozzle Discharge Coefficient",
"-",
{"min_factor": 0.5, "max_factor": 1.5},
),
# Bottle parameters
"bottle_volume": (
"V_bottle",
"Bottle Volume",
"m³",
{"min_factor": 0.5, "max_factor": 2.0},
),
# Mass parameters
"empty_mass": (
"m_empty",
"Empty Mass",
"kg",
{"min_factor": 0.5, "max_factor": 2.0},
),
# Aerodynamic parameters
"drag_coefficient": (
"C_drag",
"Drag Coefficient",
"-",
{"min_factor": 0.3, "max_factor": 3.0},
),
"reference_area": (
"A_rocket",
"Reference Area",
"m²",
{"min_factor": 0.5, "max_factor": 2.0},
),
"rocket_diameter": (
"A_rocket",
"Rocket Diameter",
"m",
{
"min_factor": 0.7,
"max_factor": 1.5,
"convert_func": self._diameter_to_area,
},
),
# Liquid gas
"liquid_gas_mass": (
"liquid_gas_mass",
"Liquid Gas Mass",
"kg",
{"min_factor": 0.0, "max_factor": 10.0},
),
}
def _diameter_to_area(self, diameter: float) -> float:
"""Convert diameter to circular area."""
return np.pi * (diameter / 2) ** 2
def _update_parameter(
self, params: Dict[str, Any], explorer_param_name: str, value: float
):
"""Generic parameter updater using the parameter mappings."""
if explorer_param_name not in self.parameter_mappings:
raise ValueError(f"Unknown parameter: {explorer_param_name}")
mapping = self.parameter_mappings[explorer_param_name]
sim_param_key = mapping[0]
# Apply conversion function if specified
if len(mapping) > 4 and "convert_func" in mapping[4]:
convert_func = mapping[4]["convert_func"]
converted_value = convert_func(value)
else:
converted_value = value
# Apply parameter-specific constraints
if explorer_param_name == "water_fraction":
converted_value = np.clip(converted_value, 0.01, 0.99)
elif "area" in explorer_param_name.lower():
converted_value = max(
converted_value, 1e-6
) # Prevent zero/negative areas
elif "mass" in explorer_param_name.lower():
converted_value = max(
converted_value, 0.0
) # Prevent negative mass
elif "pressure" in explorer_param_name.lower():
converted_value = max(
converted_value, ATMOSPHERIC_PRESSURE
) # Minimum atmospheric pressure
# Debug output
old_value = params.get(sim_param_key, "NOT FOUND")
if(self.verbose):
print(f"Debug - Updating {explorer_param_name} -> {sim_param_key}")
print(f"Debug - Old value: {old_value}")
print(f"Debug - New value: {converted_value}")
params[sim_param_key] = converted_value
def extract_base_parameters(self, rocket) -> Dict[str, float]:
"""Extract base parameter values from a rocket configuration."""
# Convert rocket to simulation parameters to get the base values
builder = RocketBuilder.from_dict(rocket.__dict__)
sim_params = builder.to_simulation_params()
base_params = {}
print("Debug - Available simulation parameters:")
for key, value in sim_params.items():
print(f" {key}: {value}")
# Extract parameters based on our mappings
for (
explorer_param_name,
mapping_info,
) in self.parameter_mappings.items():
sim_param_key = mapping_info[0]
if sim_param_key in sim_params:
value = sim_params[sim_param_key]
# Convert area back to diameter if needed
if (
"diameter" in explorer_param_name
and "area" in sim_param_key.lower()
):
# Convert area to diameter: A = π(d/2)², so d = 2√(A/π)
value = 2 * np.sqrt(value / np.pi)
base_params[explorer_param_name] = value
print("\nDebug - Extracted base parameters:")
for key, value in base_params.items():
print(f" {key}: {value}")
return base_params
def create_parameter_configs(
self,
base_params: Dict[str, float],
parameter_names: List[str],
custom_ranges: Dict[str, Dict] = None,
) -> Dict[str, ParameterConfig]:
"""Create parameter configurations for exploration."""
configs = {}
for param_name in parameter_names:
if param_name not in base_params:
raise ValueError(
f"Parameter '{param_name}' not found in base parameters. Available: {list(base_params.keys())}"
)
if param_name not in self.parameter_mappings:
raise ValueError(
f"Parameter '{param_name}' not defined in parameter mappings. Available: {list(self.parameter_mappings.keys())}"
)
# Get mapping info
mapping = self.parameter_mappings[param_name]
sim_param_key, display_name, unit = mapping[:3]
default_range = mapping[4] if len(mapping) > 4 else {}
# Apply custom ranges if provided
if custom_ranges and param_name in custom_ranges:
range_config = {**default_range, **custom_ranges[param_name]}
else:
range_config = default_range
configs[param_name] = ParameterConfig(
name=param_name,
base_value=base_params[param_name],
unit=unit,
**{
k: v
for k, v in range_config.items()
if k != "convert_func"
},
)
return configs
def simulate_single_point(
self,
base_rocket,
param_values: Dict[str, float],
sim_settings: Dict[str, Any] = None,
) -> Optional[Any]:
"""Simulate a single parameter point."""
try:
# Create a copy of the base rocket parameters
builder = RocketBuilder.from_dict(base_rocket.__dict__)
sim_params = builder.to_simulation_params()
if(self.verbose):
print(f"Debug - Simulating with param_values: {param_values}")
print(
f"Debug - Original sim_params keys: {list(sim_params.keys())}"
)
# Update parameters using the generic updater
for param_name, value in param_values.items():
self._update_parameter(sim_params, param_name, value)
if(self.verbose):
print(
f"Debug - Updated sim_params keys: {list(sim_params.keys())}"
)
# Default simulation settings
if sim_settings is None:
sim_settings = {
"max_time": 15.0,
"time_step": 0.01,
"solver": "RK45",
}
# Run simulation
flight_data = self.simulator.simulate(sim_params, sim_settings)
return flight_data
except Exception as e:
warnings.warn(
f"Simulation failed for parameters {param_values}: {e}"
)
print(f"Debug - Exception details: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return None
def explore_single_parameter(
self,
base_rocket,
param_config: ParameterConfig,
target: str = "apogee",
sim_settings: Dict[str, Any] = None,
) -> ExplorationResult:
"""Explore a single parameter."""
print(f"Exploring parameter: {param_config.name}")
target_attr, target_unit, target_display = self.target_extractors[
target
]
# Get base target value
base_flight_data = self.simulate_single_point(
base_rocket, {}, sim_settings
)
if base_flight_data is None:
raise RuntimeError("Base simulation failed")
base_target_value = getattr(base_flight_data, target_attr)
# Explore parameter range
target_values = []
valid_param_values = []
for param_value in param_config.values:
flight_data = self.simulate_single_point(
base_rocket, {param_config.name: param_value}, sim_settings
)
if flight_data is not None:
target_values.append(getattr(flight_data, target_attr))
valid_param_values.append(param_value)
else:
target_values.append(np.nan)
valid_param_values.append(param_value)
# Calculate sensitivity (numerical derivative at base value)
sensitivity = self._calculate_sensitivity(
base_rocket, param_config, target, base_target_value, sim_settings
)
return ExplorationResult(
parameter_names=[param_config.name],
parameter_values={param_config.name: np.array(valid_param_values)},
target_values=np.array(target_values),
target_name=target_display,
target_unit=target_unit,
base_target_value=base_target_value,
sensitivity_analysis={param_config.name: sensitivity},
)
def explore_multiple_parameters(
self,
base_rocket,
param_configs: Dict[str, ParameterConfig],
target: str = "apogee",
sim_settings: Dict[str, Any] = None,
use_parallel: bool = True,
) -> List[ExplorationResult]:
"""Explore multiple parameters with pairwise combinations."""
print(
f"Exploring {len(param_configs)} parameters: {list(param_configs.keys())}"
)
target_attr, target_unit, target_display = self.target_extractors[
target
]
# Get base target value
base_flight_data = self.simulate_single_point(
base_rocket, {}, sim_settings
)
if base_flight_data is None:
raise RuntimeError("Base simulation failed")
base_target_value = getattr(base_flight_data, target_attr)
results = []
param_names = list(param_configs.keys())
# Generate all pairwise combinations
for param1_name, param2_name in combinations(param_names, 2):
print(f" Exploring pair: {param1_name} vs {param2_name}")
param1_config = param_configs[param1_name]
param2_config = param_configs[param2_name]
# Create parameter grids
p1_values = param1_config.values
p2_values = param2_config.values
P1, P2 = np.meshgrid(p1_values, p2_values)
target_grid = np.full_like(P1, np.nan)
# Simulate all combinations
total_sims = P1.size
completed_sims = 0
for i in range(P1.shape[0]):
for j in range(P1.shape[1]):
param_values = {
param1_name: P1[i, j],
param2_name: P2[i, j],
}
flight_data = self.simulate_single_point(
base_rocket, param_values, sim_settings
)
if flight_data is not None:
target_grid[i, j] = getattr(flight_data, target_attr)
completed_sims += 1
if completed_sims % 10 == 0:
print(
f" Progress: {completed_sims}/{total_sims} ({100*completed_sims/total_sims:.1f}%)"
)
# Calculate sensitivities for both parameters
sensitivity1 = self._calculate_sensitivity(
base_rocket,
param1_config,
target,
base_target_value,
sim_settings,
)
sensitivity2 = self._calculate_sensitivity(
base_rocket,
param2_config,
target,
base_target_value,
sim_settings,
)
results.append(
ExplorationResult(
parameter_names=[param1_name, param2_name],
parameter_values={
param1_name: p1_values,
param2_name: p2_values,
},
target_values=target_grid,
target_name=target_display,
target_unit=target_unit,
base_target_value=base_target_value,
sensitivity_analysis={
param1_name: sensitivity1,
param2_name: sensitivity2,
},
)
)
return results
def _calculate_sensitivity(
self,
base_rocket,
param_config: ParameterConfig,
target: str,
base_target_value: float,
sim_settings: Dict[str, Any] = None,
) -> float:
"""Calculate sensitivity (numerical derivative) of target with respect to parameter."""
target_attr, _, _ = self.target_extractors[target]
# Small perturbation (1% of base value)
delta = param_config.base_value * 0.01
# Simulate with positive perturbation
plus_flight_data = self.simulate_single_point(
base_rocket,
{param_config.name: param_config.base_value + delta},
sim_settings,
)
# Simulate with negative perturbation
minus_flight_data = self.simulate_single_point(
base_rocket,
{param_config.name: param_config.base_value - delta},
sim_settings,
)
if plus_flight_data is not None and minus_flight_data is not None:
plus_value = getattr(plus_flight_data, target_attr)
minus_value = getattr(minus_flight_data, target_attr)
# Central difference
sensitivity = (plus_value - minus_value) / (2 * delta)
else:
# Fallback to one-sided difference
if plus_flight_data is not None:
plus_value = getattr(plus_flight_data, target_attr)
sensitivity = (plus_value - base_target_value) / delta
elif minus_flight_data is not None:
minus_value = getattr(minus_flight_data, target_attr)
sensitivity = (base_target_value - minus_value) / delta
else:
sensitivity = 0.0
return sensitivity
def plot_results(
self, results: List[ExplorationResult], save_plots: bool = False
):
"""Create plots for exploration results."""
for i, result in enumerate(results):
if len(result.parameter_names) == 1:
self._plot_single_parameter(result, save_plots, i)
elif len(result.parameter_names) == 2:
self._plot_two_parameters(result, save_plots, i)
def _plot_single_parameter(
self, result: ExplorationResult, save_plots: bool, plot_idx: int
):
"""Plot results for single parameter exploration."""
param_name = result.parameter_names[0]
param_values = result.parameter_values[param_name]
plt.figure(figsize=(10, 6))
# Remove NaN values for plotting
mask = ~np.isnan(result.target_values)
x_vals = param_values[mask]
y_vals = result.target_values[mask]
plt.plot(x_vals, y_vals, "o-", linewidth=2, markersize=6)
plt.axhline(
y=result.base_target_value,
color="red",
linestyle="--",
alpha=0.7,
label="Base Value",
)
plt.xlabel(f'{param_name.replace("_", " ").title()}')
plt.ylabel(f"{result.target_name} ({result.target_unit})")
plt.title(
f'{result.target_name} vs {param_name.replace("_", " ").title()}'
)
plt.grid(True, alpha=0.3)
plt.legend()
# Add sensitivity annotation
sensitivity = result.sensitivity_analysis[param_name]
plt.text(
0.05,
0.95,
f"Sensitivity: {sensitivity:.2e} {result.target_unit}/unit",
transform=plt.gca().transAxes,
verticalalignment="top",
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
)
plt.tight_layout()
if save_plots:
plt.savefig(
f"parameter_exploration_{plot_idx}_{param_name}.png",
dpi=300,
bbox_inches="tight",
)
plt.show()
def _plot_two_parameters(
self, result: ExplorationResult, save_plots: bool, plot_idx: int
):
"""Plot results for two parameter exploration."""
param1_name, param2_name = result.parameter_names
param1_values = result.parameter_values[param1_name]
param2_values = result.parameter_values[param2_name]
# Create 2D contour plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Contour plot
P1, P2 = np.meshgrid(param1_values, param2_values)
contour = ax1.contour(
P1,
P2,
result.target_values,
levels=15,
colors="black",
alpha=0.5,
linewidths=0.5,
)
contourf = ax1.contourf(
P1, P2, result.target_values, levels=20, cmap="viridis", alpha=0.8
)
ax1.clabel(contour, inline=True, fontsize=8)
cbar1 = plt.colorbar(contourf, ax=ax1)
cbar1.set_label(f"{result.target_name} ({result.target_unit})")
ax1.set_xlabel(f'{param1_name.replace("_", " ").title()}')
ax1.set_ylabel(f'{param2_name.replace("_", " ").title()}')
ax1.set_title(f"{result.target_name} Contour Map")
ax1.grid(True, alpha=0.3)
# 3D surface plot
from mpl_toolkits.mplot3d import Axes3D
ax2 = fig.add_subplot(122, projection="3d")
surface = ax2.plot_surface(
P1, P2, result.target_values, cmap="viridis", alpha=0.8
)
ax2.set_xlabel(f'{param1_name.replace("_", " ").title()}')
ax2.set_ylabel(f'{param2_name.replace("_", " ").title()}')
ax2.set_zlabel(f"{result.target_name} ({result.target_unit})")
ax2.set_title(f"{result.target_name} Surface")
cbar2 = plt.colorbar(surface, ax=ax2, shrink=0.5)
cbar2.set_label(f"{result.target_name} ({result.target_unit})")
plt.tight_layout()
if save_plots:
plt.savefig(
f"parameter_exploration_{plot_idx}_{param1_name}_{param2_name}.png",
dpi=300,
bbox_inches="tight",
)
plt.show()
def get_available_parameters(self) -> Dict[str, str]:
"""Get list of available parameters and their descriptions."""
available = {}
for param_name, mapping in self.parameter_mappings.items():
sim_param_key, display_name, unit = mapping[:3]
available[param_name] = (
f"{display_name} ({unit}) -> {sim_param_key}"
)
return available
def add_parameter_mapping(
self,
explorer_name: str,
sim_param_key: str,
display_name: str,
unit: str,
range_config: Dict = None,
convert_func: Callable = None,
):
"""Add a new parameter mapping for exploration.
Args:
explorer_name: Name used in the explorer (e.g., 'nozzle_diameter')
sim_param_key: Key in simulation parameters (e.g., 'A_nozzle')
display_name: Human-readable name for plots
unit: Unit for display
range_config: Dictionary with 'min_factor', 'max_factor', etc.
convert_func: Optional function to convert explorer value to sim value
"""
mapping = [sim_param_key, display_name, unit]
if range_config or convert_func:
config = range_config or {}
if convert_func:
config["convert_func"] = convert_func
mapping.append(config)
self.parameter_mappings[explorer_name] = tuple(mapping)
def print_sensitivity_analysis(self, results: List[ExplorationResult]):
"""Print sensitivity analysis results."""
print("\n" + "=" * 60)
print("SENSITIVITY ANALYSIS")
print("=" * 60)
all_sensitivities = {}
for result in results:
for param_name, sensitivity in result.sensitivity_analysis.items():
if param_name not in all_sensitivities:
all_sensitivities[param_name] = []
all_sensitivities[param_name].append(abs(sensitivity))
# Average sensitivities and sort by magnitude
avg_sensitivities = {
name: np.mean(values) for name, values in all_sensitivities.items()
}
sorted_params = sorted(
avg_sensitivities.items(), key=lambda x: x[1], reverse=True
)
print(f"\nParameter Sensitivities (for {results[0].target_name}):")
print("-" * 50)
for param_name, avg_sensitivity in sorted_params:
unit = results[0].target_unit
print(f"{param_name:20s}: {avg_sensitivity:12.2e} {unit}/unit")
# Relative importance
max_sensitivity = (
max(avg_sensitivities.values()) if avg_sensitivities else 1
)
print(
f"\nRelative Importance (normalized to most sensitive parameter):"
)
print("-" * 50)
for param_name, avg_sensitivity in sorted_params:
relative = avg_sensitivity / max_sensitivity * 100
print(f"{param_name:20s}: {relative:6.1f}%")
add_parameter_mapping(self, explorer_name, sim_param_key, display_name, unit, range_config=None, convert_func=None)
¶
Add a new parameter mapping for exploration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
explorer_name |
str |
Name used in the explorer (e.g., 'nozzle_diameter') |
required |
sim_param_key |
str |
Key in simulation parameters (e.g., 'A_nozzle') |
required |
display_name |
str |
Human-readable name for plots |
required |
unit |
str |
Unit for display |
required |
range_config |
Dict |
Dictionary with 'min_factor', 'max_factor', etc. |
None |
convert_func |
Callable |
Optional function to convert explorer value to sim value |
None |
Source code in waterrocketpy/visualization/parameter_explorer.py
def add_parameter_mapping(
self,
explorer_name: str,
sim_param_key: str,
display_name: str,
unit: str,
range_config: Dict = None,
convert_func: Callable = None,
):
"""Add a new parameter mapping for exploration.
Args:
explorer_name: Name used in the explorer (e.g., 'nozzle_diameter')
sim_param_key: Key in simulation parameters (e.g., 'A_nozzle')
display_name: Human-readable name for plots
unit: Unit for display
range_config: Dictionary with 'min_factor', 'max_factor', etc.
convert_func: Optional function to convert explorer value to sim value
"""
mapping = [sim_param_key, display_name, unit]
if range_config or convert_func:
config = range_config or {}
if convert_func:
config["convert_func"] = convert_func
mapping.append(config)
self.parameter_mappings[explorer_name] = tuple(mapping)
create_parameter_configs(self, base_params, parameter_names, custom_ranges=None)
¶
Create parameter configurations for exploration.
Source code in waterrocketpy/visualization/parameter_explorer.py
def create_parameter_configs(
self,
base_params: Dict[str, float],
parameter_names: List[str],
custom_ranges: Dict[str, Dict] = None,
) -> Dict[str, ParameterConfig]:
"""Create parameter configurations for exploration."""
configs = {}
for param_name in parameter_names:
if param_name not in base_params:
raise ValueError(
f"Parameter '{param_name}' not found in base parameters. Available: {list(base_params.keys())}"
)
if param_name not in self.parameter_mappings:
raise ValueError(
f"Parameter '{param_name}' not defined in parameter mappings. Available: {list(self.parameter_mappings.keys())}"
)
# Get mapping info
mapping = self.parameter_mappings[param_name]
sim_param_key, display_name, unit = mapping[:3]
default_range = mapping[4] if len(mapping) > 4 else {}
# Apply custom ranges if provided
if custom_ranges and param_name in custom_ranges:
range_config = {**default_range, **custom_ranges[param_name]}
else:
range_config = default_range
configs[param_name] = ParameterConfig(
name=param_name,
base_value=base_params[param_name],
unit=unit,
**{
k: v
for k, v in range_config.items()
if k != "convert_func"
},
)
return configs
explore_multiple_parameters(self, base_rocket, param_configs, target='apogee', sim_settings=None, use_parallel=True)
¶
Explore multiple parameters with pairwise combinations.
Source code in waterrocketpy/visualization/parameter_explorer.py
def explore_multiple_parameters(
self,
base_rocket,
param_configs: Dict[str, ParameterConfig],
target: str = "apogee",
sim_settings: Dict[str, Any] = None,
use_parallel: bool = True,
) -> List[ExplorationResult]:
"""Explore multiple parameters with pairwise combinations."""
print(
f"Exploring {len(param_configs)} parameters: {list(param_configs.keys())}"
)
target_attr, target_unit, target_display = self.target_extractors[
target
]
# Get base target value
base_flight_data = self.simulate_single_point(
base_rocket, {}, sim_settings
)
if base_flight_data is None:
raise RuntimeError("Base simulation failed")
base_target_value = getattr(base_flight_data, target_attr)
results = []
param_names = list(param_configs.keys())
# Generate all pairwise combinations
for param1_name, param2_name in combinations(param_names, 2):
print(f" Exploring pair: {param1_name} vs {param2_name}")
param1_config = param_configs[param1_name]
param2_config = param_configs[param2_name]
# Create parameter grids
p1_values = param1_config.values
p2_values = param2_config.values
P1, P2 = np.meshgrid(p1_values, p2_values)
target_grid = np.full_like(P1, np.nan)
# Simulate all combinations
total_sims = P1.size
completed_sims = 0
for i in range(P1.shape[0]):
for j in range(P1.shape[1]):
param_values = {
param1_name: P1[i, j],
param2_name: P2[i, j],
}
flight_data = self.simulate_single_point(
base_rocket, param_values, sim_settings
)
if flight_data is not None:
target_grid[i, j] = getattr(flight_data, target_attr)
completed_sims += 1
if completed_sims % 10 == 0:
print(
f" Progress: {completed_sims}/{total_sims} ({100*completed_sims/total_sims:.1f}%)"
)
# Calculate sensitivities for both parameters
sensitivity1 = self._calculate_sensitivity(
base_rocket,
param1_config,
target,
base_target_value,
sim_settings,
)
sensitivity2 = self._calculate_sensitivity(
base_rocket,
param2_config,
target,
base_target_value,
sim_settings,
)
results.append(
ExplorationResult(
parameter_names=[param1_name, param2_name],
parameter_values={
param1_name: p1_values,
param2_name: p2_values,
},
target_values=target_grid,
target_name=target_display,
target_unit=target_unit,
base_target_value=base_target_value,
sensitivity_analysis={
param1_name: sensitivity1,
param2_name: sensitivity2,
},
)
)
return results
explore_single_parameter(self, base_rocket, param_config, target='apogee', sim_settings=None)
¶
Explore a single parameter.
Source code in waterrocketpy/visualization/parameter_explorer.py
def explore_single_parameter(
self,
base_rocket,
param_config: ParameterConfig,
target: str = "apogee",
sim_settings: Dict[str, Any] = None,
) -> ExplorationResult:
"""Explore a single parameter."""
print(f"Exploring parameter: {param_config.name}")
target_attr, target_unit, target_display = self.target_extractors[
target
]
# Get base target value
base_flight_data = self.simulate_single_point(
base_rocket, {}, sim_settings
)
if base_flight_data is None:
raise RuntimeError("Base simulation failed")
base_target_value = getattr(base_flight_data, target_attr)
# Explore parameter range
target_values = []
valid_param_values = []
for param_value in param_config.values:
flight_data = self.simulate_single_point(
base_rocket, {param_config.name: param_value}, sim_settings
)
if flight_data is not None:
target_values.append(getattr(flight_data, target_attr))
valid_param_values.append(param_value)
else:
target_values.append(np.nan)
valid_param_values.append(param_value)
# Calculate sensitivity (numerical derivative at base value)
sensitivity = self._calculate_sensitivity(
base_rocket, param_config, target, base_target_value, sim_settings
)
return ExplorationResult(
parameter_names=[param_config.name],
parameter_values={param_config.name: np.array(valid_param_values)},
target_values=np.array(target_values),
target_name=target_display,
target_unit=target_unit,
base_target_value=base_target_value,
sensitivity_analysis={param_config.name: sensitivity},
)
extract_base_parameters(self, rocket)
¶
Extract base parameter values from a rocket configuration.
Source code in waterrocketpy/visualization/parameter_explorer.py
def extract_base_parameters(self, rocket) -> Dict[str, float]:
"""Extract base parameter values from a rocket configuration."""
# Convert rocket to simulation parameters to get the base values
builder = RocketBuilder.from_dict(rocket.__dict__)
sim_params = builder.to_simulation_params()
base_params = {}
print("Debug - Available simulation parameters:")
for key, value in sim_params.items():
print(f" {key}: {value}")
# Extract parameters based on our mappings
for (
explorer_param_name,
mapping_info,
) in self.parameter_mappings.items():
sim_param_key = mapping_info[0]
if sim_param_key in sim_params:
value = sim_params[sim_param_key]
# Convert area back to diameter if needed
if (
"diameter" in explorer_param_name
and "area" in sim_param_key.lower()
):
# Convert area to diameter: A = π(d/2)², so d = 2√(A/π)
value = 2 * np.sqrt(value / np.pi)
base_params[explorer_param_name] = value
print("\nDebug - Extracted base parameters:")
for key, value in base_params.items():
print(f" {key}: {value}")
return base_params
get_available_parameters(self)
¶
Get list of available parameters and their descriptions.
Source code in waterrocketpy/visualization/parameter_explorer.py
def get_available_parameters(self) -> Dict[str, str]:
"""Get list of available parameters and their descriptions."""
available = {}
for param_name, mapping in self.parameter_mappings.items():
sim_param_key, display_name, unit = mapping[:3]
available[param_name] = (
f"{display_name} ({unit}) -> {sim_param_key}"
)
return available
plot_results(self, results, save_plots=False)
¶
Create plots for exploration results.
Source code in waterrocketpy/visualization/parameter_explorer.py
def plot_results(
self, results: List[ExplorationResult], save_plots: bool = False
):
"""Create plots for exploration results."""
for i, result in enumerate(results):
if len(result.parameter_names) == 1:
self._plot_single_parameter(result, save_plots, i)
elif len(result.parameter_names) == 2:
self._plot_two_parameters(result, save_plots, i)
print_sensitivity_analysis(self, results)
¶
Print sensitivity analysis results.
Source code in waterrocketpy/visualization/parameter_explorer.py
def print_sensitivity_analysis(self, results: List[ExplorationResult]):
"""Print sensitivity analysis results."""
print("\n" + "=" * 60)
print("SENSITIVITY ANALYSIS")
print("=" * 60)
all_sensitivities = {}
for result in results:
for param_name, sensitivity in result.sensitivity_analysis.items():
if param_name not in all_sensitivities:
all_sensitivities[param_name] = []
all_sensitivities[param_name].append(abs(sensitivity))
# Average sensitivities and sort by magnitude
avg_sensitivities = {
name: np.mean(values) for name, values in all_sensitivities.items()
}
sorted_params = sorted(
avg_sensitivities.items(), key=lambda x: x[1], reverse=True
)
print(f"\nParameter Sensitivities (for {results[0].target_name}):")
print("-" * 50)
for param_name, avg_sensitivity in sorted_params:
unit = results[0].target_unit
print(f"{param_name:20s}: {avg_sensitivity:12.2e} {unit}/unit")
# Relative importance
max_sensitivity = (
max(avg_sensitivities.values()) if avg_sensitivities else 1
)
print(
f"\nRelative Importance (normalized to most sensitive parameter):"
)
print("-" * 50)
for param_name, avg_sensitivity in sorted_params:
relative = avg_sensitivity / max_sensitivity * 100
print(f"{param_name:20s}: {relative:6.1f}%")
simulate_single_point(self, base_rocket, param_values, sim_settings=None)
¶
Simulate a single parameter point.
Source code in waterrocketpy/visualization/parameter_explorer.py
def simulate_single_point(
self,
base_rocket,
param_values: Dict[str, float],
sim_settings: Dict[str, Any] = None,
) -> Optional[Any]:
"""Simulate a single parameter point."""
try:
# Create a copy of the base rocket parameters
builder = RocketBuilder.from_dict(base_rocket.__dict__)
sim_params = builder.to_simulation_params()
if(self.verbose):
print(f"Debug - Simulating with param_values: {param_values}")
print(
f"Debug - Original sim_params keys: {list(sim_params.keys())}"
)
# Update parameters using the generic updater
for param_name, value in param_values.items():
self._update_parameter(sim_params, param_name, value)
if(self.verbose):
print(
f"Debug - Updated sim_params keys: {list(sim_params.keys())}"
)
# Default simulation settings
if sim_settings is None:
sim_settings = {
"max_time": 15.0,
"time_step": 0.01,
"solver": "RK45",
}
# Run simulation
flight_data = self.simulator.simulate(sim_params, sim_settings)
return flight_data
except Exception as e:
warnings.warn(
f"Simulation failed for parameters {param_values}: {e}"
)
print(f"Debug - Exception details: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return None
main()
¶
Example usage of the parameter explorer.
Source code in waterrocketpy/visualization/parameter_explorer.py
def main():
"""Example usage of the parameter explorer."""
print("=== Water Rocket Parameter Explorer ===\n")
# Create base rocket configuration
print("1. Creating base rocket configuration...")
base_rocket = create_standard_rocket()
print(f" Base rocket: {base_rocket.name}")
# Initialize explorer
explorer = ParameterExplorer()
# Extract base parameters
base_params = explorer.extract_base_parameters(base_rocket)
print("\n2. Base parameters:")
for name, value in base_params.items():
print(f" {name}: {value}")
# Define parameters to explore - now using correct parameter names
parameters_to_explore = [
"initial_pressure",
"water_fraction",
"nozzle_diameter",
]
target_metric = "apogee"
print(f"\n3. Available parameters:")
available_params = explorer.get_available_parameters()
for name, description in available_params.items():
print(f" {name}: {description}")
print(f"\n4. Exploring parameters: {parameters_to_explore}")
print(f" Target metric: {target_metric}")
# Create parameter configurations
param_configs = explorer.create_parameter_configs(
base_params,
parameters_to_explore,
custom_ranges={
"initial_pressure": {
"min_factor": 0.4,
"max_factor": 2.5,
"num_points": 8,
},
"water_fraction": {
"min_factor": 0.5,
"max_factor": 1.8,
"num_points": 8,
},
"nozzle_diameter": {
"min_factor": 0.6,
"max_factor": 2.0,
"num_points": 8,
},
},
)
# Run exploration
print("\n5. Running parameter exploration...")
results = explorer.explore_multiple_parameters(
base_rocket,
param_configs,
target=target_metric,
sim_settings={"max_time": 20.0, "time_step": 0.01},
)
# Display results
print(f"\n6. Generated {len(results)} result sets")
# Create plots
print("\n7. Creating plots...")
explorer.plot_results(results)
# Print sensitivity analysis
explorer.print_sensitivity_analysis(results)
print("\nExploration complete!")