Skip to content

Monte Carlo Method for Displacement Dynamic #1647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

Bodzio-2
Copy link

No description provided.

Copy link

@Eniterusx Eniterusx left a comment

Choose a reason for hiding this comment

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

minor fixes

Comment on lines 11 to 13
def displacement(_, position_in_cell, cell_id, c_l, c_r, enable_monte_carlo, u01):

return (position_in_cell + (np.floor(np.abs(max(c_l, c_r))) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) + (np.abs(max(c_l, c_r)) > u01) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) if enable_monte_carlo else ((c_l * (1 - position_in_cell) + c_r * position_in_cell) / (1 - c_r + c_l))
Copy link

@Eniterusx Eniterusx Jun 13, 2025

Choose a reason for hiding this comment

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

This line is absolutely unreadable. Please consider splitting it into a few lines for the sake of clarity.

Suggested change
def displacement(_, position_in_cell, cell_id, c_l, c_r, enable_monte_carlo, u01):
return (position_in_cell + (np.floor(np.abs(max(c_l, c_r))) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) + (np.abs(max(c_l, c_r)) > u01) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) if enable_monte_carlo else ((c_l * (1 - position_in_cell) + c_r * position_in_cell) / (1 - c_r + c_l))
def displacement(_, position_in_cell, cell_id, c_l, c_r, enable_monte_carlo, u01):
if enable_monte_carlo:
max_c = max(c_l, c_r)
abs_max_c = np.abs(max_c)
sign_max_c = np.sign(abs_max_c / max_c)
floor_term = np.floor(abs_max_c) * sign_max_c
jump_term = (abs_max_c > u01) * sign_max_c
return position_in_cell + floor_term + jump_term
else:
numerator = c_l * (1 - position_in_cell) + c_r * position_in_cell
denominator = 1 - c_r + c_l
normalized_position = numerator / denominator
return normalized_position

Choose a reason for hiding this comment

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

I have changed the file to make it more readable :)

Comment on lines 11 to 12
def displacement(_, position_in_cell, cell_id, c_l, c_r, enable_monte_carlo, u01):
return (position_in_cell + (np.floor(np.abs(max(c_l, c_r))) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) + (np.abs(max(c_l, c_r)) > u01) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) if enable_monte_carlo else (c_l * (1 - position_in_cell) + c_r * position_in_cell)
Copy link

@Eniterusx Eniterusx Jun 13, 2025

Choose a reason for hiding this comment

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

Same as above, unreadable.

Suggested change
def displacement(_, position_in_cell, cell_id, c_l, c_r, enable_monte_carlo, u01):
return (position_in_cell + (np.floor(np.abs(max(c_l, c_r))) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) + (np.abs(max(c_l, c_r)) > u01) * np.sign(np.abs(max(c_l, c_r)) / max(c_l, c_r))) if enable_monte_carlo else (c_l * (1 - position_in_cell) + c_r * position_in_cell)
def displacement(_, position_in_cell, cell_id, c_l, c_r, enable_monte_carlo, u01):
if enable_monte_carlo:
max_c = max(c_l, c_r)
abs_max_c = np.abs(max_c)
sign_max_c = np.sign(abs_max_c / max_c)
floor_term = np.floor(abs_max_c) * sign_max_c
jump_term = (abs_max_c > u01) * sign_max_c
return position_in_cell + floor_term + jump_term
else:
numerator = c_l * (1 - position_in_cell) + c_r * position_in_cell
return numerator

Choose a reason for hiding this comment

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

I have changed the file to make it more readable :)

Comment on lines 55 to 57
sut, particulator = settings.get_displacement(
backend_class, scheme="ImplicitInSpace"
)
Copy link

Choose a reason for hiding this comment

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

What do you think about parametrizing tests in this file with ["ImplicitInSpace", "ExplicitInSpace"]? You use them selectively, but probably both cases should be tested

Copy link
Author

Choose a reason for hiding this comment

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

Added parametrization in this test!

422d166

Comment on lines +166 to +172
value_a = 0.1
value_b = 0.2
weight = 0.125
settings.courant_field_data = (
np.array([[0, 0]]).T,
np.array([[value_a, value_b]]),
)
Copy link

Choose a reason for hiding this comment

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

Could you test behaviour with different Courant number e.g 0, 1, 2, 3, 1000?

Copy link

@kubijaku kubijaku Jun 23, 2025

Choose a reason for hiding this comment

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

I think that for basic checking thats enough but it can be added of course in a future

@@ -127,15 +128,16 @@ def test_calculate_displacement_dim1(backend_class):
)

Choose a reason for hiding this comment

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

The tests test_calculate_displacement and test_calculate_displacement_dim1 are very similar. They both test the calculate_displacement method, but with different input data to check behavior along different dimensions. This structural duplication can be reduced.

@staticmethod @pytest.mark.parametrize( "courant_field_data, positions, asserted_dimension, asserted_slice", [ ( (np.array([[0.1, 0.2]]).T, np.array([[0, 0]])), [[0.25], [0]], 0, slice(0, 1) ), ( (np.array([[0, 0]]).T, np.array([[0.1, 0.2]])), [[0], [0.25]], 1, slice(0, 1) ), ], ) def test_calculate_displacement_by_dimension( backend_class, courant_field_data, positions, asserted_dimension, asserted_slice ):

Copy link

Choose a reason for hiding this comment

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

You are absolutely right! Those two test methods are nearly identical and can definitely be refactored into a single parametrized test. Here is how I would implement your suggestion:

@staticmethod
@pytest.mark.parametrize(
    "courant_field_data, positions, asserted_dimension, asserted_slice",
    [
        (
            (np.array([[0.1, 0.2]]).T, np.array([[0, 0]])),
            [[0.25], [0]],
            0,
            slice(0, 1)
        ),
        (
            (np.array([[0, 0]]).T, np.array([[0.1, 0.2]])),
            [[0], [0.25]],
            1,
            slice(0, 1)
        ),
    ],
)
def test_calculate_displacement_by_dimension(
    backend_class, courant_field_data, positions, asserted_dimension, asserted_slice
):
    # Arrange
    settings = DisplacementSettings()
    value_a = 0.1
    value_b = 0.2
    weight = 0.25
    settings.courant_field_data = courant_field_data
    settings.positions = positions
    sut, particulator = settings.get_displacement(
        backend_class, scheme="ExplicitInSpace", adaptive=False, enable_monte_carlo=False
    )

    # Act
    sut.calculate_displacement(
        displacement=sut.displacement,
        courant=sut.courant,
        cell_origin=particulator.attributes["cell origin"],
        position_in_cell=particulator.attributes["position in cell"],
        cell_id=particulator.attributes["cell id"],
    )

    # Assert
    np.testing.assert_equal(
        sut.displacement[asserted_dimension, asserted_slice].to_ndarray(),
        (1 - weight) * value_a + weight * value_b,
    )

)

@staticmethod
def test_advection(backend_class):

Choose a reason for hiding this comment

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

I see the DisplacementSettings setup is repeated in most tests. Could we extract this into a shared pytest fixture to keep the tests DRY (Don't Repeat Yourself)?

):
if self.enable_sedimentation and self.enable_monte_carlo:
dt = self.particulator.dt / self._n_substeps
dt_over_dz = dt / self.particulator.mesh.dz

Choose a reason for hiding this comment

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

From above to variables names it is not obvious what they represent. Maybe delta_time and delta_altitude (if z is altitude) would be better?

Choose a reason for hiding this comment

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

Variable naming

Thank you for the proposed update concerning the naming of the certain variables!

I agree that clarity in variable naming is important, for both maintainability and readability of the code. In this case, however, the names dt and dt_over_dz are intentionally kept concise and consistent with standard nomenclature widely used in numerical methods and literature.

With that being said, we can consider adding a short inline comment to clarify their meaning if that would help in understanding the functionality.

if backend_class.__name__ == "Numba":
np.testing.assert_equal(
sut.displacement[0, slice(0, 1)].to_ndarray(),
0.125

Choose a reason for hiding this comment

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

So for different backends we get different results? Isn't that a little bit concerning?

Copy link
Author

Choose a reason for hiding this comment

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

This is because our solution didn't implement Monte Carlo in ThrustRTC (GPU), hence why it tests against values that you would get without Monte Carlo, this is surely something to add later down the line, just went out of scope of this PR.

position_in_cell=position_in_cell,
n_substeps=n_substeps,
enable_monte_carlo=enable_monte_carlo,
rng=self.Random(1,1).generator,

Choose a reason for hiding this comment

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

This function slowly starts being too complex. There are at least two flags used in a little bit confusing way (if flag1 and flag2 and after that if flag1 and not flag2).
The fact that we are passing rng which I presume we only need when enable_monte_carlo == True already sounds like reason enough to create a new function such as calculate_displacement_monte_carlo.

position_in_cell=position_in_cell,
n_substeps=n_substeps,
enable_monte_carlo=enable_monte_carlo,
rng=self.Random(1,1).generator,

Choose a reason for hiding this comment

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

In the displacement calculation, the Random generator is instantiated with a hardcoded seed (seed=1) for each backend call
rng=self.Random(1,1).generator
This makes the backend behavior fully deterministic, which is incorrect for Monte Carlo simulations.

position_in_cell.data,
trtc.DVInt64(n_substeps),
trtc.DVBool(enable_monte_carlo),
trtc.DVDouble(1.0) # TODO

Choose a reason for hiding this comment

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

In line 123, the value 1.0 is hardcoded:
trtc.DVDouble(1.0) # TODO

This leads to deterministic behavior in Monte Carlo simulations, which should instead use a random value from a uniform distribution. The propossed solution:
trtc.DVDouble(rng.uniform(0.0, 1.0))

This is the same issue as in the CPU backend, where the RNG is deterministically seeded – here the randomness is replaced by a constant.

Choose a reason for hiding this comment

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

Thank you for mentioning this, when I was debugging the GPU backend I must have missed this. I think now the problem is solved.

@@ -14,12 +14,15 @@
@numba.njit(**{**conf.JIT_FLAGS, **{"parallel": False}})
# pylint: disable=too-many-arguments
def calculate_displacement_body_common(
dim, droplet, scheme, _l, _r, displacement, courant, position_in_cell, n_substeps
dim, droplet, scheme, _l, _r, displacement, courant, position_in_cell, cell_id, n_substeps, enable_monte_carlo, rng
Copy link

@pepe5p pepe5p Jun 16, 2025

Choose a reason for hiding this comment

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

I see you pass rng object which is numpy Random Generator for this jit function. I think it would be better to pass already created random uniform distribution as array (see supported types for numba)

Choose a reason for hiding this comment

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

I have added your changes and the tests that I run locally run correctly :)

n_substeps,
enable_monte_carlo,
rng,
Copy link

Choose a reason for hiding this comment

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

According to previous command it is how the code would look like here. It is the same for 2d and 3d

Suggested change
rng,
rng.uniform(0., 1., displacement.data.shape[1]),

n_substeps,
enable_monte_carlo,
rng,
Copy link

Choose a reason for hiding this comment

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

Here it would be something like this:

Suggested change
rng,
rng[droplet],

The same for 2d and 3d

@slayoo
Copy link
Member

slayoo commented Jun 18, 2025

CI fails (https://github.com/open-atmos/PySDM/actions/runs/15562732280/job/43825166589?pr=1647) due to pylint-reported issues:

************* Module PySDM.backends.impl_numba.methods.displacement_methods
PySDM/backends/impl_numba/methods/displacement_methods.py:17:0: C0301: Line too long (119/100) (line-too-long)
PySDM/backends/impl_numba/methods/displacement_methods.py:34:0: C0301: Line too long (119/100) (line-too-long)
PySDM/backends/impl_numba/methods/displacement_methods.py:60:0: C0301: Line too long (119/100) (line-too-long)
PySDM/backends/impl_numba/methods/displacement_methods.py:92:0: C0301: Line too long (119/100) (line-too-long)
PySDM/backends/impl_numba/methods/displacement_methods.py:123:0: C0301: Line too long (120/100) (line-too-long)
************* Module PySDM.backends.impl_thrust_rtc.methods.displacement_methods
PySDM/backends/impl_thrust_rtc/methods/displacement_methods.py:105:0: C0301: Line too long (120/100) (line-too-long)
PySDM/backends/impl_thrust_rtc/methods/displacement_methods.py:105:117: W0613: Unused argument 'rng' (unused-argument)
************* Module PySDM.dynamics.displacement
PySDM/dynamics/displacement.py:144:69: C0303: Trailing whitespace (trailing-whitespace)
PySDM/dynamics/displacement.py:127:4: R0913: Too many arguments (6/5) (too-many-arguments)
************* Module PySDM.particulator
PySDM/particulator.py:429:0: C0301: Line too long (110/100) (line-too-long)
************* Module PySDM.physics.particle_advection.explicit_in_space
PySDM/physics/particle_advection/explicit_in_space.py:12:0: C0301: Line too long (282/100) (line-too-long)
PySDM/physics/particle_advection/explicit_in_space.py:11:4: R0913: Too many arguments (7/5) (too-many-arguments)
PySDM/physics/particle_advection/explicit_in_space.py:11:42: W0613: Unused argument 'cell_id' (unused-argument)
************* Module PySDM.physics.particle_advection.implicit_in_space
PySDM/physics/particle_advection/implicit_in_space.py:13:0: C0301: Line too long (302/100) (line-too-long)
PySDM/physics/particle_advection/implicit_in_space.py:11:4: R0913: Too many arguments (7/5) (too-many-arguments)
PySDM/physics/particle_advection/implicit_in_space.py:11:42: W0613: Unused argument 'cell_id' (unused-argument)
************* Module tests.unit_tests.dynamics.displacement.displacement_settings
tests/unit_tests/dynamics/displacement/displacement_settings.py:42:0: C0301: Line too long (125/100) (line-too-long)
************* Module tests.unit_tests.dynamics.displacement.test_monte_carlo
tests/unit_tests/dynamics/displacement/test_monte_carlo.py:161:0: C0303: Trailing whitespace (trailing-whitespace)

-----------------------------------
Your code has been rated at 9.99/10

and due to pre-commit not being run or installed (https://github.com/open-atmos/PySDM/actions/runs/15562732326/job/43825167611?pr=1647):

black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted PySDM/backends/impl_numba/methods/displacement_methods.py
reformatted PySDM/backends/impl_thrust_rtc/methods/displacement_methods.py
reformatted PySDM/dynamics/displacement.py
reformatted PySDM/physics/particle_advection/explicit_in_space.py
reformatted PySDM/physics/particle_advection/implicit_in_space.py
reformatted PySDM/particulator.py
reformatted tests/unit_tests/dynamics/displacement/displacement_settings.py
reformatted tests/unit_tests/dynamics/displacement/test_advection.py
reformatted tests/unit_tests/dynamics/displacement/test_monte_carlo.py

All done! ✨ 🍰 ✨
9 files reformatted, 819 files left unchanged.

For pre-commit help see e.g. https://github.com/open-atmos/python-dev-hints/wiki/Pre-commit-FAQ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.