Source code for pyLOM.RL.plotting

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
import aerosandbox.tools.pretty_plots as p
from ..utils import raiseWarning


[docs] def create_airfoil_optimization_progress_plot(airfoils, rewards, airfoil_name='Airfoil Shape', save_path=None): """ Create a plot showing the evolution of airfoil shapes and their corresponding lift-to-drag ratios. Args: airfoils (List[asb.Airfoil]): List of airfoil objects representing the evolution. rewards (List[float]): List of lift-to-drag ratios corresponding to each airfoil. airfoil_name (str): Name of the airfoil for the plot title. Default: ``'Airfoil Shape'``. save_path (Optional[str]): Path to save the plot. If None, the plot will not be saved. Default: ``None``. """ fig, ax = plt.subplots(1, 2, figsize=(10, 4), dpi=300) plt.rcParams['figure.facecolor'] = 'white' plt.rcParams['savefig.facecolor'] = 'white' ax[0].set_title(f"{airfoil_name} Evolution") ax[0].set_xlabel("$x/c$") ax[0].set_ylabel("$y/c$") ax[1].set_xlabel("Step") ax[1].set_ylabel("Lift-to-Drag Ratio $C_L/C_D$") plt.tight_layout() cmap = LinearSegmentedColormap.from_list( "custom_cmap", colors=[ p.adjust_lightness(c, 0.8) for c in ["orange", "darkseagreen", "dodgerblue"] ] ) colors = cmap(np.linspace(0, 1, len(airfoils))) # Plot airfoils for i in range(len(airfoils)): plt.sca(ax[0]) plt.tight_layout() plt.plot( airfoils[i].x(), airfoils[i].y(), "-", color=colors[i], alpha=0.5, ) plt.axis('equal') plt.sca(ax[1]) # Plot rewards plt.sca(ax[1]) p.plot_color_by_value( np.arange(len(rewards)), np.array(rewards), ".-", c=np.arange(len(rewards)), cmap=cmap, clim=(0, len(airfoils)), alpha=0.8 ) # Create a mappable object for the colorbar sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(0, len(airfoils))) sm.set_array([]) # Add colorbar using the scalar mappable cbar = fig.colorbar(sm, ax=ax[1], pad=0.01) cbar.set_label('Step') plt.suptitle("Optimization Progress") if save_path is not None: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.tight_layout() plt.show()
try: import manim class AirfoilEvolutionAnimation(manim.Scene): """ Generate an animation showing the evolution of airfoils and their corresponding lift-to-drag ratios. Manim is required to run this animation, and it is recommended to install it with `conda install -c conda-forge manim`. Properties: - `airfoils`: List of airfoil objects representing the evolution. - `rewards`: List of lift-to-drag ratios corresponding to each airfoil. - `run_time`: Duration of the animation in seconds. Default: ``5``. - `title`: Title of the animation. Default: ``"Airfoil evolution"``. Examples: To use in a notebook: >>> import manim >>> from pyLOM.RL import AirfoilEvolutionAnimation >>> %%manim -qm -v WARNING AirfoilEvolution >>> AirfoilEvolutionAnimation.airfoils = airfoils >>> AirfoilEvolutionAnimation.rewards = rewards >>> AirfoilEvolutionAnimation.title = "NACA0012 Evolution" To use it in a script: >>> from pyLOM.RL import AirfoilEvolutionAnimation >>> from manim import * >>> from main import config >>> # config.format = "gif" # Change output format to GIF >>> config.output_file = "airfoil_evolution.mp4" # Change output file name >>> config.quality = "production_quality" # Set quality to maximum (Full HD) >>> animation = AirfoilEvolutionAnimation() >>> animation.airfoils = airfoils >>> animation.rewards = rewards >>> animation.title = "Airfoil Evolution" >>> animation.render() """ airfoils = None rewards = None run_time = 5 title = "Airfoil evolution" def get_airfoil_coordinates(self, airfoil, axes): """ Get airfoil coordinates and map them to Axes coordinates. Assumes airfoil.coordinates is a (points, 2) numpy array. """ return np.array([axes.c2p(x, y) for x, y in airfoil.coordinates]) def interpolate_airfoils(self, start, end, alpha): """Interpolate between two sets of coordinates using alpha.""" return (1 - alpha) * start + alpha * end def construct(self): import manim # Configure manim to not use LaTeX manim.config.renderer = "cairo" # Use Cairo renderer instead of LaTeX # Create axes for airfoil visualization axes_airfoil = manim.Axes( x_range=[0, 1, 0.2], # x-axis range with ticks every 0.2 y_range=[-0.3, 0.3, 0.1], # y-axis range with ticks every 0.1 axis_config={ "include_numbers": True, "font_size": 50, # Larger font size to compensate for scaling }, tips=False, # Remove arrow tips ) # Create axis labels using Text instead of Tex x_label = manim.Text("x", font_size=55).next_to(axes_airfoil.x_axis, manim.DOWN + manim.RIGHT) y_label = manim.Text("y", font_size=55).next_to(axes_airfoil.y_axis, manim.LEFT + manim.UP) # Create the airfoil shape airfoil_shape = manim.VMobject().set_color(manim.BLUE).set_stroke(width=1.5) airfoil_shape.set_points_as_corners(self.get_airfoil_coordinates(self.airfoils[0], axes_airfoil)) self.add(axes_airfoil, x_label, y_label, airfoil_shape) # Group the airfoil-related objects for transformation airfoil_group = manim.VGroup(axes_airfoil, airfoil_shape, x_label, y_label) airfoil_group.scale(0.45).move_to(3.5 * manim.LEFT) # Create axes for the reward plot x_step = len(self.rewards) // 10 if len(self.rewards) > 10 else 1 axes_reward = manim.Axes( x_range=[0, len(self.rewards), x_step], # Iteration range y_range=[ min(self.rewards) * 0.9, max(self.rewards) * 1.1, int((max(self.rewards) * 1.1 - min(self.rewards) * 0.9) / 10) ], # Reward range with padding axis_config={ "include_numbers": True, "font_size": 50, # Larger font size to compensate for scaling }, tips=True, ) # Create reward plot labels using Text instead of Tex iteration_label = manim.Text("Iteration", font_size=55).next_to(axes_reward.x_axis, manim.DOWN) reward_label = manim.Text("CL/CD", font_size=55).next_to(axes_reward.y_axis, manim.LEFT + manim.UP) rewards_group = manim.VGroup(axes_reward, iteration_label, reward_label) rewards_group.scale(0.45).move_to(3.5 * manim.RIGHT) reward_line = manim.VMobject(color=manim.YELLOW).set_stroke(width=2) dot = manim.Dot(color=manim.YELLOW) def update_reward_line(obj): """Updater for the reward plot to grow the line dynamically.""" total_index = alpha.get_value() index = int(total_index) t = total_index - index # Interpolate rewards for smooth transition if index < len(self.rewards) - 1: x_vals = list(range(index + 1)) # Include all completed indices y_vals = self.rewards[:index + 1] # Append interpolated point at fractional step interp_x = index + t interp_y = (1 - t) * self.rewards[index] + t * self.rewards[index + 1] x_vals.append(interp_x) y_vals.append(interp_y) # Update points in the reward line points = [axes_reward.c2p(x, y) for x, y in zip(x_vals, y_vals)] obj.set_points_as_corners(points) dot.move_to(points[-1]) reward_line.add_updater(update_reward_line) self.add(reward_line, axes_reward, iteration_label, reward_label) # Add text annotation for airfoil airfoil_name = manim.Text(self.title, font_size=42).move_to(3 * manim.UP) # Create thickness display using Text and DecimalNumber thickness_label = manim.Text("Max Thickness = ", font_size=30, color=manim.WHITE) thickness_value = manim.DecimalNumber( self.airfoils[0].max_thickness(), num_decimal_places=4, font_size=50, color=manim.WHITE ) thickness_display = manim.VGroup(thickness_label, thickness_value).arrange(manim.RIGHT) thickness_display.next_to(airfoil_group, manim.DOWN) # Create a ValueTracker for interpolation alpha = manim.ValueTracker(0) def update_airfoil_shape(obj): """Updater to interpolate between airfoil shapes.""" total_index = alpha.get_value() index = int(total_index) # Determine the starting airfoil t = total_index - index # Fractional part for interpolation if index < len(self.airfoils) - 1: start_points = self.get_airfoil_coordinates(self.airfoils[index], axes_airfoil) end_points = self.get_airfoil_coordinates(self.airfoils[index + 1], axes_airfoil) interpolated_points = self.interpolate_airfoils(start_points, end_points, t) obj.set_points_as_corners(interpolated_points) # Update thickness value thickness_value.set_value(self.airfoils[index].max_thickness()) airfoil_shape.add_updater(update_airfoil_shape) self.play( manim.Write(airfoil_name), ) self.play( manim.FadeIn(thickness_display), # manim.Write(thickness_display), ) self.add(dot) # add the dot after writing the title # Animate the airfoil evolution and reward plot together self.play( alpha.animate.set_value(len(self.airfoils) - 1), run_time=self.run_time, rate_func=manim.rate_functions.ease_in_circ ) # Clean up airfoil_shape.remove_updater(update_airfoil_shape) reward_line.remove_updater(update_reward_line) self.wait() class WingEvolutionAnimation(manim.ThreeDScene): """ Generate an animation showing the evolution of wings and their corresponding lift-to-drag ratios. Manim is required to run this animation, and it is recommended to install it with `conda install -c conda-forge manim`. Properties: - `wings`: List of wing objects representing the evolution. - `rewards`: List of lift-to-drag ratios corresponding to each airplane. - `run_time_per_update`: Duration of each update in seconds. Default: ``0.25``. Examples: To use in a notebook: >>> import manim >>> from pyLOM.RL import WingEvolutionAnimation on a separete cell, define the wings and rewards: >>> %%manim -qm -v WARNING AirfoilEvolution >>> WingEvolutionAnimation.wings = wings >>> WingEvolutionAnimation.rewards = rewards >>> WingEvolutionAnimation.run_time_per_update = 0.1 """ rewards = None wings = None run_time_per_update = 0.25 def get_mesh_from_airplane(self, airplane): """Constructs a VGroup of polygons from an airplane's mesh data.""" import manim points, faces = airplane.mesh_body(method='tri') # scale the points so that y is normalized to [-1, 1] and the aspect ratio is maintained points = np.array(points) y_min = points[:, 1].min() y_max = points[:, 1].max() # Calculate the center of the y-span y_center = (y_max + y_min) / 2.0 # Calculate the scaling factor scale_factor = 2.0 / (y_max - y_min) # Center the y-axis coordinates around 0 points[:, 1] = points[:, 1] - y_center points *= scale_factor mesh_polys = manim.VGroup() for face in faces: # Get the vertices for this face. # the 5 is hardcoded, but it should look fine independently the size of the wing triangle_vertices = [points[idx] * 5 for idx in face] # Create the polygon. triangle = manim.Polygon( *triangle_vertices, fill_color=manim.BLUE, fill_opacity=0.5, stroke_color=manim.WHITE, stroke_width=0.1, ) mesh_polys.add(triangle) return mesh_polys def construct(self): import manim manim.config.renderer = "cairo" # Use Cairo renderer instead of LaTeX # Set up 3D camera self.set_camera_orientation(phi=75 * manim.DEGREES, theta=210 * manim.DEGREES) x_step = len(self.rewards) // 10 if len(self.rewards) > 10 else 1 rewards_axes = manim.Axes( x_range=[0, len(self.wings), x_step], y_range=[min(self.rewards), max(self.rewards), int((max(self.rewards) * 1.1 - min(self.rewards) * 0.9) / 5)], x_length=7, y_length=3, axis_config={"include_numbers": True}, tips=False ).scale(0.85) labels_airfoil = rewards_axes.get_axis_labels(x_label=manim.Text("Iteration", font_size=16), y_label=manim.Text("CL/CD", font_size=16)) # Position the axes at the bottom of the screen rewards_group = manim.VGroup(rewards_axes, labels_airfoil) rewards_group.to_corner(manim.DOWN + manim.LEFT, buff=0.5) # Create the rewards plot line rewards_points = [rewards_axes.coords_to_point(i, reward) for i, reward in enumerate(self.rewards)] rewards_line = manim.VMobject() rewards_line.set_points_smoothly(rewards_points[:1]) # Add the 3D mesh first current_mesh = self.get_mesh_from_airplane(self.wings[0]).move_to([0, 0, 1]) self.add(current_mesh) # Add the 2D elements as fixed in frame self.add_fixed_in_frame_mobjects(rewards_group, rewards_line) self.wait(1) # Animate through each airplane and update rewards plot for i, (airplane, reward) in enumerate(zip(self.wings[1:], self.rewards[1:]), 1): new_mesh = self.get_mesh_from_airplane(airplane).move_to([0, 0, 1]) # Create the next segment of the rewards line new_line = manim.VMobject() new_line.set_points_smoothly(rewards_points[:i+1]) new_line.set_color(manim.BLUE) # Match the wing color # Add the new line as fixed in frame self.remove(rewards_line) self.add_fixed_in_frame_mobjects(new_line) # Animate both the mesh transformation and the rewards line update self.play( manim.ReplacementTransform(current_mesh, new_mesh), manim.ReplacementTransform(rewards_line, new_line), run_time=self.run_time_per_update, ) current_mesh = new_mesh rewards_line = new_line self.wait(1) # Camera rotation (only affects the 3D part) self.move_camera(theta=(2 + 7/6) * manim.PI, run_time=7, rate_func=manim.linear) self.wait(1) except:
[docs] class AirfoilEvolutionAnimation: """ Generate an animation showing the evolution of airfoils and their corresponding lift-to-drag ratios. Manim is required to run this animation, and it is recommended to install it with `conda install -c conda-forge manim`. Properties: - `airfoils`: List of airfoil objects representing the evolution. - `rewards`: List of lift-to-drag ratios corresponding to each airfoil. - `run_time`: Duration of the animation in seconds. Default: ``5``. - `title`: Title of the animation. Default: ``"Airfoil evolution"``. Examples: To use in a notebook: >>> import manim >>> from pyLOM.RL import AirfoilEvolutionAnimation >>> %%manim -qm -v WARNING AirfoilEvolution >>> AirfoilEvolutionAnimation.airfoils = airfoils >>> AirfoilEvolutionAnimation.rewards = rewards >>> AirfoilEvolutionAnimation.title = "NACA0012 Evolution" To use it in a script: >>> from pyLOM.RL import AirfoilEvolutionAnimation >>> from manim import * >>> from main import config >>> # config.format = "gif" # Change output format to GIF >>> config.output_file = "airfoil_evolution.mp4" # Change output file name >>> config.quality = "production_quality" # Set quality to maximum (Full HD) >>> animation = AirfoilEvolutionAnimation() >>> animation.airfoils = airfoils >>> animation.rewards = rewards >>> animation.title = "Airfoil Evolution" >>> animation.render() """ def __init__(self, *args, **kwargs): raiseWarning( "AirfoilEvolutionAnimation requires manim to be installed." "Please, install with: conda install -c conda-forge manim", False )
[docs] class WingEvolutionAnimation: """ Generate an animation showing the evolution of wings and their corresponding lift-to-drag ratios. Manim is required to run this animation, and it is recommended to install it with `conda install -c conda-forge manim`. Properties: - `wings`: List of wing objects representing the evolution. - `rewards`: List of lift-to-drag ratios corresponding to each airplane. - `run_time_per_update`: Duration of each update in seconds. Default: ``0.25``. Examples: To use in a notebook: >>> import manim >>> from pyLOM.RL import WingEvolutionAnimation on a separete cell, define the wings and rewards: >>> %%manim -qm -v WARNING AirfoilEvolution >>> WingEvolutionAnimation.wings = wings >>> WingEvolutionAnimation.rewards = rewards >>> WingEvolutionAnimation.run_time_per_update = 0.1 """ def __init__(self, *args, **kwargs): raiseWarning( "WingEvolutionAnimation requires manim to be installed." "Please, install with: conda install -c conda-forge manim", False )