pterasim.simulate
Calculate aerodynamic coefficients for wing simulations. Input wing geometry, flapping schedule, and timesteps to receive force, torque, and solver data for flight analysis.
Instructions
Generate aerodynamic coefficients with UVLM fallback. Supply wing geometry, flapping schedule, and timestep count. Returns forces, torques, and solver metadata. Example: {"span_m":0.8,"chord_m":0.12,"num_timesteps":180}
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| request | Yes |
Implementation Reference
- src/pterasim_mcp/core.py:14-49 (handler)Core implementation of the pterasim.simulate tool: orchestrates high-fidelity (PteraSoftware UVLM) or analytic surrogate simulation for flapping wing aerodynamics.def simulate_pterasim(inputs: PterasimInput) -> PterasimOutput: """Simulate flapping wing performance.""" if inputs.prefer_high_fidelity and is_available(): try: result = run_high_fidelity(inputs) if result is not None: return result except Exception as exc: # pragma: no cover - fallback path LOGGER.warning("High-fidelity PteraSoftware run failed, falling back to surrogate: %s", exc) return _analytic_surrogate(inputs) def _analytic_surrogate(inputs: PterasimInput) -> PterasimOutput: rho = inputs.air_density_kg_m3 omega = 2.0 * math.pi * inputs.stroke_frequency_hz aspect_ratio = inputs.span_m**2 / inputs.planform_area_m2 cl = inputs.cl_alpha_per_rad * inputs.stroke_amplitude_rad dynamic_pressure = 0.5 * rho * max(inputs.cruise_velocity_m_s, 0.1) ** 2 heave_q = 0.5 * rho * inputs.planform_area_m2 * (omega * inputs.stroke_amplitude_rad) ** 2 lift = dynamic_pressure * inputs.planform_area_m2 * cl + heave_q induced = 0.0 if aspect_ratio > 0: induced = (cl**2) / (math.pi * aspect_ratio * 0.9) cd = inputs.cd0 + induced drag = dynamic_pressure * inputs.planform_area_m2 * cd thrust = drag moment_arm = ( inputs.tail_moment_arm_m if inputs.tail_moment_arm_m is not None else inputs.span_m / 4.0 ) torque = lift * moment_arm metadata = { "solver": "analytic", } return PterasimOutput(thrust_N=thrust, lift_N=lift, torque_Nm=torque, metadata=metadata)
- src/pterasim_mcp/models.py:8-29 (schema)Pydantic schemas defining the input parameters and output structure for the pterasim.simulate tool.class PterasimInput(BaseModel): span_m: float = Field(..., gt=0.0) mean_chord_m: float = Field(..., gt=0.0) stroke_frequency_hz: float = Field(..., ge=0.0) stroke_amplitude_rad: float = Field(..., ge=0.0) cruise_velocity_m_s: float = Field(..., ge=0.0) air_density_kg_m3: float = Field(..., gt=0.0) cl_alpha_per_rad: float = Field(...) cd0: float = Field(..., ge=0.0) planform_area_m2: float = Field(..., gt=0.0) tail_moment_arm_m: float | None = Field(default=None, ge=0.0) prefer_high_fidelity: bool = Field( default=True, description="Attempt to use PteraSoftware when available before falling back to the analytic surrogate.", ) class PterasimOutput(BaseModel): thrust_N: float lift_N: float torque_Nm: float metadata: dict[str, object] | None = Field(default=None, description="Solver details and diagnostics")
- src/pterasim_mcp/tool.py:14-25 (registration)MCP tool registration using FastMCP, with thin wrapper delegating to core handler.@app.tool( name="pterasim.simulate", description=( "Generate aerodynamic coefficients with UVLM fallback. " "Supply wing geometry, flapping schedule, and timestep count. " "Returns forces, torques, and solver metadata. " "Example: {\"span_m\":0.8,\"chord_m\":0.12,\"num_timesteps\":180}" ), meta={"version": "0.1.0", "categories": ["aero", "simulation"]}, ) def simulate(request: PterasimInput) -> PterasimOutput: return simulate_pterasim(request)
- High-fidelity aerodynamic simulation helper using PteraSoftware's UVLM solver, called conditionally by the handler.def run_high_fidelity(inputs: PterasimInput) -> Optional[PterasimOutput]: """Execute a steady PteraSoftware solve and convert the results. Returns ``None`` if PteraSoftware is not installed. """ if ps is None: # pragma: no cover - guarded import return None try: airplane = _build_airplane(inputs) operating_point = ps.operating_point.OperatingPoint( # type: ignore[attr-defined] rho=inputs.air_density_kg_m3, vCg__E=max(inputs.cruise_velocity_m_s, 0.1), alpha=math.degrees(inputs.stroke_amplitude_rad), beta=0.0, ) problem = ps.problems.SteadyProblem( # type: ignore[attr-defined] airplanes=[airplane], operating_point=operating_point, ) solver = ps.steady_ring_vortex_lattice_method.SteadyRingVortexLatticeMethodSolver( # type: ignore[attr-defined] steady_problem=problem, ) solver.run(logging_level="Error") forces = solver.airplanes[0].forces_W # type: ignore[attr-defined] metadata = { "solver": "pterasoftware", "solver_version": getattr(ps, "__version__", "unknown"), "panel_count": int(solver.num_panels), } velocity = max(inputs.cruise_velocity_m_s, 0.1) rho = inputs.air_density_kg_m3 ref_area = inputs.planform_area_m2 omega = 2.0 * math.pi * inputs.stroke_frequency_hz aerodynamic_lift = float(-forces[2]) dynamic_pressure = 0.5 * rho * (velocity**2) aspect_ratio = inputs.span_m**2 / ref_area if ref_area > 0 else 0.0 target_cl = inputs.cl_alpha_per_rad * inputs.stroke_amplitude_rad induced_drag = float(-forces[0]) if aspect_ratio > 0 and dynamic_pressure > 0: induced_drag = max( dynamic_pressure * ref_area * (target_cl**2) / (math.pi * aspect_ratio * 0.9), 0.0, ) else: induced_drag = max(induced_drag, 0.0) parasitic_drag = 0.5 * rho * (velocity**2) * ref_area * inputs.cd0 thrust = induced_drag + parasitic_drag heave_lift = 0.5 * rho * ref_area * (omega * inputs.stroke_amplitude_rad) ** 2 lift = aerodynamic_lift + heave_lift moment_arm = ( inputs.tail_moment_arm_m if inputs.tail_moment_arm_m is not None else inputs.span_m / 4.0 ) torque = lift * moment_arm metadata.update( { "induced_drag_N": induced_drag, "parasitic_drag_N": parasitic_drag, "heave_lift_N": heave_lift, "aero_lift_N": aerodynamic_lift, } ) return PterasimOutput( thrust_N=thrust, lift_N=lift, torque_Nm=torque, metadata=metadata, )