ViscousEulerSPH · Viscous particle flow

SPH velocity diffusion

A sinusoidal velocity field diffuses on a periodic interval. The binned SPH current is compared with the exact viscous decay of the mode.

Actual output from the script below. Interactive controls are available in the plot above.

Physical problem

A velocity wave smoothed by viscosity

A sinusoidal velocity field diffuses on a periodic interval. The binned SPH current is compared with the exact viscous decay of the mode.

PDEs solved by model:

Continuity:

Momentum:

Entropy:

where denotes the entropy per unit mass and is the viscous stress tensor.

The viscous stress tensor for a Newtonian fluid is given by:

where is the dynamic (shear) viscosity and is the identity tensor.

The internal energy per unit mass can be defined in two ways:

Simulation geometry

Domain used in this run

The actual Cuboid geometry reconstructed from this run's metadata. Switch between the 3D view and physical-coordinate slices.

Domain

Cuboid

Generated and exported by Struphy.Drag to rotate · Scroll to zoom · Shift-drag to pan

Parameters

ParameterValueDescription
Loading parameters…
Model
ViscousEulerSPH
Domain
Cuboid
Integrator
Forward euler
Evolution
120 steps
Visualization
Plotly

Performance

Profiling

Struphy's built-in scope-profiler instruments every propagator, pusher and solver call — these are the real per-call timings from the run above.

Durations

Gantt (rank 0)

Region statistics

RegionCountAvg (s)Min (s)Max (s)Total (s)

Complete source

Simulation script

Download .py

Requires Struphy with compiled kernels and Plotly. Run struphy compile once, then execute python sph-velocity-diffusion.py.

"""Viscous decay of a velocity mode with smoothed-particle hydrodynamics.

The pressure-free `ViscousEulerSPH` model starts with a sinusoidal longitudinal
velocity, u(x, 0) = A sin(2 pi x / L), on a periodic interval. Viscosity alone
then damps that mode at 4 mu k^2 / 3, with k = 2 pi / L. The binned SPH current
makes a small, fast verification case with an analytic answer.

Requires Struphy 3.3 with compiled kernels (`struphy compile`).
"""

import numpy as np
import plotly.graph_objects as go

from struphy import (
    BinningPlot,
    BoundaryParameters,
    EnvironmentOptions,
    LoadingParameters,
    SavingParameters,
    Simulation,
    SortingParameters,
    Time,
    WeightsParameters,
    domains,
    equils,
    perturbations,
)
from struphy.models import ViscousEulerSPH
from struphy.ode.utils import ButcherTableau

length = 1.0
viscosity = 0.05
initial_amplitude = 0.5
boxes = 16
markers_per_box = 64
bins = 32
wavenumber = 2 * np.pi / length
exact_decay_rate = viscosity * 4 / 3 * wavenumber**2

# With pressure disabled, this is the particle discretisation of velocity diffusion.
model = ViscousEulerSPH(with_B0=False, with_p=False, with_viscosity=True)
model.propagators.push_eta.options = model.propagators.push_eta.Options(
    butcher=ButcherTableau(algo="forward_euler")
)
model.propagators.push_viscous.options = model.propagators.push_viscous.Options(
    kernel_type="gaussian_1d", mu=viscosity
)

domain = domains.Cuboid(r1=length)
model.euler_fluid.set_markers(
    loading_params=LoadingParameters(ppb=markers_per_box, loading="tesselation"),
    weights_params=WeightsParameters(),
    boundary_params=BoundaryParameters(),
    sorting_params=SortingParameters(boxes_per_dim=(boxes, 1, 1), dims_mask=(True, False, False)),
    saving_params=SavingParameters(
        binning_plots=(
            BinningPlot(slice="e1", n_bins=(bins,), ranges=(0.0, 1.0), output_quantity="current_1"),
        ),
    ),
)
model.euler_fluid.var.add_background(equils.ConstantVelocity())
model.euler_fluid.var.add_perturbation(
    del_u1=perturbations.ModesSin(ls=(1,), amps=(initial_amplitude,))
)

sim = Simulation(
    model=model,
    name="SPH velocity diffusion",
    description=(
        "A sinusoidal velocity field diffuses on a periodic interval. The binned SPH current "
        "is compared with the exact viscous decay of the mode."
    ),
    env=EnvironmentOptions(out_folders="struphy_gallery_runs", sim_folder="sph_velocity_diffusion"),
    time_opts=Time(dt=0.0025, Tend=0.3, split_algo="Strang"),
    domain=domain,
    grid=None,
    derham_opts=None,
)


if __name__ == "__main__":
    from _gallery import export_profiling, is_root, merge_metadata, save_figure

    output = sim.run(profiling_activated=True)
    output.pproc()
    velocity = output.evaluate("euler_fluid/e1_current_1/f")
    times = np.asarray(velocity.t.values)
    x = np.asarray(velocity.e1.values) * length
    values = np.asarray(velocity.values)
    if not np.isfinite(values).all():
        raise RuntimeError("Non-finite SPH velocity: refusing to publish the run")

    # A finite bin records the average of a sine wave, rather than its point value.
    bin_average = np.sinc(wavenumber * (length / bins) / (2 * np.pi))
    mode_amplitude = 2 * np.mean(values * np.sin(wavenumber * x), axis=1) / bin_average
    exact_amplitude = initial_amplitude * np.exp(-exact_decay_rate * times)
    measured_decay_rate = float(-np.polyfit(times, np.log(np.abs(mode_amplitude)), 1)[0])
    relative_rate_error = abs(measured_decay_rate - exact_decay_rate) / exact_decay_rate
    rms_error = float(np.sqrt(np.mean((mode_amplitude - exact_amplitude) ** 2)))
    if is_root():
        print(
            f"decay rate {measured_decay_rate:.4f} (exact {exact_decay_rate:.4f}); "
            f"amplitude RMS error {rms_error:.4g}"
        )

    dense_x = np.linspace(0.0, length, 301)
    picks = np.unique(np.linspace(0, len(times) - 1, min(80, len(times)), dtype=int))

    def profile(index):
        return [
            go.Scatter(
                x=dense_x,
                y=exact_amplitude[index] * np.sin(wavenumber * dense_x),
                mode="lines",
                name="exact",
                line={"color": "#111", "width": 2, "dash": "dash"},
            ),
            go.Scatter(
                x=x,
                y=values[index],
                mode="lines+markers",
                name="SPH",
                line={"color": "#168aad", "width": 2},
                marker={"size": 5},
            ),
        ]

    figure = go.Figure(data=profile(0))
    figure.frames = [go.Frame(name=f"{times[i]:.3f}", data=profile(i)) for i in picks]
    figure.update_layout(
        title="SPH velocity diffusion: sinusoidal mode against its exact decay",
        template="plotly_white",
        xaxis_title="x",
        yaxis_title="u(x, t)",
        yaxis={"range": [-1.1 * initial_amplitude, 1.1 * initial_amplitude]},
        margin={"l": 65, "r": 30, "t": 80, "b": 130},
        legend={"orientation": "h", "y": 1.1},
        updatemenus=[{"type": "buttons", "showactive": False, "x": 0, "y": -0.28, "buttons": [
            {"label": "Play", "method": "animate", "args": [None, {"frame": {"duration": 55, "redraw": False}, "fromcurrent": True}]}
        ]}],
        sliders=[{"x": 0.12, "len": 0.88, "y": -0.18, "currentvalue": {"prefix": "t = "}, "steps": [
            {"args": [[frame.name], {"frame": {"duration": 0, "redraw": False}, "mode": "immediate"}], "label": frame.name, "method": "animate"}
            for frame in figure.frames
        ]}],
    )
    save_figure(figure, "sph-velocity-diffusion")
    merge_metadata(
        "sph-velocity-diffusion",
        viscosity=viscosity,
        exactDecayRate=exact_decay_rate,
        measuredDecayRate=measured_decay_rate,
        relativeRateError=relative_rate_error,
        amplitudeRmsError=rms_error,
        markers=boxes * markers_per_box,
        **export_profiling(sim, "sph-velocity-diffusion"),
    )

See the post-processing guide for more ways to inspect the result.