| """ |
| Sensitivity Analysis Engine |
| ============================ |
| Computes parameter sensitivity, trade-off curves, and feasibility boundaries. |
| """ |
|
|
| import numpy as np |
| from engine.buoyancy_calculator import compute_buoyancy |
|
|
|
|
| def compute_tornado(base_params, variation_pct=20.0): |
| """ |
| Tornado chart data: vary each parameter +/- variation_pct and measure |
| effect on mass_available_kg. |
| Returns list of dicts sorted by impact magnitude, and the base mass. |
| """ |
| param_defs = [ |
| ("Outer Radius", "outer_radius_m"), |
| ("Shell Thickness", "thickness_m"), |
| ("Material Density", "material_density_kg_m3"), |
| ("Internal Pressure", "internal_pressure_Pa"), |
| ("Atmospheric Pressure", "atmospheric_pressure_Pa"), |
| ] |
| base_result = compute_buoyancy(**base_params) |
| base_mass = base_result.mass_available_kg |
| results = [] |
|
|
| for label, key in param_defs: |
| base_val = base_params[key] |
| low_val = base_val * (1 - variation_pct / 100.0) |
| high_val = base_val * (1 + variation_pct / 100.0) |
| p_low = {**base_params, key: low_val} |
| p_high = {**base_params, key: high_val} |
| if key == "thickness_m": |
| p_low[key] = max(p_low[key], 0.00001) |
| p_high[key] = min(p_high[key], base_params["outer_radius_m"] * 0.5) |
| if key == "internal_pressure_Pa": |
| p_high[key] = min(p_high[key], base_params["atmospheric_pressure_Pa"] - 100) |
| try: |
| mass_low = compute_buoyancy(**p_low).mass_available_kg |
| except (ValueError, ZeroDivisionError): |
| mass_low = base_mass |
| try: |
| mass_high = compute_buoyancy(**p_high).mass_available_kg |
| except (ValueError, ZeroDivisionError): |
| mass_high = base_mass |
| results.append({ |
| "parameter": label, "base_value": base_val, |
| "mass_at_low": mass_low, "mass_at_high": mass_high, |
| "delta_low": mass_low - base_mass, "delta_high": mass_high - base_mass, |
| "total_swing": abs(mass_high - mass_low), |
| }) |
| results.sort(key=lambda x: x["total_swing"], reverse=True) |
| return results, base_mass |
|
|
|
|
| def compute_tradeoff_grid(material_density, internal_pressure_Pa, |
| atmospheric_pressure_Pa, |
| r_min=1.0, r_max=20.0, r_steps=40, |
| t_min=0.0001, t_max=0.005, t_steps=40): |
| """2D grid of mass_available for radius vs thickness.""" |
| radii = np.linspace(r_min, r_max, r_steps) |
| thicknesses = np.linspace(t_min, t_max, t_steps) |
| mass_grid = np.zeros((len(thicknesses), len(radii))) |
| for i, t in enumerate(thicknesses): |
| for j, r in enumerate(radii): |
| try: |
| result = compute_buoyancy(r, t, material_density, |
| internal_pressure_Pa, atmospheric_pressure_Pa) |
| mass_grid[i, j] = result.mass_available_kg |
| except (ValueError, ZeroDivisionError): |
| mass_grid[i, j] = float('nan') |
| return radii, thicknesses, mass_grid |
|
|
|
|
| def compute_feasibility_boundary(material_density, internal_pressure_Pa, |
| atmospheric_pressure_Pa, |
| r_min=1.0, r_max=20.0, r_steps=100): |
| """For each radius, find max thickness with positive buoyancy.""" |
| radii = np.linspace(r_min, r_max, r_steps) |
| max_thicknesses = [] |
| for r in radii: |
| max_t = 0.0 |
| for t_1000x in range(1, 200): |
| t = t_1000x / 1000.0 |
| if t >= r: |
| break |
| try: |
| result = compute_buoyancy(r, t, material_density, |
| internal_pressure_Pa, atmospheric_pressure_Pa) |
| if result.buoyancy_state == "Positive Buoyancy": |
| max_t = t |
| else: |
| break |
| except (ValueError, ZeroDivisionError): |
| break |
| max_thicknesses.append(max_t) |
| return radii, np.array(max_thicknesses) |