Skip to content

Commit 596561d

Browse files
authored
Update Rocket.draw() to visualize ClusterMotor configurations
1. **Import `ClusterMotor`:** * The file now imports `ClusterMotor` (Line 5) to check the motor type. 2. **Refactored `_draw_motor` Method (Line 234):** * This method is completely refactored. It now acts as a dispatcher, calling the new `_generate_motor_patches` helper function. * It checks `isinstance(self.rocket.motor, ClusterMotor)`. * If it's a cluster, it adds all patches generated by the helper. * If it's a simple motor, it uses the original logic (drawing the nozzle and chamber centered at y=0). * It also correctly calls `_draw_nozzle_tube` to connect the airframe to the start of the cluster or single motor. 3. **New `_generate_motor_patches` Method (Line 259):** * This is an **entirely new** helper function. * It contains the core logic for drawing clusters. * It iterates through `cluster.motors` and `cluster.positions`. * For each sub-motor, it correctly calculates the 2D plot offset (using `sub_pos[0]` for the 'xz' plane or `sub_pos[1]` for 'yz') and the longitudinal position (`sub_pos[2]`). * It re-uses the plotting logic of the individual sub-motors (e.g., `_generate_combustion_chamber`, `_generate_grains`) but applies the correct `translate=(pos_z, offset)` transform. * This allows multiple motors to be drawn side-by-side. 4. **Fix `_draw_center_of_mass_and_pressure` (Line 389):** * This method is updated to handle the new 3D `Vector` object returned by `self.rocket.center_of_mass(0)`. * It now accesses the Z-coordinate correctly using `cm_z = float(cm_vector.z.real)` instead of assuming the return value is a simple float.
1 parent 20992f0 commit 596561d

File tree

1 file changed

+145
-41
lines changed

1 file changed

+145
-41
lines changed

rocketpy/plots/rocket_plots.py

Lines changed: 145 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import numpy as np
33

44
from rocketpy.motors import EmptyMotor, HybridMotor, LiquidMotor, SolidMotor
5+
from rocketpy.motors.ClusterMotor import ClusterMotor
56
from rocketpy.rocket.aero_surface import Fins, NoseCone, Tail
67
from rocketpy.rocket.aero_surface.generic_surface import GenericSurface
78

@@ -201,7 +202,7 @@ def draw(self, vis_args=None, plane="xz", *, filename=None):
201202

202203
drawn_surfaces = self._draw_aerodynamic_surfaces(ax, vis_args, plane)
203204
last_radius, last_x = self._draw_tubes(ax, drawn_surfaces, vis_args)
204-
self._draw_motor(last_radius, last_x, ax, vis_args)
205+
self._draw_motor(last_radius, last_x, ax, vis_args, plane)
205206
self._draw_rail_buttons(ax, vis_args)
206207
self._draw_center_of_mass_and_pressure(ax)
207208
self._draw_sensors(ax, self.rocket.sensors, plane)
@@ -410,45 +411,141 @@ def _draw_tubes(self, ax, drawn_surfaces, vis_args):
410411
)
411412
return radius, last_x
412413

413-
def _draw_motor(self, last_radius, last_x, ax, vis_args):
414+
def _draw_motor(self, last_radius, last_x, ax, vis_args, plane="xz"):
414415
"""Draws the motor from motor patches"""
415-
total_csys = self.rocket._csys * self.rocket.motor._csys
416-
nozzle_position = (
417-
self.rocket.motor_position + self.rocket.motor.nozzle_position * total_csys
418-
)
416+
417+
# Obtenir les patches (logique cluster/simple est dans la fonction ci-dessous)
418+
motor_patches = self._generate_motor_patches(ax, plane)
419419

420-
# Get motor patches translated to the correct position
421-
motor_patches = self._generate_motor_patches(total_csys, ax)
422-
423-
# Draw patches
420+
# Dessiner les patches
424421
if not isinstance(self.rocket.motor, EmptyMotor):
425-
# Add nozzle last so it is in front of the other patches
426-
nozzle = self.rocket.motor.plots._generate_nozzle(
427-
translate=(nozzle_position, 0), csys=self.rocket._csys
428-
)
429-
motor_patches += [nozzle]
430-
431-
outline = self.rocket.motor.plots._generate_motor_region(
432-
list_of_patches=motor_patches
433-
)
434-
# add outline first so it is behind the other patches
435-
ax.add_patch(outline)
436-
for patch in motor_patches:
437-
ax.add_patch(patch)
422+
423+
if isinstance(self.rocket.motor, ClusterMotor):
424+
# Logique pour Cluster
425+
for patch in motor_patches:
426+
ax.add_patch(patch)
427+
428+
# Raccorder le tube au début (point z min) du cluster
429+
if self.rocket.motor.positions:
430+
# Trouve le z le plus proche de la coiffe
431+
cluster_z_positions = [pos[2] for pos in self.rocket.motor.positions]
432+
z_connector = min(cluster_z_positions) if self.rocket._csys == 1 else max(cluster_z_positions)
433+
self._draw_nozzle_tube(last_radius, last_x, z_connector, ax, vis_args)
434+
435+
else:
436+
# Logique originale pour Moteur Simple
437+
total_csys = self.rocket._csys * self.rocket.motor._csys
438+
nozzle_position = (
439+
self.rocket.motor_position + self.rocket.motor.nozzle_position * total_csys
440+
)
441+
442+
# Ajouter la tuyère (logique originale)
443+
nozzle = self.rocket.motor.plots._generate_nozzle(
444+
translate=(nozzle_position, 0), csys=self.rocket._csys
445+
)
446+
motor_patches += [nozzle]
438447

439-
self._draw_nozzle_tube(last_radius, last_x, nozzle_position, ax, vis_args)
448+
outline = self.rocket.motor.plots._generate_motor_region(
449+
list_of_patches=motor_patches
450+
)
451+
# Ajouter l'outline en premier
452+
ax.add_patch(outline)
453+
for patch in motor_patches:
454+
ax.add_patch(patch)
440455

441-
def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-argument
456+
self._draw_nozzle_tube(last_radius, last_x, nozzle_position, ax, vis_args)
457+
def _generate_motor_patches(self, ax, plane="xz"):
442458
"""Generates motor patches for drawing"""
443459
motor_patches = []
460+
total_csys = self.rocket._csys * self.rocket.motor._csys
444461

445-
if isinstance(self.rocket.motor, SolidMotor):
462+
# ################################################
463+
# ## LOGIQUE D'AFFICHAGE POUR CLUSTER DE MOTEURS ##
464+
# ################################################
465+
if isinstance(self.rocket.motor, ClusterMotor):
466+
cluster = self.rocket.motor
467+
all_sub_patches = [] # Pour l'outline global
468+
469+
for sub_motor, sub_pos in zip(cluster.motors, cluster.positions):
470+
# sub_pos est [x, y, z]
471+
motor_longitudinal_pos = sub_pos[2] # Position Z du moteur
472+
473+
# Déterminer le décalage (offset)
474+
offset = 0
475+
if plane == "xz":
476+
offset = sub_pos[0] # Coordonnée X
477+
elif plane == "yz":
478+
offset = sub_pos[1] # Coordonnée Y
479+
480+
# `sub_total_csys` convertit un déplacement relatif au moteur
481+
# en un déplacement relatif à la fusée.
482+
sub_total_csys = self.rocket._csys * sub_motor._csys
483+
484+
# On réutilise la logique de SolidMotor, mais avec un offset
485+
if isinstance(sub_motor, SolidMotor):
486+
current_motor_patches = [] # Patches pour CE moteur
487+
488+
# Position Z du centre de masse des grains DANS LE REPERE FUSÉE
489+
grains_cm_pos = (
490+
motor_longitudinal_pos
491+
+ sub_motor.grains_center_of_mass_position * sub_total_csys
492+
)
493+
ax.scatter(
494+
grains_cm_pos.real, # Utiliser .real pour éviter ComplexWarning
495+
offset,
496+
color="brown",
497+
label=f"Grains CM ({sub_pos[0]:.2f}, {sub_pos[1]:.2f})",
498+
s=8,
499+
zorder=10,
500+
)
501+
502+
# `translate` prend (pos_z, pos_y_offset)
503+
# Ces fonctions utilisent le _csys interne du sub_motor (qui est 1)
504+
chamber = sub_motor.plots._generate_combustion_chamber(
505+
translate=(grains_cm_pos.real, offset), label=None
506+
)
507+
grains = sub_motor.plots._generate_grains(
508+
translate=(grains_cm_pos.real, offset)
509+
)
510+
511+
# Position Z de la tuyère DANS LE REPERE FUSÉE
512+
nozzle_position = (
513+
motor_longitudinal_pos
514+
+ sub_motor.nozzle_position * sub_total_csys
515+
)
516+
517+
# ***** CORRECTION ICI *****
518+
# On ne passe PAS de 'csys' !
519+
# On laisse la fonction _generate_nozzle utiliser son
520+
# propre _csys interne (qui est 1, tail_to_nose),
521+
# car 'nozzle_position' est déjà la coordonnée absolue.
522+
nozzle = sub_motor.plots._generate_nozzle(
523+
translate=(nozzle_position.real, offset)
524+
)
525+
# **************************
526+
527+
current_motor_patches += [chamber, *grains, nozzle]
528+
all_sub_patches.extend(current_motor_patches)
529+
530+
# Créer un outline global pour tous les moteurs
531+
if all_sub_patches:
532+
# Utiliser .plots du premier moteur pour la méthode
533+
outline = self.rocket.motor.motors[0].plots._generate_motor_region(
534+
list_of_patches=all_sub_patches
535+
)
536+
motor_patches.append(outline) # Mettre l'outline en premier
537+
motor_patches.extend(all_sub_patches)
538+
539+
# #####################################
540+
# ## LOGIQUE ORIGINALE (MOTEUR SIMPLE) ##
541+
# #####################################
542+
elif isinstance(self.rocket.motor, SolidMotor):
446543
grains_cm_position = (
447544
self.rocket.motor_position
448545
+ self.rocket.motor.grains_center_of_mass_position * total_csys
449546
)
450547
ax.scatter(
451-
grains_cm_position,
548+
grains_cm_position.real, # .real
452549
0,
453550
color="brown",
454551
label="Grains Center of Mass",
@@ -457,10 +554,10 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
457554
)
458555

459556
chamber = self.rocket.motor.plots._generate_combustion_chamber(
460-
translate=(grains_cm_position, 0), label=None
557+
translate=(grains_cm_position.real, 0), label=None # .real
461558
)
462559
grains = self.rocket.motor.plots._generate_grains(
463-
translate=(grains_cm_position, 0)
560+
translate=(grains_cm_position.real, 0) # .real
464561
)
465562

466563
motor_patches += [chamber, *grains]
@@ -471,7 +568,7 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
471568
+ self.rocket.motor.grains_center_of_mass_position * total_csys
472569
)
473570
ax.scatter(
474-
grains_cm_position,
571+
grains_cm_position.real, # .real
475572
0,
476573
color="brown",
477574
label="Grains Center of Mass",
@@ -483,16 +580,16 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
483580
translate=(self.rocket.motor_position, 0), csys=total_csys
484581
)
485582
chamber = self.rocket.motor.plots._generate_combustion_chamber(
486-
translate=(grains_cm_position, 0), label=None
583+
translate=(grains_cm_position.real, 0), label=None # .real
487584
)
488585
grains = self.rocket.motor.plots._generate_grains(
489-
translate=(grains_cm_position, 0)
586+
translate=(grains_cm_position.real, 0) # .real
490587
)
491588
motor_patches += [chamber, *grains]
492589
for tank, center in tanks_and_centers:
493590
ax.scatter(
494-
center[0],
495-
center[1],
591+
center[0].real, # .real
592+
center[1].real, # .real
496593
color="black",
497594
alpha=0.2,
498595
s=5,
@@ -506,8 +603,8 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
506603
)
507604
for tank, center in tanks_and_centers:
508605
ax.scatter(
509-
center[0],
510-
center[1],
606+
center[0].real, # .real
607+
center[1].real, # .real
511608
color="black",
512609
alpha=0.2,
513610
s=4,
@@ -576,12 +673,19 @@ def _draw_rail_buttons(self, ax, vis_args):
576673
def _draw_center_of_mass_and_pressure(self, ax):
577674
"""Draws the center of mass and center of pressure of the rocket."""
578675
# Draw center of mass and center of pressure
579-
cm = self.rocket.center_of_mass(0)
580-
ax.scatter(cm, 0, color="#1565c0", label="Center of Mass", s=10)
581-
582-
cp = self.rocket.cp_position(0)
676+
677+
# MODIFICATION 1: Récupérer le vecteur CM et extraire .z
678+
cm_vector = self.rocket.center_of_mass(0)
679+
# On prend la partie réelle pour éviter les warnings
680+
cm_z = float(cm_vector.z.real)
681+
682+
# On suppose que le CM est sur l'axe pour le dessin 2D
683+
ax.scatter(cm_z, 0, color="#1565c0", label="Center of Mass", s=10)
684+
685+
# MODIFICATION 2: Prendre la partie réelle du CP aussi
686+
cp_z = float(self.rocket.cp_position(0).real)
583687
ax.scatter(
584-
cp, 0, label="Static Center of Pressure", color="red", s=10, zorder=10
688+
cp_z, 0, label="Static Center of Pressure", color="red", s=10, zorder=10
585689
)
586690

587691
def _draw_sensors(self, ax, sensors, plane):

0 commit comments

Comments
 (0)