Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3129b9c
Add modifications for py-pde example reproduction
arekpaterak Jun 2, 2025
9cc4c21
Add a notebook with comparison of reimplementation of the Diffusion e…
arekpaterak Jun 9, 2025
75e566b
Add a similarity test between results of reimplementation of the Diff…
arekpaterak Jun 9, 2025
f358075
Add a title and description to the notebook
arekpaterak Jun 9, 2025
3e694cd
Fix comparison between results
arekpaterak Jun 10, 2025
155db14
Adjust description in the notebook
arekpaterak Jun 10, 2025
32fc63e
Sort imports
arekpaterak Jun 10, 2025
7617ea2
Add time comparison
arekpaterak Jun 10, 2025
764c24b
Fix after pre-commit
arekpaterak Jun 10, 2025
8601efe
Extend the creators list
arekpaterak Jun 15, 2025
89347a7
Add an entry in the examples docs
arekpaterak Jun 15, 2025
6bdf368
lower py-pde CI-time dependency to fix numba requirement incompatibil…
slayoo Jun 18, 2025
06af16d
Fix CI issues with | operand used for typing
arekpaterak Jun 24, 2025
5dab1eb
Remove unused import
arekpaterak Jun 24, 2025
aceeb76
added newline to .gitignore
MyNameIsArko Jun 24, 2025
ca8516a
added input validation, raising error when not selected option non_ze…
MyNameIsArko Jun 24, 2025
d840f92
fixed type annotation xd change any to Any in SimulationResult
yancostrishevsky2 Jun 24, 2025
9daeb12
Adjust the tolerance in the comparison between methods
arekpaterak Jun 24, 2025
373636f
Lower the number of the corrective iterations from 10 to 3
arekpaterak Jun 24, 2025
3a1c9b1
Merge branch 'main' of https://github.com/arekpaterak/PyMPDATA
arekpaterak Jun 24, 2025
1db01be
Fix the issue with | in another place
arekpaterak Jun 24, 2025
3153dc0
Add an additional consistency test
arekpaterak Jun 24, 2025
10d674f
added tests to better cover new functions
MyNameIsArko Jun 24, 2025
196904d
Merge branch 'main' of github.com:arekpaterak/PyMPDATA
MyNameIsArko Jun 24, 2025
ca6abe0
Add quicker simulation args
arekpaterak Jun 24, 2025
6321be4
Fix calculating dx in pympdata solution for diffusion equation
WiktorProsowicz Jun 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# VS Code
.vscode/
16 changes: 16 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@
{
"affiliation": "Jagiellonian University, Kraków, Poland",
"name": "Sadowski, Michał"
},
{
"affiliation": "AGH University of Krakow, Poland",
"name": "Jaśkowiec, Adrian"
},
{
"affiliation": "AGH University of Krakow, Poland",
"name": "Paterak, Arkadiusz"
},
{
"affiliation": "AGH University of Krakow, Poland",
"name": "Prosowicz, Wiktor"
},
{
"affiliation": "AGH University of Krakow, Poland",
"name": "Stryszewski, Jan"
}
],
"keywords": [
Expand Down
88 changes: 87 additions & 1 deletion PyMPDATA/impl/formulae_laplacian.py

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation for the heterogeneous Laplacian returns early with return when options.non_zero_mu_coeff is False, but it might be clearer to raise a NotImplementedError or state in the docstring that heterogeneous diffusion is only supported when this option is enabled.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling of edge cases (such as what happens if the diffusivity profile contains zeros or negative values) isn’t explicitly addressed—consider adding input validation or at least a warning to ensure consistent behavior for all user inputs.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,56 @@ def apply(traversals_data, advector, advectee):
null_vecfield_bc,
*null_scalarfield,
null_scalarfield_bc,
traversals_data.buffer
traversals_data.buffer,
)

return apply


def make_heterogeneous_laplacian(
non_unit_g_factor: bool, options: Options, traversals: Traversals
):
"""returns njit-ted function for heterogeneous diffusion with spatially varying diffusivity

Note: heterogeneous diffusion is only supported when options.non_zero_mu_coeff is enabled
"""
if not options.non_zero_mu_coeff:
raise NotImplementedError(
"Heterogeneous diffusion requires options.non_zero_mu_coeff to be enabled"
)
elif not options.heterogeneous_diffusion:
raise NotImplementedError(
"Heterogeneous diffusion requires options.heterogeneous_diffusion to be enabled"
)

else:
idx = traversals.indexers[traversals.n_dims]
apply_vector = traversals.apply_vector()

formulae_heterogeneous = tuple(
(
__make_heterogeneous_laplacian(
options.jit_flags, idx.ats[i], options.epsilon, non_unit_g_factor
)
if idx.ats[i] is not None
else None
)
for i in range(MAX_DIM_NUM)
)

@numba.njit(**options.jit_flags)
def apply(traversals_data, advector, advectee, diffusivity_field):
null_vecfield, null_vecfield_bc = traversals_data.null_vector_field
return apply_vector(
*formulae_heterogeneous,
*advector.field,
*advectee.field,
advectee.bc,
*null_vecfield,
null_vecfield_bc,
*diffusivity_field.field,
diffusivity_field.bc,
traversals_data.buffer,
)

return apply
Expand All @@ -62,3 +111,40 @@ def fun(advectee, _, __):
)

return fun


def __make_heterogeneous_laplacian(jit_flags, ats, epsilon, non_unit_g_factor):
"""Create heterogeneous Laplacian function that matches MPDATA's one-sided gradient pattern

Note: Diffusivity values are expected to be non-negative. Negative values will cause
an assertion error. Zero values are handled by setting a minimum threshold (epsilon).
"""
if non_unit_g_factor:
raise NotImplementedError()

@numba.njit(**jit_flags)
def fun(advectee, _, diffusivity):
# Get concentration values (matching regular laplacian pattern)
c_curr = ats(*advectee, 0)
c_right = ats(*advectee, 1)

# Get diffusivity values
D_curr = ats(*diffusivity, 0)
D_right = ats(*diffusivity, 1)

# Input validation for diffusivity values
assert D_curr >= 0.0, "Diffusivity values must be non-negative"
assert D_right >= 0.0, "Diffusivity values must be non-negative"

# Handle near-zero diffusivity by setting minimum threshold
D_curr = max(D_curr, epsilon)
D_right = max(D_right, epsilon)

# Match the exact MPDATA pattern but with diffusivity weighting
# Regular: -2 * (c[i+1] - c[i]) / (c[i+1] + c[i] + epsilon)
# Heterogeneous: weight by diffusivity at face
D_face = 0.5 * (D_curr + D_right)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question on the Discretization of Interfacial Diffusivity

In the __make_heterogeneous_laplacian function, the diffusivity at the cell face is calculated using an arithmetic mean.

While this is a straightforward approach, for problems involving heterogeneous media, using a harmonic mean for the interfacial diffusivity is often recommended to ensure the continuity of the flux and improve the physical accuracy of the model.

Have you considered this alternative? For diffusivity profiles with sharp gradients, the harmonic mean can provide more robust and accurate results. It might be worth comparing both approaches.


return -2 * D_face * (c_right - c_curr) / (c_right + c_curr + epsilon)

return fun
11 changes: 10 additions & 1 deletion PyMPDATA/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
MPDATA variants, iterations, data-type and jit-flags settings
"""

from typing import Union

import numpy as np
from pystrict import strict

Expand Down Expand Up @@ -32,8 +34,9 @@ def __init__(
DPDC: bool = False, # pylint: disable=invalid-name
epsilon: float = 1e-15,
non_zero_mu_coeff: bool = False,
heterogeneous_diffusion: bool = False,
dimensionally_split: bool = False,
dtype: [np.float32, np.float64] = np.float64
dtype: Union[np.float32, np.float64] = np.float64,
):
self._values = HashableDict(
{
Expand All @@ -47,6 +50,7 @@ def __init__(
"dimensionally_split": dimensionally_split,
"dtype": dtype,
"DPDC": DPDC,
"heterogeneous_diffusion": heterogeneous_diffusion,
}
)

Expand Down Expand Up @@ -136,6 +140,11 @@ def dimensionally_split(self) -> bool:
"""flag disabling cross-dimensional terms in antidiffusive velocities"""
return self._values["dimensionally_split"]

@property
def heterogeneous_diffusion(self) -> bool:
"""flag enabling spatially varying diffusivity support"""
return self._values["heterogeneous_diffusion"]

def __str__(self):
return str(self._values)

Expand Down
19 changes: 16 additions & 3 deletions PyMPDATA/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def __init__(
stepper: Stepper,
advectee: ScalarField,
advector: VectorField,
g_factor: [ScalarField, None] = None,
g_factor: Union[ScalarField, None] = None,
diffusivity_field: Union[ScalarField, None] = None,
):
def scalar_field(dtype=None):
return ScalarField.clone(advectee, dtype=dtype)
Expand All @@ -64,8 +65,10 @@ def vector_field():
def null_vector_field():
return VectorField.make_null(advector.n_dims, stepper.traversals)

for field in [advector, advectee] + (
[g_factor] if g_factor is not None else []
for field in (
[advector, advectee]
+ ([g_factor] if g_factor is not None else [])
+ ([diffusivity_field] if diffusivity_field is not None else [])
):
assert field.halo == stepper.options.n_halo
assert field.dtype == stepper.options.dtype
Expand All @@ -75,6 +78,7 @@ def null_vector_field():
"advectee": advectee,
"advector": advector,
"g_factor": g_factor or null_scalar_field(),
"diffusivity_field": diffusivity_field or null_scalar_field(),
"vectmp_a": vector_field(),
"vectmp_b": vector_field(),
"vectmp_c": (
Expand Down Expand Up @@ -122,6 +126,12 @@ def g_factor(self) -> ScalarField:
e.g. the changing density of a fluid."""
return self.__fields["g_factor"]

@property
def diffusivity_field(self) -> ScalarField:
"""Diffusivity field (with halo), unmodified by advance(),
assumed to be constant-in-time. Used for heterogeneous diffusion."""
return self.__fields["diffusivity_field"]

def advance(
self,
n_steps: int,
Expand All @@ -138,9 +148,12 @@ def advance(
assert self.__stepper.options.non_zero_mu_coeff
else:
mu_coeff = (0.0, 0.0, 0.0)

# Check for heterogeneous diffusion
if (
self.__stepper.options.non_zero_mu_coeff
and not self.__fields["g_factor"].meta[META_IS_NULL]
and not self.__stepper.options.heterogeneous_diffusion
):
raise NotImplementedError()

Expand Down
16 changes: 13 additions & 3 deletions PyMPDATA/stepper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .impl.formulae_antidiff import make_antidiff
from .impl.formulae_axpy import make_axpy
from .impl.formulae_flux import make_flux_first_pass, make_flux_subsequent
from .impl.formulae_laplacian import make_laplacian
from .impl.formulae_laplacian import make_heterogeneous_laplacian, make_laplacian
from .impl.formulae_nonoscillatory import make_beta, make_correction, make_psi_extrema
from .impl.formulae_upwind import make_upwind
from .impl.meta import _Impl
Expand All @@ -34,7 +34,7 @@ def __init__(
grid: (tuple, None) = None,
n_threads: (int, None) = None,
left_first: (tuple, None) = None,
buffer_size: int = 0
buffer_size: int = 0,
):
if n_dims is not None and grid is not None:
raise ValueError()
Expand Down Expand Up @@ -144,6 +144,7 @@ def make_step_impl(
n_iters = options.n_iters
non_zero_mu_coeff = options.non_zero_mu_coeff
nonoscillatory = options.nonoscillatory
heterogeneous_diffusion = options.heterogeneous_diffusion

upwind = make_upwind(options, non_unit_g_factor, traversals)
flux_first_pass = make_flux_first_pass(options, traversals)
Expand All @@ -153,6 +154,9 @@ def make_step_impl(
non_unit_g_factor, options, traversals, last_pass=True
)
laplacian = make_laplacian(non_unit_g_factor, options, traversals)
heterogeneous_laplacian = make_heterogeneous_laplacian(
non_unit_g_factor, options, traversals
)
nonoscillatory_psi_extrema = make_psi_extrema(options, traversals)
nonoscillatory_beta = make_beta(non_unit_g_factor, options, traversals)
nonoscillatory_correction = make_correction(options, traversals)
Expand All @@ -168,6 +172,7 @@ def step(
advectee,
advector,
g_factor,
diffusivity_field,
vectmp_a,
vectmp_b,
vectmp_c,
Expand All @@ -185,7 +190,12 @@ def step(
if nonoscillatory:
nonoscillatory_psi_extrema(null_impl, psi_extrema, advectee)
if non_zero_mu_coeff:
laplacian(null_impl, advector, advectee)
if heterogeneous_diffusion:
heterogeneous_laplacian(
null_impl, advector, advectee, diffusivity_field
)
else:
laplacian(null_impl, advector, advectee)
axpy(
*advector.field,
mu_coeff,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Comparison between [the *py-pde* example](https://py-pde.readthedocs.io/en/latest/examples_gallery/simple_pdes/pde_heterogeneous_diffusion.html) of d iffusion equation with spatial dependence and the reimplementation with PyMPDATA.

diffusion_equation_with_spatial_dependence.ipynb:
.. include:: ./diffusion_equation_with_spatial_dependence.ipynb
"""
Loading
Loading