Skip to content

ParticleSystem

FluxRender.entities.ParticleSystem

ParticleSystem(vec_function, color_mapper: ColorMapper = ColorMapper(), count: int = 10000, lifetime: float = 3.0, radius: float = 2, color_property: Property = Property.VELOCITY, opacity_function=None, speed: float = 1.0, normalize_speed=False, speed_scale: ScaleType = ScaleType.LINEAR, max_speed_percentile: float = 95.0, emitter=None, color_clipping_percentiles: Sequence[float] = (5.0, 95.0), scale_function=None, base_angle_vector=None, custom_color_function=None)

Bases: VectorEntity

A dynamic visualizer that simulates thousands of particles flowing through a vector field.

While a VectorField uses static arrows to show the direction of the math, a ParticleSystem brings it to life. It drops thousands of tiny "tracers" into your mathematical fluid and lets them flow. This is perfect for visualizing aerodynamics, fluid dynamics, or simply creating hypnotic, beautiful animations of your equations.

Parameters:

Name Type Description Default
vec_function Callable / VectorMathEngine

The math driving the flow. A function taking (x, y) and optionally (t) returning vector components (dx, dy), or a pre-built VectorMathEngine.

IMPORTANT REFERENCE NOTE: If you pass an existing VectorMathEngine instance AND simultaneously provide overriding parameters (such as base_angle_vector or custom_color_function), this entity will silently create an independent clone of the engine under the hood. Consequently, any subsequent modifications made to your original engine instance will NOT be reflected in this visual entity.

required
color_mapper ColorMapper

The palette used to paint the particles based on their physical properties. (Default: ColorMapper())

ColorMapper()
count int

How many particles to simulate at once. Higher numbers look denser but require more GPU power. (Default: 10000)

10000
lifetime float

How many seconds a particle lives before it fades out and respawns at a new location. Keeps the screen from getting too chaotic. (Default: 3.0)

3.0
radius float

The visual size (thickness) of each particle dot in pixels. (Default: 2.0)

2
color_property Property

What mathematical aspect decides the particle's color (e.g., VELOCITY for speed, CURL for rotation, ANGLE for direction). (Default: Property.VELOCITY)

VELOCITY
opacity_function Callable

A custom fade-in/fade-out curve. A function that takes a particle's normalized age (0.0 = just born, 1.0 = about to die) and returns its opacity (0.0 to 1.0). If None, the default smoothing function will be applied.

None
speed float

The global "gas pedal" for the animation. Multiplies how fast particles move across the screen. (Default: 1.0)

1.0
normalize_speed bool

If True, ignores the mathematical length of the vectors—every particle moves at exactly the same speed (great for analyzing pure direction). If False, particles in strong currents move fast, and those in weak currents move slowly. (Default: False)

False
speed_scale ScaleType

How the raw math translates to visual speed on screen. Use LOGARITHMIC or EXPONENTIAL if your math produces extreme values. (Default: ScaleType.LINEAR)

LINEAR
max_speed_percentile float

Outlier protection (0 to 100). Clips the fastest physical vectors at this percentile. Prevents mathematical singularities (like dividing by zero) from shooting particles off the screen at infinite speed. (Default: 95.0)

95.0
emitter SpatialRegion or dict

Controls where new particles are born: • Single region: Pass a SpatialRegion to force all particles to spawn inside that specific shape. • Weighted regions: Pass a dictionary mapping multiple SpatialRegion objects to numeric weights (e.g., {main_vent: 3.0, side_vent: 1.0}). The numbers act as relative probabilities (they don't need to sum to 1.0). In this example, a particle is 3 times more likely to spawn in the main vent than the side vent. If None, particles spawn randomly across the entire screen.

None
color_clipping_percentiles tuple

The (min, max) percentiles used to ignore extreme outliers when mapping colors, ensuring your color gradient isn't ruined by a single infinite value. (Default: (5.0, 95.0))

(5.0, 95.0)
scale_function Callable

[Speed Scale CUSTOM] A user-defined function mapping normalized vector lengths [0, 1] to output speeds [0, 1]. Used only when speed_scale is ScaleType.CUSTOM and normalize_speed is False.

None
base_angle_vector tuple, list, or Callable

[Property.ANGLE] The reference baseline for calculating angles. Can be a static [x, y] list or a dynamic vector function of (x, y, t).

None
custom_color_function Callable

[Property.CUSTOM] A user-defined function for calculating colors.

To maximize performance, the engine strictly avoids redundant math. Since the VectorField or ParticleSystem already evaluates the primary vector function to render arrows or move particles, it passes these exact, pre-calculated results directly into your color function.

The function must accept 4 or 5 parameters:

  • vec_dx (float/ndarray): The evaluated X vector difference (the pre-calculated result at the anchor point).
  • vec_dy (float/ndarray): The evaluated Y vector difference.
  • world_x (float/ndarray): The X anchor point (sampling coordinate) in world space, determined by the entity's internal grid mode or particle position.
  • world_y (float/ndarray): The Y anchor point in world space.
  • t (float, optional): The current simulation time (if the engine is time-dependent).

Should return a scalar or a NumPy array. Use vectorized NumPy operations for maximum efficiency.

None
Example

Basic usage with a simple rotational field:

import FluxRender as fr

# [Initializing the scene and coordinate system]

def rotational_field(x, y):
    return -y, x

particle_system = ParticleSystem(vec_function = rotational_field)

scene.add(particle_system)

The same example, but with particles colored by their angle and a custom color palette:

import FluxRender as fr

def rotational_field(x, y):
    return -y, x

# Blue Shade Color Mapping
my_color_mapper = fr.ColorMapper(max_hue = 180, min_hue = 240, max_saturation = 1, min_saturation = 0.8, max_lightness = 0.6, min_lightness = 0.45)
particle_system = fr.ParticleSystem(
    vec_function = rotational_field,
    color_mapper = my_color_mapper,
    color_property = fr.Property.ANGLE  # Color based on vector angle relative to base_angle_vector (default [1.0, 0.0])
)

scene.add(particle_system)

Source code in FluxRender/entities.py
def __init__(
            self, vec_function,
            color_mapper: ColorMapper = ColorMapper(),
            count: int = 10000,
            lifetime: float = 3.0,
            radius: float = 2,
            color_property: Property = Property.VELOCITY,
            opacity_function = None, # function of time: [0, 1] -> [0, 1]
            speed: float = 1.0,
            normalize_speed = False,
            speed_scale: ScaleType = ScaleType.LINEAR,
            max_speed_percentile: float = 95.0, # from 0 to 100
            emitter = None,
            color_clipping_percentiles: Sequence[float] = (5.0, 95.0), # from 0 to 100, (min_percentile, max_percentile)

            # parameters for speed_scale.CUSTOM and normalize_speed = False
            scale_function = None, # function domain: [0, 1]

            # Parameters specific to Property.ANGLE color_property
            base_angle_vector = None,

            # Parameters specific to custom color function mode (when color_property is Property.CUSTOM_FUNCTION)
            custom_color_function = None
            ):
    """
    Args:
        vec_function (Callable/VectorMathEngine): The math driving the flow. A function taking (x, y)
            and optionally (t) returning vector components (dx, dy), or a pre-built VectorMathEngine.

            **IMPORTANT REFERENCE NOTE**: If you pass an existing VectorMathEngine instance
            AND simultaneously provide overriding parameters (such as `base_angle_vector`
            or `custom_color_function`), this entity will silently create an independent
            **clone** of the engine under the hood. Consequently, any subsequent modifications
            made to your original engine instance will NOT be reflected in this visual entity.
        color_mapper (ColorMapper): The palette used to paint the particles based on their physical properties. (Default: ColorMapper())
        count (int): How many particles to simulate at once. Higher numbers look denser but require
            more GPU power. (Default: 10000)
        lifetime (float): How many seconds a particle lives before it fades out and respawns
            at a new location. Keeps the screen from getting too chaotic. (Default: 3.0)
        radius (float): The visual size (thickness) of each particle dot in pixels. (Default: 2.0)
        color_property (Property): What mathematical aspect decides the particle's color
            (e.g., VELOCITY for speed, CURL for rotation, ANGLE for direction). (Default: Property.VELOCITY)
        opacity_function (Callable, optional): A custom fade-in/fade-out curve. A function that takes
            a particle's normalized age (0.0 = just born, 1.0 = about to die) and returns its opacity (0.0 to 1.0).
            If None, the default smoothing function will be applied.
        speed (float): The global "gas pedal" for the animation. Multiplies how fast particles
            move across the screen. (Default: 1.0)
        normalize_speed (bool): If True, ignores the mathematical length of the vectors—every particle
            moves at exactly the same speed (great for analyzing pure direction). If False, particles
            in strong currents move fast, and those in weak currents move slowly. (Default: False)
        speed_scale (ScaleType): How the raw math translates to visual speed on screen.
            Use LOGARITHMIC or EXPONENTIAL if your math produces extreme values. (Default: ScaleType.LINEAR)
        max_speed_percentile (float): Outlier protection (0 to 100). Clips the fastest physical vectors
            at this percentile. Prevents mathematical singularities (like dividing by zero) from shooting
            particles off the screen at infinite speed. (Default: 95.0)
        emitter (SpatialRegion or dict, optional): Controls where new particles are born:
            • Single region: Pass a `SpatialRegion` to force all particles to spawn inside that specific shape.
            • Weighted regions: Pass a dictionary mapping multiple `SpatialRegion` objects to numeric weights (e.g., `{main_vent: 3.0, side_vent: 1.0}`). The numbers act as relative probabilities (they don't need to sum to 1.0). In this example, a particle is 3 times more likely to spawn in the main vent than the side vent.
            If None, particles spawn randomly across the entire screen.
        color_clipping_percentiles (tuple): The (min, max) percentiles used to ignore extreme outliers
            when mapping colors, ensuring your color gradient isn't ruined by a single infinite value. (Default: (5.0, 95.0))

        scale_function (Callable, optional): **[Speed Scale CUSTOM]** A user-defined function mapping
            normalized vector lengths [0, 1] to output speeds [0, 1]. Used only when speed_scale is ScaleType.CUSTOM
            and normalize_speed is False.
        base_angle_vector (tuple, list, or Callable): **[Property.ANGLE]** The reference baseline for calculating angles.
            Can be a static [x, y] list or a dynamic vector function of (x, y, t).
        custom_color_function (Callable, optional): **[Property.CUSTOM]** A user-defined function for calculating colors.

            To maximize performance, the engine strictly avoids redundant math. Since the
            VectorField or ParticleSystem already evaluates the primary vector function to
            render arrows or move particles, it passes these exact, pre-calculated results
            directly into your color function.

            The function must accept 4 or 5 parameters:

            * `vec_dx` (float/ndarray): The evaluated X vector difference (the pre-calculated result at the anchor point).
            * `vec_dy` (float/ndarray): The evaluated Y vector difference.
            * `world_x` (float/ndarray): The X anchor point (sampling coordinate) in world space, determined by the entity's internal grid mode or particle position.
            * `world_y` (float/ndarray): The Y anchor point in world space.
            * `t` (float, optional): The current simulation time (if the engine is time-dependent).

            Should return a scalar or a NumPy array. Use vectorized NumPy operations for maximum efficiency.


    Example:
        Basic usage with a simple rotational field:
        ```python
        import FluxRender as fr

        # [Initializing the scene and coordinate system]

        def rotational_field(x, y):
            return -y, x

        particle_system = ParticleSystem(vec_function = rotational_field)

        scene.add(particle_system)
        ```

        The same example, but with particles colored by their angle and a custom color palette:
        ```python
        import FluxRender as fr

        def rotational_field(x, y):
            return -y, x

        # Blue Shade Color Mapping
        my_color_mapper = fr.ColorMapper(max_hue = 180, min_hue = 240, max_saturation = 1, min_saturation = 0.8, max_lightness = 0.6, min_lightness = 0.45)
        particle_system = fr.ParticleSystem(
            vec_function = rotational_field,
            color_mapper = my_color_mapper,
            color_property = fr.Property.ANGLE  # Color based on vector angle relative to base_angle_vector (default [1.0, 0.0])
        )

        scene.add(particle_system)
        ```
    """

    super().__init__(vec_function, color_mapper, color_property, color_clipping_percentiles)

    # Dynamic configuration GPU
    self.radius = radius

    # Dynamic Python configuration
    self.count = count
    self.lifetime = lifetime
    self.speed = speed
    self.normalize_speed = normalize_speed
    self.speed_scale = speed_scale
    self.scale_function = scale_function
    self.max_speed_percentile = max_speed_percentile
    self.opacity_function = opacity_function
    self.emitter = emitter

    self.custom_color_function = custom_color_function
    self.base_angle_vector = base_angle_vector


    self.coords = None
    self.scene = None
    self.math_engine = None


    self._particles_positions = ti.Vector.field(2, dtype=ti.float32, shape=count)
    self._particles_colors = ti.Vector.field(4, dtype=ti.float32, shape=count)

    self._particles_positions_np = np.zeros((count, 2), dtype=np.float32)
    self._particles_lifetimes_np = np.zeros(count, dtype=np.float32)
    self._particles_colors_np = np.zeros((count, 4), dtype=np.float32)

    self._is_initialized = True

evaluate_angle_vector

evaluate_angle_vector(x: float, y: float) -> tuple

Evaluates the base reference angle vector at the specified spatial coordinates.

This method determines whether the reference angle vector is a static coordinate pair or a dynamically evaluated mathematical function. If it is a callable function, it safely executes it, automatically injecting the current time if the function signature requires it.

Parameters:

Name Type Description Default
x float or ndarray

The x-coordinate(s) in the mathematical world space.

required
y float or ndarray

The y-coordinate(s) in the mathematical world space.

required

Returns:

Name Type Description
tuple tuple

A tuple (component_x, component_y) representing the evaluated reference vector components.

Notes
  • Broadcasting: This method fully supports NumPy broadcasting. You can pass single float values for pinpoint evaluation, or large multidimensional arrays (like those generated by numpy.meshgrid) to evaluate the entire mathematical space simultaneously.
Example

Evaluating a dynamically rotating reference angle at the origin:

import FluxRender as fr
import numpy as np

# [Initializing the scene and coordinate system]

def rotating_reference(x, y, t):
    direction_x = np.cos(t)
    direction_y = np.sin(t)
    return direction_x, direction_y

field = fr.VectorField(
    vec_function = lambda x, y: (y, -x),
    color_property = fr.Property.ANGLE,
    base_angle_vector = rotating_reference
)
scene.add(field)

# The engine automatically handles the underlying time injection
angle_vector_x, angle_vector_y = field.evaluate_angle_vector(0.0, 0.0)

print(f"Reference angle vector at the origin: ({angle_vector_x}, {angle_vector_y})")

Source code in FluxRender/entities.py
def evaluate_angle_vector(self, x: float, y: float) -> tuple:
    """Evaluates the base reference angle vector at the specified spatial coordinates.

    This method determines whether the reference angle vector is a static
    coordinate pair or a dynamically evaluated mathematical function. If it is
    a callable function, it safely executes it, automatically injecting the
    current time if the function signature requires it.

    Args:
        x (float or numpy.ndarray): The x-coordinate(s) in the mathematical world space.
        y (float or numpy.ndarray): The y-coordinate(s) in the mathematical world space.

    Returns:
        tuple: A tuple (component_x, component_y) representing the evaluated reference vector components.

    Notes:
        * **Broadcasting:** This method fully supports NumPy broadcasting. You can pass single
          float values for pinpoint evaluation, or large multidimensional arrays (like those
          generated by `numpy.meshgrid`) to evaluate the entire mathematical space simultaneously.

    Example:
        Evaluating a dynamically rotating reference angle at the origin:
        ```python
        import FluxRender as fr
        import numpy as np

        # [Initializing the scene and coordinate system]

        def rotating_reference(x, y, t):
            direction_x = np.cos(t)
            direction_y = np.sin(t)
            return direction_x, direction_y

        field = fr.VectorField(
            vec_function = lambda x, y: (y, -x),
            color_property = fr.Property.ANGLE,
            base_angle_vector = rotating_reference
        )
        scene.add(field)

        # The engine automatically handles the underlying time injection
        angle_vector_x, angle_vector_y = field.evaluate_angle_vector(0.0, 0.0)

        print(f"Reference angle vector at the origin: ({angle_vector_x}, {angle_vector_y})")
        ```
    """

    return self.math_engine.evaluate_angle_vector(x, y)

evaluate_scalar_function

evaluate_scalar_function(user_defined_function, *spatial_arguments) -> np.ndarray

Evaluates a user-provided mathematical scalar function, automatically handling time injection and vectorization.

This method serves as a robust adapter for custom user logic that maps spatial coordinates (and potentially vector components) to a single scalar value. It analyzes the signature of the provided function to dynamically inject the simulation time if required. It attempts to execute the function using native NumPy vectorization for maximum performance, automatically falling back to numpy.vectorize if strictly scalar Python operations are detected.

Parameters:

Name Type Description Default
user_defined_function Callable

The custom scalar function or lambda to evaluate.

required
*spatial_arguments

The base spatial and/or vector arrays to pass into the function (e.g., evaluated_vector_x, evaluated_vector_y, world_x, world_y).

()

Returns:

Type Description
ndarray

numpy.ndarray: A single NumPy array containing the computed scalar values, properly sanitized and ready for rendering or further mathematical processing.

Notes
  • Time Injection: If user_defined_function accepts exactly one parameter more than the number of provided *spatial_arguments, the current scene time is automatically injected during execution.
  • Vectorization Fallback: You do not need to write strictly vectorized NumPy code. Regular Python scalar operations will be caught and vectorized automatically, though writing native NumPy code is highly recommended for optimal rendering performance.
  • Scalar Output: Unlike its vector counterpart, this method strictly expects the user's function to return a single value (or a single array) per spatial coordinate, not a tuple.
Example

Calculating and printing a custom physical metric (like kinetic energy) at specific points, while the field continues to render its default colors visually:

import FluxRender as fr
import numpy as np

# [Initializing the scene and coordinate system]

field = fr.VectorField(
    vec_function = lambda x, y: (y, -x),
)
scene.add(field)

# Custom function that internally queries the field for vector data and calculates a scalar property (e.g., kinetic energy = 0.5 * (vx^2 + vy^2))
def calculate_kinetic_energy(x, y):
    vector_dx, vector_dy = field.evaluate_vector_field(x, y)
    kinetic_energy = 0.5 * (vector_dx**2 + vector_dy**2)
    return kinetic_energy

# Define the exact spatial points we want to analyze
target_coordinates_x = np.array([0.0, 1.0, 2.0])
target_coordinates_y = np.array([0.0, 1.0, 2.0])

# Evaluate the custom metric across all points simultaneously
energy_results = field.evaluate_scalar_function(
    calculate_kinetic_energy,
    target_coordinates_x,
    target_coordinates_y
)

print(f"Kinetic energy at points (0,0), (1,1) and (2,2): {energy_results}")

Source code in FluxRender/entities.py
def evaluate_scalar_function(self, user_defined_function, *spatial_arguments) -> np.ndarray:
    """Evaluates a user-provided mathematical scalar function, automatically handling time injection and vectorization.

    This method serves as a robust adapter for custom user logic that maps spatial coordinates (and potentially
    vector components) to a single scalar value. It analyzes the signature of the provided function to dynamically
    inject the simulation time if required. It attempts to execute the function using native NumPy vectorization
    for maximum performance, automatically falling back to `numpy.vectorize` if strictly scalar Python operations
    are detected.

    Args:
        user_defined_function (Callable): The custom scalar function or lambda to evaluate.
        *spatial_arguments: The base spatial and/or vector arrays to pass into the function
            (e.g., evaluated_vector_x, evaluated_vector_y, world_x, world_y).

    Returns:
        numpy.ndarray: A single NumPy array containing the computed scalar values, properly
            sanitized and ready for rendering or further mathematical processing.

    Notes:
        * **Time Injection:** If `user_defined_function` accepts exactly one parameter more than the
          number of provided `*spatial_arguments`, the current scene time is automatically injected
          during execution.
        * **Vectorization Fallback:** You do not need to write strictly vectorized NumPy code. Regular
          Python scalar operations will be caught and vectorized automatically, though writing native
          NumPy code is highly recommended for optimal rendering performance.
        * **Scalar Output:** Unlike its vector counterpart, this method strictly expects the user's
          function to return a single value (or a single array) per spatial coordinate, not a tuple.

    Example:
        Calculating and printing a custom physical metric (like kinetic energy) at specific points,
        while the field continues to render its default colors visually:
        ```python
        import FluxRender as fr
        import numpy as np

        # [Initializing the scene and coordinate system]

        field = fr.VectorField(
            vec_function = lambda x, y: (y, -x),
        )
        scene.add(field)

        # Custom function that internally queries the field for vector data and calculates a scalar property (e.g., kinetic energy = 0.5 * (vx^2 + vy^2))
        def calculate_kinetic_energy(x, y):
            vector_dx, vector_dy = field.evaluate_vector_field(x, y)
            kinetic_energy = 0.5 * (vector_dx**2 + vector_dy**2)
            return kinetic_energy

        # Define the exact spatial points we want to analyze
        target_coordinates_x = np.array([0.0, 1.0, 2.0])
        target_coordinates_y = np.array([0.0, 1.0, 2.0])

        # Evaluate the custom metric across all points simultaneously
        energy_results = field.evaluate_scalar_function(
            calculate_kinetic_energy,
            target_coordinates_x,
            target_coordinates_y
        )

        print(f"Kinetic energy at points (0,0), (1,1) and (2,2): {energy_results}")
        ```
    """

    return self.math_engine._safe_evaluate_scalar_function(user_defined_function, *spatial_arguments)

evaluate_vector_field

evaluate_vector_field(x: float, y: float) -> tuple

Evaluates the primary vector field function at the specified spatial coordinates.

This method acts as a safe execution wrapper for the user-defined vector function. It delegates the execution to the internal evaluation handler, which manages potential numpy broadcasting issues, scalar fallbacks, and automatic time-parameter injection.

Parameters:

Name Type Description Default
x float or ndarray

The x-coordinate(s) in the mathematical world space.

required
y float or ndarray

The y-coordinate(s) in the mathematical world space.

required

Returns:

Name Type Description
tuple tuple

A tuple (vector_x, vector_y) representing the evaluated vector field components.

Notes
  • Broadcasting: This method fully supports NumPy broadcasting. You can pass single float values for pinpoint evaluation, or large multidimensional arrays (like those generated by numpy.meshgrid) to evaluate the entire mathematical space simultaneously.
Example

Evaluating the field at a single focal point:

import FluxRender as fr

# [Initializing the scene and coordinate system]

field = fr.VectorField(
    vec_function = lambda x, y: (y, -x),
    color_property = fr.Property.VELOCITY
)
scene.add(field)

vector_component_x, vector_component_y = field.evaluate_vector_field(1.0, 0.0)

print(f"Vector field at (1.0, 0.0): ({vector_component_x}, {vector_component_y})")
# Result: (0.0, -1.0)

Evaluating the field at multiple points simultaneously using numpy arrays:

import numpy as np

# Define the exact spatial points we want to analyze
target_coordinates_x = np.array([0.0, 1.0, 2.0])
target_coordinates_y = np.array([0.0, 1.0, 2.0])

x_vectors, y_vectors = vec_field.evaluate_vector_field(
    target_coordinates_x,
    target_coordinates_y
)
print(f"Vector field at points (0,0), (1,1) and (2,2): [{x_vectors[0]}, {y_vectors[0]}] | [{x_vectors[1]}, {y_vectors[1]}] | [{x_vectors[2]}, {y_vectors[2]}]")

Source code in FluxRender/entities.py
def evaluate_vector_field(self, x: float, y: float) -> tuple:
    """Evaluates the primary vector field function at the specified spatial coordinates.

    This method acts as a safe execution wrapper for the user-defined vector
    function. It delegates the execution to the internal evaluation handler,
    which manages potential numpy broadcasting issues, scalar fallbacks, and
    automatic time-parameter injection.

    Args:
        x (float or numpy.ndarray): The x-coordinate(s) in the mathematical world space.
        y (float or numpy.ndarray): The y-coordinate(s) in the mathematical world space.

    Returns:
        tuple: A tuple (vector_x, vector_y) representing the evaluated vector field components.

    Notes:
        * **Broadcasting:** This method fully supports NumPy broadcasting. You can pass single
          float values for pinpoint evaluation, or large multidimensional arrays (like those
          generated by `numpy.meshgrid`) to evaluate the entire mathematical space simultaneously.

    Example:
        Evaluating the field at a single focal point:
        ```python
        import FluxRender as fr

        # [Initializing the scene and coordinate system]

        field = fr.VectorField(
            vec_function = lambda x, y: (y, -x),
            color_property = fr.Property.VELOCITY
        )
        scene.add(field)

        vector_component_x, vector_component_y = field.evaluate_vector_field(1.0, 0.0)

        print(f"Vector field at (1.0, 0.0): ({vector_component_x}, {vector_component_y})")
        # Result: (0.0, -1.0)
        ```

        Evaluating the field at multiple points simultaneously using numpy arrays:
        ```python
        import numpy as np

        # Define the exact spatial points we want to analyze
        target_coordinates_x = np.array([0.0, 1.0, 2.0])
        target_coordinates_y = np.array([0.0, 1.0, 2.0])

        x_vectors, y_vectors = vec_field.evaluate_vector_field(
            target_coordinates_x,
            target_coordinates_y
        )
        print(f"Vector field at points (0,0), (1,1) and (2,2): [{x_vectors[0]}, {y_vectors[0]}] | [{x_vectors[1]}, {y_vectors[1]}] | [{x_vectors[2]}, {y_vectors[2]}]")
        ```
    """

    return self.math_engine.evaluate_primary_vector_function(x, y)

evaluate_vector_function

evaluate_vector_function(user_defined_function, *spatial_arguments) -> tuple

Evaluates a user-provided mathematical function, automatically handling time injection and vectorization.

This method serves as a robust adapter for custom user logic. It analyzes the signature of the provided function to dynamically inject the simulation time if required. Furthermore, it attempts to execute the function using native NumPy vectorization for maximum performance. If the function is strictly scalar (e.g., uses standard Python math modules instead of numpy), it automatically falls back to numpy.vectorize to ensure compatibility across large coordinate grids.

Parameters:

Name Type Description Default
user_defined_function Callable

The custom function or lambda to evaluate.

required
*spatial_arguments

The base spatial arrays to pass into the function (typically world_x, world_y).

()

Returns:

Name Type Description
tuple tuple

A tuple (evaluated_vector_x, evaluated_vector_y) containing the computed field components as sanitized NumPy arrays.

Notes
  • Time Injection: If user_defined_function accepts exactly one parameter more than the number of provided *spatial_arguments (e.g., taking x, y, and t), the current scene time is automatically injected during execution.
  • Vectorization Fallback: You do not need to write strictly vectorized NumPy code. Regular Python scalar operations will be caught and vectorized automatically, though writing native NumPy code is highly recommended for optimal rendering performance.

Examples:

Evaluating a custom mathematical perturbation with automatic time injection:

import FluxRender as fr
import numpy as np

# [Initializing the scene and coordinate system]

field = fr.VectorField(
    vec_function = lambda x, y: (y, -x),
)
scene.add(field)

# Notice the third parameter 't'. The engine detects this and injects it.
def custom_wind_perturbation(x, y, t):
    perturbation_x = np.sin(x + t)
    perturbation_y = np.cos(y + t)
    return perturbation_x, perturbation_y

spatial_coordinates_x = np.array([0.0, 1.0, 2.0])
spatial_coordinates_y = np.array([0.0, 1.0, 2.0])

result_vectors_x, result_vectors_y = field.evaluate_vector_function(
    custom_wind_perturbation,
    spatial_coordinates_x,
    spatial_coordinates_y
)

Source code in FluxRender/entities.py
def evaluate_vector_function(self, user_defined_function, *spatial_arguments) -> tuple:
    """Evaluates a user-provided mathematical function, automatically handling time injection and vectorization.

    This method serves as a robust adapter for custom user logic. It analyzes the signature of the
    provided function to dynamically inject the simulation time if required. Furthermore, it attempts
    to execute the function using native NumPy vectorization for maximum performance. If the function
    is strictly scalar (e.g., uses standard Python `math` modules instead of `numpy`), it automatically
    falls back to `numpy.vectorize` to ensure compatibility across large coordinate grids.

    Args:
        user_defined_function (Callable): The custom function or lambda to evaluate.
        *spatial_arguments: The base spatial arrays to pass into the function (typically world_x, world_y).

    Returns:
        tuple: A tuple (evaluated_vector_x, evaluated_vector_y) containing the computed field components
            as sanitized NumPy arrays.

    Notes:
        * **Time Injection:** If `user_defined_function` accepts exactly one parameter more than the
          number of provided `*spatial_arguments` (e.g., taking x, y, and t), the current scene time
          is automatically injected during execution.
        * **Vectorization Fallback:** You do not need to write strictly vectorized NumPy code. Regular
          Python scalar operations will be caught and vectorized automatically, though writing native
          NumPy code is highly recommended for optimal rendering performance.


    Examples:
        Evaluating a custom mathematical perturbation with automatic time injection:
        ```python
        import FluxRender as fr
        import numpy as np

        # [Initializing the scene and coordinate system]

        field = fr.VectorField(
            vec_function = lambda x, y: (y, -x),
        )
        scene.add(field)

        # Notice the third parameter 't'. The engine detects this and injects it.
        def custom_wind_perturbation(x, y, t):
            perturbation_x = np.sin(x + t)
            perturbation_y = np.cos(y + t)
            return perturbation_x, perturbation_y

        spatial_coordinates_x = np.array([0.0, 1.0, 2.0])
        spatial_coordinates_y = np.array([0.0, 1.0, 2.0])

        result_vectors_x, result_vectors_y = field.evaluate_vector_function(
            custom_wind_perturbation,
            spatial_coordinates_x,
            spatial_coordinates_y
        )
        ```
    """

    return self.math_engine._safe_evaluate_vector_function(user_defined_function, *spatial_arguments)