41. Journal Viewer

Note

The Journal viewer is only available under Windows operating system.

Journal Viewer is a Graphical User Interface (GUI) that allows you to play and post-process journals, as shown in Fig. 41.1.

../_images/journal_viewer_interface_curved_conveyor_two_lanes.png

Fig. 41.1 The interface of the Journal Viewer. A journal from a simulation of a curved two-lane conveyor transporting bottles is loaded.

In AGX Dynamics (and Algoryx Momentum), simulation data can be stored in an .agxJournal file. This simulation data file, known as a journal, is a recording of the simulation.

By playing the journal with the Journal Viewer, the saved simulation is displayed similarly to a video, but unlike a traditional video that consists of image frames, this playback uses simulation data, such as object positions, velocities, and other properties. This allows for dynamic viewing from any angle, zooming in or out, and offers post-processing capabilities such as coloring particles based on scalar quantities (e.g., velocity), and much more. Additionally, custom scripts can be appended for tailored post-processing.

In this section, we will describe how to find and use Journal Viewer, including how to load and play a journal, as well as its post-processing features. First, we will describe the key functions and features. Following this, we will detail the specific menu items, explaining how each one contributes to the overall functionality. Lastly, we’ll describe how to create and use custom scripts for tailored post-processing.

41.1. Save Journal

In AGX Dynamics (and Algoryx Momentum), simulation data can be stored in an .agxJournal, which can later be played using the Journal Viewer. This file, referred to as a journal, contains a detailed recording of the simulation, including various data points (object positions, velocities, etc.). To save a journal using AGX Dynamics, you can use agxViewer (see agxViewer). To save a journal, open your terminal, set up your AGX environment, and run the following command:

>> agxviewer <agxScenePath> --journalRecord

where <agxScenePath> is the path to the AGX scene file, which can be either relative to the current directory or an absolute path. This saves the journal to the default journal filename Journal.agxJournal.

For example, to save a journal for the scene located at D:/agx_scenes/excavator_E85_terrain.agxPy, run this command from any directory:

>> agxviewer D:/agx_scenes/excavator_E85_terrain.agxPy --journalRecord

Alternatively, if you are already in the directory D:/agx_scenes/, you can simplify the command:

>> agxviewer excavator_E85_terrain.agxPy --journalRecord

To save the journal to a custom path, either relative to the current directory or an absolute path, use the argument --journalRecordPath followed by the custom path. For example:

>> agxviewer excavator_E85_terrain.agxPy --journalRecord --journalRecordPath excavator_E85_terrain.agxJournal

To explore more options related to journal recording, use the following command:

>> agxviewer --help

41.2. Launch Journal Viewer

The Journal Viewer is located in .../bin/ARCH/, where ARCH corresponds to your system’s architecture. For example, if you are using Windows with a 64-bit architecture, the location would be .../bin/x64/journalViewer.exe.

Additionally, the Journal Viewer can be accessed from the terminal or shell from any folder, similar to how agxViewer is accessed. To do this, open your terminal, set up you AGX environment, type journalViewer, and press Enter. The interface of the Journal Viewer will appear, as shown in Fig. 41.2.

../_images/journal_viewer_launch_journal_viewer_interface_no_journal_loaded.png

Fig. 41.2 The interface of the Journal Viewer with no journal loaded.

To launch the Journal Viewer and simultanously load a journal, refer to Load journal.

41.3. Load journal

To load a journal, navigate to the main menu (Main > Open File) and select the desired journal file. Once a journal is loaded, the interface will display the simulation, as shown in Fig. 41.3.

../_images/journal_viewer_launch_journal_viewer_interface_E85_excavator.png

Fig. 41.3 A loaded journal of a scene with an E85 excavator.

You can also launch the Journal Viewer and simultaneously load a journal directly from the terminal. To do this, use the following command:

>> journalviewer <journalPath>

where <journalPath> is the path to the journal, which can be either relative to the current directory or an absolute path.

For example, to load the journal located at D:/journals/excavator_E85_terrain.agxJournal, you would run the following command from any directory:

>> journalviewer D:/journals/excavator_E85_terrain.agxJournal

Alternatively, if you are already in the directory D:/journals/, you can simply use:

>> journalviewer excavator_E85_terrain.agxJournal

41.4. Playback

The playback panel of the Journal Viewer, see Fig. 41.4, functions similarly to the controls of a traditional video player.

../_images/journal_viewer_playback_panel.png

Fig. 41.4 The playback panel.

Table 41.1 describes the buttons and items found on the playback panel.

Table 41.1 Playback Panel Items

ITEM

DESCRIPTION

Play/Pause

Starts the playback of the journal. If playback is active, the play button changes to a pause button.

Stop

Stops playback and returns to the beginning of the journal.

Previous Frame

Steps backward by N simulation frames, where N is the current frame stride.

Next Frame

Steps forward by N simulation frames.

Decrease Frame Stride

Reduces the frame stride. Frame stride refers to the number of simulation frames skipped at each playback step. For example, if the frame stride is set to 4, every 4th frame is displayed during playback.

Increase Frame Stride

Increases the frame stride.

Current Frame Stride

Displays the current frame stride value.

Current Time

Shows the current playback time.

End Time

Displays the total duration of the journal.

Loop Journal

When enabled, the journal replays continuously after reaching the end.

41.5. Adjusting the viewing window

The viewing window is the area where the simulation and its objects are displayed. Users can adjust the view using the mouse, keyboard, or specific settings. In this section, we describe the available mouse and keyboard commands for interacting with the viewing window.

Table 41.2 lists the mouse commands for manipulating the viewing window.

Table 41.2 Mouse commands for the viewing window.

MOUSE BUTTON

ACTION

Left

Zoom in or out by moving the mouse up or down

Scroll wheel

Pan the view horizontally or vertically by moving the mouse in any direction.

Right

Rotate the object by moving the cursor in any direction.

Table 41.3 lists the keyboard commands for manipulating the viewing window.

Table 41.3 Keyboard commands for the viewing window.

KEY

ACTION

space

Reset and center view to display all objects.

b

Draw statistics (time, timestep, etc.).

+

Increment the clip plane position in the direction of the clip plane normal.

-

Decrement the clip plane position in the direction of the clip plane normal.

p

Toggle the left side docket (containing Analysis and Coloring).

Ctrl+1 to Ctrl+6

Set the view to the positive/negative direction of a coordinate axis. 1: X, 2: -X, 3: Y, 4: -Y, 5: Z, 6: -Z.

Ctrl+7

Set the view to the normal direction of the clip plane.

Ctrl++

Zoom in with Orthographic View (if orthographic mode is enabled).

Ctrl+-

Zoom out with Orthographic View (if orthographic mode is enabled).

J

Open a Camera File.

K

Save Cameras. The currently stored camera views are saved to an .cfg file.

H

Show Views. Opens a panel with the currently stored camera views.

M

Store Current Camera.

PgUp

Next Camera. Selects the next camera view stored.

PgDown

Previous Camera. Selects the previous camera view stored.

G

Debug Rendering.

O

Standard Rendering.

Home

Mesh Rendering > Collision Mesh.

Ins

Mesh Rendering > Render Mesh

F12

Take screenshot. The image is saved to the directory of the journal.

e

Plays/pauses playback.

l

Sets the light source in the direction of the current view.

41.6. Cameras

The Cameras menu allows you to store different camera views, either temporarily or permanently in a file, making it easy to switch between saved views. To save a camera view, first adjust the viewing window to your desired position (refer to Adjusting the viewing window for details). Then, select Store Current Camera (shortcut: M) to add the current view to an internal list. You can repeat this process by adjusting the viewing window and selecting Store Current Camera again to save multiple views.

To view the list of saved camera views, select Show Views, which will display the list on the right side of the screen. To navigate between stored views, use Next Camera (PgUp) to cycle forward through the list or Previous Camera (PgDown) to go back.

To save your list of camera views to a file, select Save Cameras (shortcut: K). This will open a file dialog where you can save the views as a .cfg file in a folder of your choice. To load a previously saved camera views file, select Open Camera File (shortcut: J) and choose the desired file.

Note

If you save a camera file in the same folder as a journal with the same name as the journal (except for the file extension), then, by loading the journal, the camera file will automatically be loaded.

41.7. Media

The Media menu offers options to save a video file of the journal and capture screenshots.

To save a video, start by loading the desired journal and adjusting the size of the viewing window. Keep in mind that the window size directly affects the video’s resolution; for instance, a full-screen viewer will result in a larger video than a smaller window. Next, select Video Generation Settings (shortcut: V), which opens the window shown in Fig. 41.5.

../_images/journal_viewer_media_video_generation_settings.png

Fig. 41.5 The Video Generation Settings window.

In the settings window, start by clicking Set Output to choose the filename and directory for the video. You can adjust the video quality using the quality slider. FPS determines the Frames Per Second of the video. The journal’s current FPS is displayed to the right. You’ll also set the Real Time Factor (RTF), which controls the playback speed relative to real time:

  • RTF = 1. The video will play in real time. A 10-second simulation will result in a 10-second video.

  • RTF > 1. The video will play faster than real time. For example, with an RTF of 2, a 10-second simulation will produce a 5-second video.

  • RTF < 1. The video will play slower than real time. For instance, with an RTF of 0.5, a 10-second simulation will produce a 20-second video.

Once the settings are configured, select Close.

To begin recording, ensure the journal is paused, then select Capture Video (shortcut: C) from the Media menu. A red “Recording Video” message will appear above the playback panel. As the simulation progresses (either by playing or advancing frame by frame), the content shown in the main window will be recorded. You can pause and resume recording as needed, and adjust the camera view during the recording. Be careful not to use the Stop button, as this will reset the playback. Once you have recorded the desired content, pause the video, return to Video Generation Settings, and select Finalize Video to save the recording.

To take a screenshot, select Take Screenshot (shortcut: F12). This captures an image of the current frame and saves it to the journal’s directory. Similar to video generation, the screenshot will reflect the current size of the viewing window.

41.8. Analysis (tab)

The Analysis tab located in the left side docket is shown in Fig. 41.6. It provides a range of features designed to enhance the visualization and exploration of scenes, and carry out measurements and analyzes. It allows users to isolate specific regions, manipulate views, and obtain detailed data about objects or areas of interest. The features are described in the section below.

../_images/journal_viewer_analysis.png

Fig. 41.6 The analysis tab.

41.8.1. Analysis Bound

The Analysis Bound function is available within the Analysis tab, as shown in Fig. 41.7.

../_images/journal_viewer_analysis_analysis_bound_panel.png

Fig. 41.7 The Analysis Bound Tab.

This feature allows users to define a rectangular region in the scene with customizable size, position, and orientation. Enable Analysis Bound activates or deactivates this region, while Enable Rendering toggles the visual display of the Analysis Bound, shown as a blue rectangle. Once enabled, statistical data for the defined region are displayed in the Contents panel. An example with and without the Analysis Bound is illustrated in Fig. 41.8.

../_images/journal_viewer_analysis_analysis_bound_excavator_scenes.png

Fig. 41.8 A scene with a CAT365 excavator about to dig. (a) No analysis bound enabled. (b) Analysis bound enabled.

The settings and data for Fig. 41.8 are shown in Fig. 41.9.

../_images/journal_viewer_analysis_analysis_bound_excavator_panels.png

Fig. 41.9 The analysis bound settings for Fig. 41.8. (a) No analysis bound enabled. (b) Analysis bound enabled.

In this example, the Analysis Bound has been configured to match the excavator’s digging area. By analyzing the Contents panel in Fig. 41.9 (b), we can derive useful data such as the number of particles (246946) and the bulk density (1509 kg/m³) within the defined region.

41.8.2. Clip Plane

The Clip Plane function is available in the Analysis tab, as shown in Fig. 41.10.

../_images/journal_viewer_analysis_clip_plane_panel.png

Fig. 41.10 The Clip Plane panel.

This feature introduces an xz-plane that slices through the scene, rendering all objects with negative y-coordinates invisible, effectively “clipping” those portions. The position and orientation of the Clip Plane can be adjusted by changing its center or rotating it, allowing users to clip the scene in any desired configuration. To active the feature, check Use Clip Plane. To activate the visual display of the Clip Plane, which appears as a red rectangle with diagonal lines, check Render Clip Plane. The point where these lines intersect represents the center of the Clip Plane, aiding in precise positioning and orientation.

A usage example is shown in Fig. 41.11.

../_images/journal_viewer_analysis_clip_plane_excavator.png

Fig. 41.11 An CAT365 excavator in the process of digging. Particle coloring by velocity. Particle color interval: [0, 1] m/s. (a) Clip Plane not enabled. (b) Clip Plane enabled and positioned to split the scene along the length of the digging area. (c) Clip Plane enabled, centered at the bucket, and rotated 30 degrees about the vertical axis.

In Fig. 41.11 (a), the soil dynamics are obscured by particles and the excavator bucket, demonstrating the need for the Clip Plane. In contrast, Fig. 41.11 (b) reveals the dynamics within the soil and inside the bucket. Finally, Fig. 41.11 (c) shows the effect of rotating the Clip Plane for a different view angle.

41.8.3. Measurement Axes

The Measurement Axes function can be accessed from the Analysis tab, as shown in Fig. 41.12.

../_images/journal_viewer_analysis_measurement_axes_panel.png

Fig. 41.12 The Measurement Axes panel.

This feature provides a customizable coordinate system that can be moved, rotated, and customized.

To activate the coordinate system, check Show Axes, which displays a red x-axis, a green y-axis, and a blue z-axis. Individual axes can be shown or hidden by toggling Show X, Show Y, or Show Z. The coordinate system can be repositioned by adjusting the Center (m) values for x, y, and z, with the default position at (0,0,0) representing the scene’s origin. The axes length and line thickness can be adjusted using Axes Length (m) and Line Size (px), respectively. Additionally, you can change the default colors to a single one using Enable Axes Single Color, which is helpful when the default colors blend into the scene.

Enabling Show Ticks adds tick markers to the axes, and the number and length of these ticks can be adjusted via Num Ticks and Length (m). To display grids, check Enable Grid, and customize tick colors using Tick Color. You can also show tick numbers by enabling Show Tick Numbers, with options to adjust the font size (Text Size) and change how often numbers appear by specifying a Text Tick Stride. The default stride value of 1 labels every tick, while higher values (e.g., 2 or more) reduce the frequency of numbered ticks.

An example of Measurement Axes usage is provided in Fig. 41.13.

../_images/journal_viewer_analysis_measurement_axes_recirculation_conveyor.png

Fig. 41.13 A recirculation conveyor with curved walls transporting barrels. (a) Measurment axes disabled. (b) Measurment axes enabled. Axes length: 1.5 m. Line size: 4px. (c) Close-up view of (b). (d) Same as (c), but with Show Ticks and Enable Grid checked, and Num Ticks set to 3.

41.9. Coloring (tab)

The Journal Viewer provides a variety of tools for coloring and post-processing granular particles, accessible through the Coloring tab in the left side docket. This tab contains two sub-tabs: Particle Color and Particle Primitives. Below is a brief overview of each:

  • Particle Color: Allows particles to be colored based on scalar values such as velocity, displacement, or radius.

  • Particle Primitives: Provides visualization of particle dynamics using trajectories and contact networks.

These two sub-tabs are described in the following sections.

41.9.1. Particle Color

The Particle Color tab is displayed in Fig. 41.14.

../_images/journal_viewer_coloring_particle_color_tab.png

Fig. 41.14 The particle color tab.

The central feature of this tab is to color particles based on scalar quantities such as velocity, displacement, or radius.

The Alpha value, found under Alpha Settings, controls the opacity of particles when Alpha Sprites are enabled (see Particle Shader).

The dropdown list titled Scalar coloring provides a range of scalar quantities for coloring. If Limit to Analysis Bound is checked, only particles within the analysis bound will be colored. The colormap is defined by specifying minimum (Min) and maximum (Max) values. Particles at or below the minimum are colored blue, while those at or above the maximum are colored red. Particles with intermediate values will transition smoothly through a gradient of colors, as shown in Fig. 41.15.

The Particle Render Filter applies a threshold to the coloring, meaning only particles exceeding a selected quantity will be colored. For an example, refer to Velocity.

In the Legend Settings, the color legend, which reflects the Min-Max color interval, can be customized in terms of size, orientation (horizontal or vertical), and placement. An example legend for displacement, ranging from -2 to 4 m, is shown in Fig. 41.15.

../_images/journal_viewer_coloring_particle_color_displacement_legend.png

Fig. 41.15 An example legend for displacement. Interval: [-2, 4] m.

41.9.1.1. Contact Force

This feature colors particles based on the magnitude of the contact forces acting on them. Visualizing particles in this way helps identify areas of high stress or compression, which is useful for analyzing how forces are distributed in granular systems. For instance, it can highlight potential breakage points in materials such as powders or soils under pressure. To use this feature, select Contact Force from the Scalar coloring dropdown. An example is shown in Fig. 41.16.

../_images/journal_viewer_coloring_particle_color_contact_force.png

Fig. 41.16 A tractor tyre on a bed of particles. Contact force coloring interval: [0, 100] Nm.

In the example (Fig. 41.16), the highest contact forces are concentrated beneath the tire and spread outward from the tire-soil interface. This pattern is characteristic of granular materials like soil, where forces are transmitted through networks of interconnected force chains. These force chains distribute the load unevenly, creating regions of high stress extending from the point of contact.

41.9.1.2. Displacement

Coloring particles by displacement reveals how far they have moved from their initial positions, making it useful for identifying deformation patterns or flow paths over time. This feature is particularly valuable for studying pile formations, static regions, or other processes involving particle rearrangement.

To use this feature, first navigate to the desired time in the simulation. Then, select Displacement from the Scalar coloring dropdown, which establishes the reference positions of the particles at that specific moment. Next, set the desired coloring interval. As the journal progresses, particles will be colored according to how far they have moved from their reference positions, representing their displacement. An example is shown in Fig. 41.17.

../_images/journal_viewer_coloring_particle_color_displacement.png

Fig. 41.17 A tall silo with an outlet. Displacement coloring interval: [0, 2] m. (a) The reference time at which the outlet is opened; (b) 1.5 s after the outlet has opened; (c) 4 s after the outlet has opened.

In Fig. 41.17 (a), all particles are blue, indicating a displacement of 0 m, as this is the reference time. In Fig. 41.17 (b), 1.5 seconds later, displacement is concentrated in a semi-elliptic region aligned with the outlet, suggesting this area experiences the most flow. By Fig. 41.17 (c), 4 seconds after opening the outlet, the displacement has expanded, forming a red cylinder through the outlet, extending halfway up the silo. Above this region, a gradient of orange, yellow, and green indicates decreasing displacement, showing non-uniform flow. Meanwhile, particles near the bottom walls remain blue, indicating they are static.

41.9.1.3. Height X (Y, Z)

Coloring particles based on their height along a specific axis provides useful insights into the spatial distribution and layering of particles. It can also help identify particles that exceed a critical height, such as in a silo scenario. To use this feature, select Height X, Height Y, or Height Z from the Scalar coloring dropdown. An example is shown in Fig. 41.18.

../_images/journal_viewer_coloring_particle_color_height_y.png

Fig. 41.18 A tractor driving on a bed of particles. Height (Y) coloring interval: [1.9, 2.5] m.

In Fig. 41.18, particles are colored by their height along the Y axis, as gravity acts in this direction. This height-based coloring provides an intuitive way to differentiate undisturbed soil from areas affected by the tractor’s movement. In this example, the surface of the soil is mostly green, while the tracks left by the tractor are identifiable by their red color.

41.9.1.4. Kinetic Energy

Kinetic energy coloring offers valuable insights into how energy is distributed among particles, helping to track energy dissipation within the system. This is particularly useful for analyzing collisions, identifying areas where energy is concentrated or lost, and optimizing process efficiency. To use this feature, select Kinetic Energy from the Scalar coloring dropdown. An example is shown in Fig. 41.19.

../_images/journal_viewer_coloring_particle_color_kinetic_energy.png

Fig. 41.19 A heavy wall pipe section falling onto a particle bed. Kinetic energy coloring interval: [0, 0.01] J. (a) Prior to impact. (b) About 5 ms after impact. (b) About 25 ms after impact.

We note in Fig. 41.19 (a) that just after impact, the particles just beneath the pipe has absorbed a relatively large amount of kinetic energy. In Fig. 41.19 (b), a short time later, this energy has dissipated and also been transmitted to more distant regions.

In Fig. 41.19 (a), prior to impact, the kinetic energy is essentially zero, as the particles are static. By Fig. 41.19 (b), 5 ms after impact, the particles directly beneath the pipe absorb a significant amount of kinetic energy from the pipe section. By Fig. 41.19 (c), 25 ms after the impact, this energy has begun to dissipate and spread to surrounding regions.

41.9.1.5. Velocity

Coloring particles by velocity helps visualize dynamic regions within the system. This method highlights areas of rapid motion or flow, making it useful for assessing granular flows, detecting eddies, or evaluating mixing efficiency. The coloring is based on the magnitude of the velocity vector. To use this feature, select Velocity from the Scalar coloring dropdown. An example is shown in Fig. 41.20.

../_images/journal_viewer_coloring_particle_color_velocity.png

Fig. 41.20 A rotary drum with pellets modelled as granular particles. Velocity coloring interval: [0, 1] m/s. (a) No filter threshold. (b) Filter threshold: 0.5 m/s.

In Fig. 41.20 (a), the highest velocities are observed on the surface of the material, while the velocity is lowest inside the bulk. In Fig. 41.20 (b), a filter threshold of 0.5 m/s has been applied using the Particle Render Filter, with Velocity selected for filtering. This renders particles below the threshold invisible, effectively isolating regions of faster movement.

41.9.1.6. Radius

This feature colors the particles according to their radii. The radius coloring feature is particularly useful for identifying particle segregation, which is the phenomenon in which granular particles of different sizes separate into distinct regions. This allows users to easily visualize how particles of specific radii migrate and cluster in certain areas of the system over time. To use this feature, select Radius from the Scalar coloring dropdown. An example is shown in Fig. 41.21.

../_images/journal_viewer_coloring_particle_color_radius.png

Fig. 41.21 A rotary drum with pellets modelled as granular particles. Radius coloring interval: [0.005, 0.01] m. Particles of radius 0.005, 0.075, and 0.01 m are blue, green, and red, respectively. (a) The initial state; (b) Shortly after the drum has started to rotate; (c) After 30 seconds of rotation.

In Fig. 41.21 (b), shortly after the drum starts rotating, particles of different radii are more or less uniformly distributed. However, as the drum continues to rotate, Fig. 41.21 (c) shows that smaller particles congregate in the center of the bulk.

41.9.2. Particle Primitives

The Particle Primitives tab, as shown in Fig. 41.22, provides post-processing tools that visualize particle dynamics using lines and curves. The available features are Trajectory Lines and Contact Network, which help illustrate the movement and interactions of particles in the system.

../_images/journal_viewer_coloring_particle_primitives_tab.png

Fig. 41.22 The particle primitives tab.

41.9.2.1. Trajectory Lines

The Trajectory Lines feature analyzes particle motion and visualizes it by drawing trajectory lines that trace the paths of the particles. This feature offers two key settings:

  • Number of positions: This setting defines how many previous particle positions are used to create the trajectory line. A trajectory is constructed by connecting these past positions with straight segments. If set to 2, the trajectory consists of a single straight line. With values of 3 or higher, the lines become more complex, capturing more detailed particle movement.

  • Samples: This setting determines the percentage of particles to include when rendering trajectory lines.

An example is shown in Fig. 41.23.

../_images/journal_viewer_coloring_particle_primitives_trajectory_lines.png

Fig. 41.23 A rotary drum with pellets modelled as granular particles. (a) No particle trajectories rendered. (b) Particle trajectories rendered. Number of positions: 8. Samples: 4%.

41.9.2.2. Contact Network

The Contact Network feature allows users to visualize all contact forces between particles, represented as lines connecting them. It is similar to the Contact Force coloring option (see Contact Force), but it additionally provides a geometric representation of the forces.

To use this feature, enable Render Contact Network in the Contact Network panel and specify the Min Force (N) and Max Force (N) values. These values work similarly to the Min and Max settings used in Scalar coloring. When rendering the Contact Network, it may be helpful to disable Render Particles at the top of the Particle Primitives tab for a clearer view of the network. An example is shown in Fig. 41.24.

../_images/journal_viewer_coloring_particle_primitives_contact_network.png

Fig. 41.24 A tyre on a particle soil bed. (a) Contact Network not rendered. Render Particles enabled. (b) Contact Network rendered. Min Force: 0 N. Max Force: 100 N. Render Particles disabled.

41.11. View (menu)

This menu contains items that influences and changes the settings of the viewing window displaying the playback of the journal.

41.11.1. Draw Mode

The Draw Mode setting allows you to choose the method for rendering the scene. The default option is Polygons, but you can also select Wireframe or Points for different visualization styles, see Fig. 41.25.

../_images/journal_viewer_view_menu_draw_mode.png

Fig. 41.25 A scene featuring an L70 wheel loader rendered in different Draw Modes. (a) Polygons. (b) Wireframe. (c) Points.

41.11.2. Center Scene (Space)

This command adjusts the camera view to ensure all objects in the scene are visible. If the view appears distant, it may be due to objects positioned far away, such as an object that has unintentionally fallen and accelerated downward for a long time due to gravity.

41.11.3. Draw Statistics (B)

This command toggles the display of simulation statistics on the screen, see Fig. 41.26.

../_images/journal_viewer_view_menu_draw_statistics.png

Fig. 41.26 A scene showing a discharging silo with Draw Statistics enabled.

The displayed statistics include details such as simulation time, time step, number of rigid bodies (Num RigidBodies), number of particles (Num Particles), and more. When playing a journal, fewer statistics are shown compared to a running simulation, as not all data is saved during journal recording. If a statistic is unavailable, <Not reported> will be displayed.

41.11.4. Increment Clip Plane (+)

This command moves the Clip Plane forward by a small distance along its normal direction. Since the Clip Plane’s orientation can be adjusted by rotating it, you can control its movement in any desired direction by applying the appropriate rotations.

41.11.5. Decrement Clip Plane (-)

This command moves the Clip Plane backward by a small distance along its normal direction.

41.11.6. Toggle View Side Docket (P)

This command toogles the visibility of the Side Docket (The panel with tabs Analysis and Coloring).

41.11.7. Set Window size

This menu item allows you to specify the size of the Journal Viewer window. It provides an alternative to manually resizing the window by dragging its edges. You can select predefined sizes such as HD, Full HD, or 4K.

41.11.8. Set View

This menu item allows you to specify the direction of the camera in a particular direction: X, Y, Z, -X, -Y, -Z, Clip Plane.

  • If X or is selected, the camera is positioned at (X_0, 0, 0), X_0 > 0, and faces toward the origin in the World (Scene) coordinate system. (Similarly for Y and Z.)

  • If -X is selected, the camera is positioned at (-X_0, 0, 0), X_0 > 0, and faces toward the origin in the World (Scene) coordinate system. (Similarly for Y and Z.)

  • If Clip Plane is selected, the camera is positioned at (0, 0, Z_0), Z_0 > 0, and faces toward the origin in the Clip Plane coordinate system.

41.11.9. Enable Ortographic View

This menu option toggles the Orthographic View on or off. The Journal Viewer supports two types of views: Perspective and Orthographic. When Orthographic View is disabled, Perspective View is automatically enabled.

In Perspective View, the scene is rendered similarly to how the human eye perceives it. Objects appear smaller as they move farther from the camera, with lines converging toward a vanishing point. This creates a sense of depth and spatial relationships, enhancing realism and making it ideal for visualizing how objects interact in 3D space.

On the other hand, Orthographic View displays objects without perspective distortion. In this mode, lines remain parallel, and objects maintain their size regardless of their distance from the camera. This is useful for measurements and for easier visualization of geometrical relationships.

When Orthographic View is enabled, zooming with the mouse is disabled, though you can still rotate the camera using the mouse. Zooming in or out can be done via the keyboard, refer to: Zoom Ortographic View + (Ctrl++).

An example is shown in Fig. 41.27.

../_images/journal_viewer_view_menu_enable_orthographic_view_two_views.png

Fig. 41.27 A vertical conveyor lifting wooden boxes with canisters. (a) Perspective (default) view. (b) Orthographic view.

41.11.10. Zoom Ortographic View + (Ctrl++)

This command zooms in a small distance when Orthographic mode is enabled. When disabled, the command has no effect.

41.11.11. Zoom Ortographic View - (Ctrl-)

This command zooms out a small distance when Orthographic mode is enabled. When disabled, the command has no effect.

41.12. Cameras (menu)

41.12.1. Open Camera File (J)

This command opens a Camera file from a directory of your choice. File extension: .cfg.

41.12.2. Save Cameras (K)

This command saves a Camera file to a directory of your choice. File extension: .cfg.

41.12.3. Show Views (H)

This command display a panel to the right with a list of the views of the camera. If you have not stored any cameras or have not loaded a camera file, the list is empty.

41.12.4. Store Current Camera (M)

This command stores the current camera (view), meaning that is it accessible from the list of views. Note: It does not save the camera views to a file.

41.12.5. Next Camera (PgUp)

This command switches to the next camera view in the camera view list.

41.12.6. Previous Camera (PgDown)

This command switches to the previous camera view in the camera view list.

41.13. Media (menu)

41.13.1. Video Generation Settings (V)

This option opens up the Video Generation Settings window, which is used for the creation of a video, see Media.

41.13.2. Capture Video (C)

This option starts the recording of a video, see Media.

41.13.3. Take Screenshot (F12)

This feature captures a screenshot of the current frame in the viewing window and saves the image to the journal directory. The screenshot reflects the current size of the viewing window, meaning a full-screen viewer will produce a larger image compared to a smaller window.

41.14. Rendering (menu)

41.14.1. Debug Rendering (G)

This option toggles Debug Rendering on or off. For more information, see Debug rendering. A comparison of debug rendering enabled and disabled is shown in Fig. 41.28.

../_images/journal_viewer_rendering_menu_debug_rendering_conveyor_belt_off_on.png

Fig. 41.28 A two-lane conveyor belt transporting bottles. (a) Debug rendering off (standard rendering on). (b) Debug rendering on (standard rendering off).

Although debug rendering is less visually appealing than standard rendering, it is highly useful for inspecting and analyzing the simulation. Examples of what debug rendering reveals include:

  • Meshes: Mesh geometries are represented by straight lines forming triangles.

  • Contacts: Contacts are marked by small orange spheres with lines indicating their direction. See for example the interface between the curved wall and the bottles.

  • Constraints: Constraints are visualized by various objects. For instance, the prismatic constraint governing the dynamics of the actuating orange cylinder is represented by a light brown arrow.

41.14.2. Standard Rendering (O)

This option toggles Standard Rendering on or off. Standard rendering is displayed in Fig. 41.28 (a). If both Standard Rendering and Debug Rendering are disabled, the viewing window will appear empty. When using Debug Rendering, disabling Standard Rendering can improve performance and make it easier to identify simulation objects.

41.14.3. Mesh Rendering (O)

The Mesh Rendering feature includes two options: Collision Mesh and Render Mesh.

  • Collision Mesh toggles the visibility of the collision mesh.

  • Render Mesh toggles the visibility of the render mesh.

To visualize certain elements, such as a Surface Modifier, the collision mesh must be enabled. For an example, refer to Fig. 41.29.

../_images/journal_viewer_rendering_menu_mesh_rendering_collision_mesh_off_on.png

Fig. 41.29 An empty rotary drum with a Surface Modifier. (a) Collision mesh disabled. (b) Collision mesh enabled.

41.14.4. Particle Shader

The Particle Shader provides several options for rendering particles, with three available modes:

  • Sprites: This mode applies a single color to the particles with shading. As a result, any rotation of a particle is not visually detectable.

  • Rotational Sprites: This mode colors the upper and lower hemispheres of a particle in the same color but different shades. This allows the rotation of a particle to be visually detected.

  • Alpha Sprites: Similar to Rotational Sprites, but with the added ability to adjust the alpha (opacity) value. The alpha value can be modified under Coloring > Particle Color > Alpha Setting. A value of 1 represents full opacity, 0 represents full transparency, and intermediate values produce corresponding levels of opacity.

The three options are illustrated in Fig. 41.30.

../_images/journal_viewer_rendering_menu_particle_shader_plough_near.png

Fig. 41.30 Close-up of a plough digging in a particle soil bed, showing different Particle Shader options. (a) Sprites. (b) Rotational Sprites. (c) Alpha Sprites.

A distant view of the scene is shown in Fig. 41.31.

../_images/journal_viewer_rendering_menu_particle_shader_plough_far.png

Fig. 41.31 Far view of a plough digging in a particle soil bed.

41.14.6. Background Color

This option allows you to select the background color for the viewing window. It also applies to video exports.

41.15. Keyshot (menu)

KeyShot is a third-party 3D rendering and animation software and is not included with AGX Dynamics. To utilize KeyShot for rendering journal data, you will need to purchase it separately. BIP is the native file format used by KeyShot, containing all relevant data associated with the model. In KeyShot, you can create 3D renderings or animations using a series of BIP files.

41.15.1. Export Current Frame to Bip

This item exports a (single) BIP file from the current frame to the folder of the journal.

41.15.2. Export Journal To Bip files

This option opens the window shown in Fig. 41.32.

../_images/journal_viewer_keyshot_menu_export_journal_to_bip_exporter.png

Fig. 41.32 The Keyshot Bip Exporter window.

From this window, you can generate a series of Bip files by selecting a Start Time and an End Time. You can also specify the FPS (Frames Per Second). If Time Snapshot is checked, only a single Bip file is generated at the specified Start Time. Clicking Start begins the export process, and the Bip files are saved to the journal’s folder.

41.16. Data Export (menu)

41.16.1. Write Simulation Data to File

This option opens the window shown in Fig. 41.33.

../_images/journal_viewer_data_export_menu_write_simulation_data_to_file_exporter.png

Fig. 41.33 The Contact Information Exporter.

The Contact Information Exporter allows you to export simulation data to a .csv file for use in analysis, plotting, or other programs. The .csv file is saved in the same folder as the journal.

Select Data Type to Export: This panel allows you to choose between two data types: Particle Data and Impact Contact Data. The exported data includes:

  • Particle Data: particleId, radius, pos x, pos y, pos z, accumulatedEnergy

  • Impact Contact Data: ContactType, bodyId1, bodyId2, pos x, pos y, pos z, contactEnergy, normalContactForce, impactForce

Time Settings: This panel lets you specify the time interval for exporting data using Start Time and End Time. For example, if Start Time is set to 0 s and End Time to 10 s, data will be exported for this interval, with a row of data written for each timestep. To export data over a time interval, ensure Time Snapshot is unchecked. If Time Snapshot is checked, End Time is disabled, and only the Start Time can be set, exporting data for a single timestep at the specified Start Time.

Export Data: This panel includes controls for exporting data to a .csv file. Click Start to begin the export process (click Cancel to abort). The progress bar below Waiting shows the progress of the export. The .csv file is saved in the same folder as the journal.

41.17. info (menu)

41.17.1. Journal Info

The Journal Info option provides information about the simulation settings, as shown in Fig. 41.34. This includes details such as the timestep, the number of resting iterations, whether PPGS is used, and other relevant settings. This feature is particularly useful for reviewing the simulation settings, as it serves as a convenient “simulation settings document.” Instead of embedding these details in the journal filename or storing them in a separate text file, Journal Info centralizes the information in one place.

../_images/journal_viewer_info_menu_journal_info_window.png

Fig. 41.34 Journal info for the journal galton_board.agxJournal.

41.18. Custom Scripting

The Journal Viewer allows for the attachment of custom Python scripts, enabling custom post-processing capabilities. To attach a single Python script, use the command line argument --attachScript <filename>. To make all Python modules in a directory available, use the command line argument --addCustomScriptPath <path>. You can view all available command line arguments by executing:

>> journalViewer --help

Examples of using both --attachScript <filename> and --addCustomScriptPath <path> are provided in the sections below.

41.18.1. Attach Script

To attach a custom Python script, my_custom_script.py, to a journal, my_journal.agxJournal, run the following terminal command:

>> journalViewer my_journal.agxJournal --attachScript my_custom_script.py

Let’s illustrate this with an example. Consider the scene shown in Fig. 41.35.

../_images/journal_viewer_custom_scripting_attach_script_drum_w_axes.png

Fig. 41.35 The initial state of a rotating drum with particle pellets. The coordinate system is displayed using Measurement Axes (see Measurement Axes).

To visually inspect the mixing dynamics and efficiency of the pellets in the drum, we can color the particles in two groups: the left half in one color, and the right half in another. We can implement this functionality using a custom script, as described in Code snippet 1 - ParticleColorer.py.

Since the drum is centered at x=0, we can divide the particles based on their position. The script defines a class ParticleColorer, which listens to each simulation time step. After a certain time (TIME_TO_COLOR, set to 4 seconds), the particles are grouped based on their x-coordinate. The IDs of particles in the right half (x >= 0) are stored in one variable, and the IDs of particles in the left half (x <= 0) in another. These variables are assigned once. Then, at each time step, the particles in the right group are colored with one color (COLOR_A, set to purple), and those in the left group with another (COLOR_B, set to yellow).

The complete script is provided in Code snippet 1 - ParticleColorer.py. The journal filename is rotary_drum.agxJournal and we place ParticleColorer.py in the same folder as the journal. To apply the script, execute the following command in the terminal:

>> journalViewer rotary_drum.agxJournal --attachScript ParticleColorer.py

The result is shown in Fig. 41.36.

../_images/journal_viewer_custom_scripting_attach_script_drums_colored.png

Fig. 41.36 The evolution of the granular material with ParticleColorer.py attached. (a) Time: 4 s. (b) Time: 7 s. (c) Time: 10 s. (d) Time: 15 s.

import numpy as np
import agxSDK
import agxPython
from agxPythonModules.utils.numpy_utils import BufferWrapper

TIME_TO_COLOR = 4  # Time to start coloring
COLOR_A = [0.502, 0, 0.502, 1]  # Purple
COLOR_B = [1, 1, 0, 1]  # Yellow

def buildViewerScene() -> None:
    '''Build the viewer scene and add the particle colorer to the simulation.'''
    sim = agxPython.getContext().environment.getSimulation()
    particle_colorer = ParticleColorer(sim)
    sim.add(particle_colorer)

class ParticleColorer(agxSDK.StepEventListener):
    def __init__(self, sim: agxSDK.Simulation) -> None:
        super().__init__()
        self.sim: agxSDK.Simulation = sim
        self.particle_ids_x_geq_0: Optional[np.ndarray] = None
        self.particle_ids_x_lt_0: Optional[np.ndarray] = None

    def post(self, time: float) -> None:
        '''Executed after each simulation step.'''
        if time >= TIME_TO_COLOR:
            if self.particle_ids_x_geq_0 is None or self.particle_ids_x_lt_0 is None:
                self.particle_pos = BufferWrapper(self.sim, 'Particle.position').array
                self.particle_id_to_index = BufferWrapper(self.sim, 'Particle._idToIndex').array
                self.particle_color = BufferWrapper(self.sim, 'Particle.color').array
                self.particle_ids_x_geq_0, self.particle_ids_x_lt_0 = \
                    self.get_particle_ids_for_coloring()
            self.color_particles()

    def color_particles(self) -> None:
        '''Color particles based on their IDs.'''
        indices_x_geq_0 = self.particle_id_to_index[self.particle_ids_x_geq_0]
        indices_x_lt_0 = self.particle_id_to_index[self.particle_ids_x_lt_0]
        self.particle_color[indices_x_geq_0] = COLOR_A
        self.particle_color[indices_x_lt_0] = COLOR_B

    def get_particle_ids_for_coloring(self) -> tuple[np.ndarray, np.ndarray]:
        '''
        Get the IDs of particles based on their x-coordinate.
        particle_ids_x_geq_0: particles with x >= 0
        particle_ids_x_lt_0:  particles with x < 0
        '''
        particle_index_to_id = np.argsort(self.particle_id_to_index[:, 0])
        particle_ids_x_geq_0 = particle_index_to_id[np.where(self.particle_pos[:, 0] >= 0)[0]]
        particle_ids_x_lt_0 = particle_index_to_id[np.where(self.particle_pos[:, 0] < 0)[0]]
        return particle_ids_x_geq_0, particle_ids_x_lt_0

Code Snippet 1 - ParticleColorer.py

41.18.2. Add Custom Scripts Path

In the example from the previous section, where we colored particles in two distinct colors, it was sufficient to contain all the custom scripting in a single script, as the code was relatively short. However, for more extensive post-processing, it can be more convenient to divide the script across several files for better clarity. This approach can be utilized in the Journal Viewer by using the --addCustomScriptPath <Path> argument.

To attach a custom Python script, my_custom_script.py, to a journal, my_journal.agxJournal, while also making the modules in D:/my_custom_utils/ accessible to my_custom_script.py, execute the following terminal command:

>> journalViewer my_journal.agxJournal --attachScript my_custom_script.py --addCustomScriptPath D:/my_custom_utils/

Let’s illustrate this with an example. We will reuse the functionality from the previous section, where particles were divided at x=0 into left and right collections. Additionally, we will add functionality to store the centers of mass for both collections and save this data into a .json file for later plotting. We will break out the ParticleColorer class into a separate file, particle_colorer.py, create a new class, CenterOfMassAnalyzer, to handle center of mass calculations in CenterOfMassAnalyzer.py, and create a class, ParticlePostProcessor, responsible for managing both previous classes in ParticlePostProcessor.py. We place ParticleColorer.py and CenterOfMassAnalyzer.py in D:/particle_post_processor_utils/, while placing ParticlePostProcessor.py in the same directory as the journal rotary_drum.agxJournal. (See Code snippet 3 - ParticlePostProcessor.py, Code snippet 4 - ParticleColorer.py, and Code snippet 5 - CenterOfMassAnalyzer.py for details.)

Navigate to the directory where the journal is located. To execute the post-processing script, use the following terminal command:

>> journalViewer rotary_drum.agxJournal --attachScript ParticlePostProcessor.py --addCustomScriptPath D:/particle_post_processor_utils/

A key feature here is that --addCustomScriptPath D:/particle_post_processor_utils/ makes the modules in that directory available to ParticlePostProcessor.py. By studying Code snippet 3 - ParticlePostProcessor.py, we can see that this script imports the ParticleColorer and CenterOfMassAnalyzer classes from the files in the directory D:/particle_post_processor_utils/.

When running the above command, the particle coloring works as described in the previous section. The new addition is the creation of a .json file that contains the center of mass positions for both particle collections as a function of time. An (abbreviated) version of this file is shown in Code snippet 2 - com_data.json

{
    "time": [
        4.0000000000002185,
        4.033333333333557,
        ...
    ],
    "pos_left": [
        [
            -0.16945233435221013,
            2.187992830899125e-05,
            -0.40797553851422996
        ],
        [
            -0.16945324192040673,
            2.1973870151488706e-05,
            -0.40797750815201633
        ],
        ...
    ],
    "pos_right": [
        [
            0.16720595499825752,
            -3.968564056495067e-05,
            -0.4074566484874103
        ],
        [
            0.16720676731927853,
            -3.968090879135949e-05,
            -0.4074585782608062
        ],
        ...
    ]
}

Code Snippet 2 - com_data.json

This .json data can be plotted using various programs. A plot generated by a Python script is shown in Fig. 41.37.

../_images/journal_viewer_custom_scripting_add_custom_scripts_path_plot.png

Fig. 41.37 Center of mass coordinates vs. time for the two particle collections in Fig. 41.35. Plotted using the data in Code snippet 2 - com_data.json.

As shown in Fig. 41.37, the mass centers in the xz-plane move in elliptical trajectories with decreasing amplitudes. This behavior is expected: as the two collections mix, their centers of mass converge, leading to the decays. Additionally, both y coordinates remain almost constant, which is also expected, as the drum’s axis lies along the y-axis, and there is no significant flow in the axial direction.

import numpy as np
import agxSDK
import agxPython
from agxPythonModules.utils.numpy_utils import BufferWrapper
from ParticleColorer import ParticleColorer
from CenterOfMassAnalyzer import CenterOfMassAnalyzer

settings = {
    "time_to_start": 4,  # Start time for coloring and analysis
    "time_to_end": 44,  # End time for coloring and analysis
    "color_A": [0.502, 0, 0.502, 1],  # Purple
    "color_B": [1, 1, 0, 1],  # Yellow
    # Path to save center of mass data
    "path_to_data_file_to_save": "D:/particle_post_processor_utils/com_data.json"
}

def buildViewerScene() -> None:
    '''Build the viewer scene and add particle post-processor.'''
    sim = agxPython.getContext().environment.getSimulation()
    particle_post_processor = ParticlePostProcessor(sim, settings)
    sim.add(particle_post_processor)

class ParticlePostProcessor(agxSDK.StepEventListener):
    def __init__(self, sim: agxSDK.Simulation, settings: dict) -> None:
        super().__init__()
        self.sim = sim
        self.settings = settings
        self.particle_colorer = ParticleColorer(self.settings)
        self.center_of_mass_analyzer = CenterOfMassAnalyzer(self.settings)
        self.has_saved_data = False
        self.particle_ids_x_geq_0 = None
        self.particle_ids_x_lt_0 = None

    def post(self, time: float) -> None:
        '''Executed after each simulation step.'''
        if self.settings["time_to_start"] < time <= self.settings["time_to_end"]:
            if self.particle_ids_x_geq_0 is None or self.particle_ids_x_lt_0 is None:
                self.initiate_buffers()
                self.particle_ids_x_geq_0, self.particle_ids_x_lt_0 = \
                    self.particle_colorer.get_particle_ids_for_coloring(
                        self.particle_pos,
                        self.particle_id_to_index)
            self.particle_colorer.color_particles(
                self.particle_color,
                self.particle_id_to_index,
                self.particle_ids_x_geq_0,
                self.particle_ids_x_lt_0)
            self.center_of_mass_analyzer.store_data(
                time,
                self.particle_pos,
                self.particle_mass,
                self.particle_id_to_index,
                self.particle_ids_x_geq_0,
                self.particle_ids_x_lt_0)
        elif time > self.settings["time_to_end"] and not self.has_saved_data:
            self.center_of_mass_analyzer.save_data()
            self.has_saved_data = True

    def initiate_buffers(self) -> None:
        '''Initialize simulation buffers for particles.'''
        self.particle_pos = BufferWrapper(self.sim, 'Particle.position').array
        self.particle_id_to_index = BufferWrapper(self.sim, 'Particle._idToIndex').array
        self.particle_mass = BufferWrapper(self.sim, 'Particle.mass').array
        self.particle_color = BufferWrapper(self.sim, 'Particle.color').array

Code Snippet 3 - ParticlePostProcessor.py

import numpy as np

class ParticleColorer():
    def __init__(self,  settings: dict) -> None:
        self.settings: dict = settings

    def color_particles(self, particle_color, particle_id_to_index, particle_ids_x_geq_0, particle_ids_x_lt_0) -> None:
        '''Color particles based on their IDs.'''
        indices_x_geq_0 = particle_id_to_index[particle_ids_x_geq_0]
        indices_x_lt_0 = particle_id_to_index[particle_ids_x_lt_0]
        particle_color[indices_x_geq_0] = self.settings["color_A"]
        particle_color[indices_x_lt_0] = self.settings["color_B"]

    def get_particle_ids_for_coloring(self, particle_pos, particle_id_to_index) -> tuple[np.ndarray, np.ndarray]:
        '''
        Get the IDs of particles based on their x-coordinate.
        particle_ids_x_geq_0: particles with x >= 0
        particle_ids_x_lt_0:  particles with x < 0
        '''
        particle_index_to_id = np.argsort(particle_id_to_index[:, 0])
        particle_ids_x_geq_0 = particle_index_to_id[np.where(particle_pos[:, 0] >= 0)[0]]
        particle_ids_x_lt_0 = particle_index_to_id[np.where(particle_pos[:, 0] < 0)[0]]
        return particle_ids_x_geq_0, particle_ids_x_lt_0

Code Snippet 4 - ParticleColorer.py

import numpy as np
import json

class CenterOfMassAnalyzer():
    def __init__(self, settings: dict) -> None:
        self.settings = settings
        self.data = {
            "time": np.array([]),  # Array to store time steps
            "pos_left": np.empty((0, 3)),  # Empty array for left side positions
            "pos_right": np.empty((0, 3))  # Empty array for right side positions
        }

    def store_data(self, time, particle_pos, particle_mass, particle_id_to_index, particle_ids_x_geq_0, particle_ids_x_lt_0) -> None:
        '''Store the center of mass for left and right particle collections.'''
        center_of_mass_left = self.get_center_of_mass(particle_pos, particle_mass, particle_id_to_index, particle_ids_x_lt_0)
        center_of_mass_right = self.get_center_of_mass(particle_pos, particle_mass, particle_id_to_index, particle_ids_x_geq_0)
        self.data["time"] = np.append(self.data["time"], time)
        self.data["pos_left"] = np.vstack([self.data["pos_left"], center_of_mass_left])
        self.data["pos_right"] = np.vstack([self.data["pos_right"], center_of_mass_right])

    def save_data(self) -> None:
        '''Save center of mass data to a JSON file.'''
        data_to_save = {
            "time": self.data["time"].tolist(),
            "pos_left": self.data["pos_left"].tolist(),
            "pos_right": self.data["pos_right"].tolist()
        }
        with open(self.settings["path_to_data_file_to_save"], 'w') as f:
            json.dump(data_to_save, f, indent=4)
        print(f'Data saved to {self.settings["path_to_data_file_to_save"]}')

    def get_center_of_mass(self, particle_pos, particle_mass, particle_id_to_index, particle_selected_ids) -> np.ndarray:
        '''Calculate the center of mass for the selected particles.'''
        particle_selected_indices = particle_id_to_index[:, 0][particle_selected_ids]
        particle_selected_pos = particle_pos[particle_selected_indices]
        particle_selected_mass = particle_mass[particle_selected_indices]
        total_mass = particle_selected_mass.sum()
        particle_selected_pos_weighted = (particle_selected_pos[:, :3] * particle_selected_mass).sum(axis=0)
        return particle_selected_pos_weighted / total_mass

Code Snippet 5 - CenterOfMassAnalyzer.py