Skip to main content
Glama
test_integrations_propellers.py23.7 kB
""" Tests for propeller and UAV energy analysis integration module. """ import pytest from aerospace_mcp.integrations.propellers import ( AEROSANDBOX_AVAILABLE, BatteryConfiguration, PropellerGeometry, UAVConfiguration, get_battery_database, get_propeller_database, motor_propeller_matching, propeller_bemt_analysis, uav_energy_estimate, ) class TestPropellerGeometry: """Test propeller geometry model.""" def test_propeller_geometry_creation(self): """Test creating propeller geometry objects.""" geometry = PropellerGeometry( diameter_m=0.254, # 10 inches pitch_m=0.178, # 7 inches num_blades=2, activity_factor=100, cl_design=0.5, cd_design=0.02, ) assert geometry.diameter_m == 0.254 assert geometry.pitch_m == 0.178 assert geometry.num_blades == 2 assert geometry.activity_factor == 100 assert geometry.cl_design == 0.5 assert geometry.cd_design == 0.02 def test_propeller_geometry_validation(self): """Test propeller geometry validation.""" # Test minimum constraints with pytest.raises(ValueError): PropellerGeometry( diameter_m=0.0, # Too small pitch_m=0.1, num_blades=2, ) with pytest.raises(ValueError): PropellerGeometry( diameter_m=0.2, pitch_m=0.1, num_blades=1, # Too few blades ) class TestPropellerAnalysis: """Test propeller performance analysis.""" def test_basic_propeller_analysis(self): """Test basic propeller analysis functionality.""" geometry = PropellerGeometry( diameter_m=0.254, # 10 inch prop pitch_m=0.178, # 7 inch pitch num_blades=2, ) rpm_list = [1000, 2000, 3000, 4000, 5000] # Static thrust analysis results = propeller_bemt_analysis(geometry, rpm_list, velocity_ms=0.0) assert len(results) == len(rpm_list) # Check basic propeller physics for i, result in enumerate(results): assert result.rpm == rpm_list[i] assert result.thrust_n >= 0 # Thrust should be non-negative assert result.power_w >= 0 # Power should be non-negative assert result.torque_nm >= 0 # Torque should be non-negative assert 0 <= result.efficiency <= 1.0 # Efficiency should be between 0 and 1 # Higher RPM should generally produce more thrust (for static case) for i in range(len(results) - 1): assert results[i + 1].thrust_n >= results[i].thrust_n assert results[i + 1].power_w >= results[i].power_w def test_forward_flight_analysis(self): """Test propeller analysis in forward flight.""" geometry = PropellerGeometry( diameter_m=0.3048, # 12 inch prop pitch_m=0.2032, # 8 inch pitch num_blades=2, ) rpm_list = [2000, 3000, 4000] velocity_ms = 20.0 # 20 m/s forward speed results = propeller_bemt_analysis(geometry, rpm_list, velocity_ms) assert len(results) == len(rpm_list) # Check advance ratios are calculated for result in results: expected_J = velocity_ms / (result.rpm / 60.0 * geometry.diameter_m) assert abs(result.advance_ratio - expected_J) < 0.01 # In forward flight, efficiency should be higher than static static_results = propeller_bemt_analysis(geometry, rpm_list, 0.0) for forward, static in zip(results, static_results, strict=False): if ( forward.rpm == static.rpm and forward.power_w > 10 ): # Avoid division by small numbers assert ( forward.efficiency >= static.efficiency - 0.1 ) # Allow some tolerance def test_altitude_effects(self): """Test altitude effects on propeller performance.""" geometry = PropellerGeometry( diameter_m=0.254, pitch_m=0.152, # 6 inch pitch num_blades=3, ) rpm_list = [3000, 5000] # Compare sea level vs altitude performance sea_level_results = propeller_bemt_analysis( geometry, rpm_list, 0.0, altitude_m=0 ) altitude_results = propeller_bemt_analysis( geometry, rpm_list, 0.0, altitude_m=3000 ) # At altitude, air density is lower, so thrust should decrease for sl, alt in zip(sea_level_results, altitude_results, strict=False): if sl.rpm == alt.rpm: assert alt.thrust_n < sl.thrust_n # Less thrust at altitude assert ( alt.power_w < sl.power_w ) # Less power at altitude (due to density) def test_propeller_coefficients(self): """Test propeller thrust and power coefficients.""" geometry = PropellerGeometry( diameter_m=0.2032, # 8 inch pitch_m=0.1016, # 4 inch pitch num_blades=2, ) results = propeller_bemt_analysis(geometry, [4000], 0.0) result = results[0] # Check coefficient calculations rho = 1.225 # Sea level density n = result.rpm / 60.0 D = geometry.diameter_m expected_CT = result.thrust_n / (rho * n**2 * D**4) expected_CP = result.power_w / (rho * n**3 * D**5) assert abs(result.thrust_coefficient - expected_CT) < 0.001 assert abs(result.power_coefficient - expected_CP) < 0.001 # Coefficients should be in reasonable ranges assert 0.01 < result.thrust_coefficient < 0.3 assert 0.01 < result.power_coefficient < 0.3 def test_different_propeller_sizes(self): """Test analysis with different propeller sizes.""" # Small prop small_prop = PropellerGeometry( diameter_m=0.152, # 6 inch pitch_m=0.102, # 4 inch pitch num_blades=2, ) # Large prop large_prop = PropellerGeometry( diameter_m=0.381, # 15 inch pitch_m=0.254, # 10 inch pitch num_blades=2, ) rpm = 3000 small_results = propeller_bemt_analysis(small_prop, [rpm], 0.0) large_results = propeller_bemt_analysis(large_prop, [rpm], 0.0) small_result = small_results[0] large_result = large_results[0] # Large prop should produce more thrust at same RPM assert large_result.thrust_n > small_result.thrust_n # Large prop should require more power assert large_result.power_w > small_result.power_w # Large prop may have better efficiency assert large_result.efficiency >= small_result.efficiency - 0.1 class TestUAVConfiguration: """Test UAV configuration models.""" def test_uav_configuration_creation(self): """Test creating UAV configuration objects.""" config = UAVConfiguration( mass_kg=2.5, wing_area_m2=0.8, cd0=0.025, cl_cruise=0.8, num_motors=1, motor_efficiency=0.88, esc_efficiency=0.96, ) assert config.mass_kg == 2.5 assert config.wing_area_m2 == 0.8 assert config.cd0 == 0.025 assert config.cl_cruise == 0.8 assert config.num_motors == 1 assert config.motor_efficiency == 0.88 assert config.esc_efficiency == 0.96 def test_battery_configuration_creation(self): """Test creating battery configuration objects.""" battery = BatteryConfiguration( capacity_ah=5.0, voltage_nominal_v=14.8, mass_kg=0.8, energy_density_wh_kg=180, discharge_efficiency=0.95, ) assert battery.capacity_ah == 5.0 assert battery.voltage_nominal_v == 14.8 assert battery.mass_kg == 0.8 assert battery.energy_density_wh_kg == 180 assert battery.discharge_efficiency == 0.95 class TestUAVEnergyAnalysis: """Test UAV energy analysis.""" def test_fixed_wing_energy_analysis(self): """Test energy analysis for fixed-wing UAV.""" uav_config = UAVConfiguration( mass_kg=2.0, wing_area_m2=0.6, cd0=0.03, cl_cruise=0.7, num_motors=1, motor_efficiency=0.85, esc_efficiency=0.95, ) battery_config = BatteryConfiguration( capacity_ah=4.0, voltage_nominal_v=14.8, mass_kg=0.6 ) mission_profile = {"velocity_ms": 15.0, "altitude_m": 100.0} result = uav_energy_estimate(uav_config, battery_config, mission_profile) # Check basic results assert result.flight_time_min > 0 assert result.range_km > 0 # Should have range for fixed-wing assert result.hover_time_min is None # No hover time for fixed-wing assert result.power_required_w > 0 assert result.energy_consumed_wh > 0 assert result.battery_energy_wh > 0 assert 0 < result.efficiency_overall <= 1.0 # Check energy balance assert result.energy_consumed_wh <= result.battery_energy_wh # Flight time should be reasonable (10 minutes to 3 hours) assert 10 < result.flight_time_min < 180 # Range should be reasonable for small UAV assert 5 < result.range_km < 100 def test_multirotor_energy_analysis(self): """Test energy analysis for multirotor UAV.""" uav_config = UAVConfiguration( mass_kg=1.5, disk_area_m2=0.2, # Total rotor disk area cd0=0.05, # Higher drag for multirotor num_motors=4, motor_efficiency=0.85, esc_efficiency=0.95, ) battery_config = BatteryConfiguration( capacity_ah=5.0, voltage_nominal_v=22.2, # 6S battery mass_kg=0.7, ) mission_profile = { "velocity_ms": 8.0, # Slower than fixed-wing "altitude_m": 50.0, } result = uav_energy_estimate(uav_config, battery_config, mission_profile) # Check basic results assert result.flight_time_min > 0 assert result.range_km is None # No range calculation for multirotor assert result.hover_time_min > 0 # Should have hover time assert ( result.hover_time_min == result.flight_time_min ) # Should be equal for multirotor # Multirotor typically has shorter flight times than fixed-wing assert 5 < result.flight_time_min < 60 # Typical multirotor endurance def test_energy_scaling_effects(self): """Test how energy consumption scales with aircraft parameters.""" base_uav = UAVConfiguration( mass_kg=2.0, wing_area_m2=0.5, cd0=0.03, # Don't specify cl_cruise so it gets calculated from weight ) base_battery = BatteryConfiguration( capacity_ah=3.0, voltage_nominal_v=11.1, mass_kg=0.4 ) mission = {"velocity_ms": 12.0} # Test mass scaling heavy_uav = UAVConfiguration( mass_kg=4.0, # Double the mass wing_area_m2=0.5, cd0=0.03, # Don't specify cl_cruise so it gets calculated from weight ) base_result = uav_energy_estimate(base_uav, base_battery, mission) heavy_result = uav_energy_estimate(heavy_uav, base_battery, mission) # Heavier aircraft should require more power assert heavy_result.power_required_w > base_result.power_required_w # And have shorter flight time with same battery assert heavy_result.flight_time_min < base_result.flight_time_min def test_battery_scaling_effects(self): """Test how battery capacity affects flight time.""" uav_config = UAVConfiguration( mass_kg=1.8, wing_area_m2=0.45, cd0=0.028, cl_cruise=0.75 ) small_battery = BatteryConfiguration( capacity_ah=2.0, voltage_nominal_v=11.1, mass_kg=0.3 ) large_battery = BatteryConfiguration( capacity_ah=6.0, # Triple capacity voltage_nominal_v=11.1, mass_kg=0.9, ) mission = {"velocity_ms": 14.0} small_result = uav_energy_estimate(uav_config, small_battery, mission) large_result = uav_energy_estimate(uav_config, large_battery, mission) # Larger battery should provide longer flight time assert large_result.flight_time_min > small_result.flight_time_min # Should be roughly proportional to capacity (accounting for increased mass) capacity_ratio = large_battery.capacity_ah / small_battery.capacity_ah time_ratio = large_result.flight_time_min / small_result.flight_time_min # Time ratio should be less than capacity ratio due to increased battery mass assert 1.5 < time_ratio < capacity_ratio def test_velocity_effects(self): """Test velocity effects on energy consumption.""" uav_config = UAVConfiguration( mass_kg=2.2, wing_area_m2=0.55, cd0=0.032, cl_cruise=0.85 ) battery_config = BatteryConfiguration( capacity_ah=4.5, voltage_nominal_v=14.8, mass_kg=0.65 ) # Test different velocities slow_result = uav_energy_estimate( uav_config, battery_config, {"velocity_ms": 10.0} ) fast_result = uav_energy_estimate( uav_config, battery_config, {"velocity_ms": 20.0} ) # There should be an optimal speed; very slow and very fast both inefficient # At minimum, power should increase with velocity^3 due to drag assert fast_result.power_required_w > slow_result.power_required_w def test_altitude_effects_energy(self): """Test altitude effects on energy consumption.""" uav_config = UAVConfiguration( mass_kg=1.9, wing_area_m2=0.48, cd0=0.03, cl_cruise=0.8 ) battery_config = BatteryConfiguration( capacity_ah=3.5, voltage_nominal_v=11.1, mass_kg=0.45 ) sea_level_result = uav_energy_estimate( uav_config, battery_config, {"velocity_ms": 13.0, "altitude_m": 0} ) altitude_result = uav_energy_estimate( uav_config, battery_config, {"velocity_ms": 13.0, "altitude_m": 2000} ) # At altitude, air density is lower, which affects both lift and drag # Results should be different assert altitude_result.power_required_w != sea_level_result.power_required_w class TestDatabases: """Test propeller and battery databases.""" def test_propeller_database_access(self): """Test propeller database functionality.""" database = get_propeller_database() assert isinstance(database, dict) assert len(database) > 0 # Check that standard propellers are present assert any("APC" in name for name in database.keys()) # Check data structure for _prop_name, data in database.items(): assert "diameter_m" in data assert "pitch_m" in data assert "num_blades" in data assert "efficiency_max" in data # Check reasonable values assert 0.05 < data["diameter_m"] < 1.0 # 2-40 inch range assert 0.02 < data["pitch_m"] < 0.5 # Reasonable pitch range assert 2 <= data["num_blades"] <= 6 # Typical blade counts assert 0.5 < data["efficiency_max"] < 0.9 # Realistic efficiency range def test_battery_database_access(self): """Test battery database functionality.""" database = get_battery_database() assert isinstance(database, dict) assert len(database) > 0 # Check that standard battery types are present assert any("LiPo" in name for name in database.keys()) # Check data structure for _battery_name, data in database.items(): assert "nominal_voltage_v" in data assert "energy_density_wh_kg" in data assert "discharge_efficiency" in data # Check reasonable values assert 3.0 < data["nominal_voltage_v"] < 30.0 # Typical voltages assert 50 < data["energy_density_wh_kg"] < 300 # Realistic energy densities assert 0.8 < data["discharge_efficiency"] < 1.0 # Reasonable efficiency class TestMotorPropellerMatching: """Test motor-propeller matching analysis.""" def test_motor_propeller_matching_analysis(self): """Test motor-propeller matching functionality.""" motor_kv = 1000 # 1000 RPM/V battery_voltage = 11.1 # 3S battery propeller_options = ["APC_10x7", "APC_12x8"] thrust_required = 15.0 # 15N thrust requirement results = motor_propeller_matching( motor_kv, battery_voltage, propeller_options, thrust_required ) assert isinstance(results, dict) assert len(results) > 0 # Check results for each propeller for prop_name, analysis in results.items(): assert prop_name in propeller_options assert "geometry" in analysis assert "operating_point" in analysis assert "thrust_error_n" in analysis assert "efficiency" in analysis assert "power_required_w" in analysis assert "rpm_recommended" in analysis assert "suitable" in analysis # Check operating point data op = analysis["operating_point"] assert op["thrust_n"] >= 0 assert op["power_w"] >= 0 assert 0 <= op["efficiency"] <= 1.0 # RPM should be reasonable for the motor/battery combination max_rpm = motor_kv * battery_voltage * 0.85 assert op["rpm"] <= max_rpm def test_thrust_requirement_matching(self): """Test matching to specific thrust requirements.""" motor_kv = 850 battery_voltage = 14.8 # 4S props = ["APC_10x7", "APC_15x10"] # Test with different thrust requirements low_thrust_results = motor_propeller_matching( motor_kv, battery_voltage, props, 5.0 ) high_thrust_results = motor_propeller_matching( motor_kv, battery_voltage, props, 25.0 ) # Should get recommendations for both cases assert len(low_thrust_results) > 0 assert len(high_thrust_results) > 0 # Larger prop (APC_15x10) should be more suitable for high thrust if "APC_15x10" in high_thrust_results: large_prop_analysis = high_thrust_results["APC_15x10"] assert ( large_prop_analysis["thrust_error_n"] >= 0 ) # Should be able to meet requirement class TestIntegration: """Integration tests for propellers module.""" def test_complete_uav_design_workflow(self): """Test complete UAV design and analysis workflow.""" # Define UAV requirements target_speed_ms = 12.0 # Design UAV configuration uav_config = UAVConfiguration( mass_kg=2.5, # Including payload wing_area_m2=0.6, cd0=0.028, cl_cruise=0.8, num_motors=1, motor_efficiency=0.86, esc_efficiency=0.95, ) # Select battery battery_config = BatteryConfiguration( capacity_ah=5.2, voltage_nominal_v=14.8, mass_kg=0.8 ) # Analyze energy requirements mission = {"velocity_ms": target_speed_ms, "altitude_m": 100.0} energy_result = uav_energy_estimate(uav_config, battery_config, mission) # Should meet endurance requirement assert energy_result.flight_time_min > 30 # At least reasonable endurance # Select propeller motor_kv = 800 battery_voltage = 14.0 # Under load voltage propeller_options = ["APC_12x8", "APC_15x10"] # Estimate required thrust (approximately equal to weight for level flight) thrust_required = ( uav_config.mass_kg * 9.81 * 1.2 ) # 120% of weight for climb capability prop_analysis = motor_propeller_matching( motor_kv, battery_voltage, propeller_options, thrust_required ) # Should get viable propeller options assert len(prop_analysis) > 0 suitable_props = [ name for name, data in prop_analysis.items() if data["suitable"] ] assert len(suitable_props) > 0 # Verify complete system integration assert energy_result.battery_energy_wh > energy_result.energy_consumed_wh assert energy_result.efficiency_overall > 0.7 # Should be reasonably efficient @pytest.mark.skipif(not AEROSANDBOX_AVAILABLE, reason="AeroSandbox not available") def test_aerosandbox_propeller_integration(self): """Test AeroSandbox propeller integration if available.""" geometry = PropellerGeometry(diameter_m=0.254, pitch_m=0.178, num_blades=2) results = propeller_bemt_analysis(geometry, [3000, 4000], 10.0) # Should get more accurate results from AeroSandbox assert len(results) == 2 for result in results: assert isinstance(result.efficiency, float) assert 0 <= result.efficiency <= 1.0 def test_error_handling_propellers(self): """Test error handling for invalid inputs.""" # Test invalid propeller geometry with pytest.raises(ValueError): PropellerGeometry( diameter_m=0.0, # Invalid pitch_m=0.1, num_blades=2, ) # Test invalid UAV configuration with pytest.raises(ValueError): UAVConfiguration(mass_kg=0.0) # Invalid mass # Test invalid battery configuration with pytest.raises(ValueError): BatteryConfiguration( capacity_ah=0.0, # Invalid voltage_nominal_v=12.0, mass_kg=1.0, ) def test_performance_characteristics_propellers(self): """Test that propeller calculations complete in reasonable time.""" import time geometry = PropellerGeometry(diameter_m=0.3, pitch_m=0.2, num_blades=3) start_time = time.time() # Run multiple analyses static_results = propeller_bemt_analysis( geometry, list(range(1000, 6001, 500)), 0.0 ) forward_results = propeller_bemt_analysis( geometry, list(range(1000, 6001, 500)), 15.0 ) # UAV analysis uav_config = UAVConfiguration( mass_kg=2.0, wing_area_m2=0.5, cd0=0.03, cl_cruise=0.8 ) battery_config = BatteryConfiguration( capacity_ah=4.0, voltage_nominal_v=14.8, mass_kg=0.6 ) energy_result = uav_energy_estimate( uav_config, battery_config, {"velocity_ms": 12.0} ) end_time = time.time() # Should complete quickly assert (end_time - start_time) < 3.0 # Should get reasonable number of results assert len(static_results) == 11 assert len(forward_results) == 11 assert energy_result.flight_time_min > 0 if __name__ == "__main__": pytest.main([__file__])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cheesejaguar/aerospace-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server