-
Notifications
You must be signed in to change notification settings - Fork 46
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
base: main
Are you sure you want to change the base?
Conversation
…-Carlo/PySDM_END into monte-carlo-impl
Updated the Explicit in space displacement function to be on one line
Updated displacement methods so that the new displacemnt functions compile
Added two numpy functions so the displacment functions uses
add test_single_cell
add the rest of the tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor fixes
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)) |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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 :)
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, unreadable.
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 |
There was a problem hiding this comment.
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 :)
sut, particulator = settings.get_displacement( | ||
backend_class, scheme="ImplicitInSpace" | ||
) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
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]]), | ||
) |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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): | |||
) |
There was a problem hiding this comment.
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 ):
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
rng, | |
rng.uniform(0., 1., displacement.data.shape[1]), |
n_substeps, | ||
enable_monte_carlo, | ||
rng, |
There was a problem hiding this comment.
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:
rng, | |
rng[droplet], |
The same for 2d
and 3d
CI fails (https://github.com/open-atmos/PySDM/actions/runs/15562732280/job/43825166589?pr=1647) due to pylint-reported issues:
and due to
For pre-commit help see e.g. https://github.com/open-atmos/python-dev-hints/wiki/Pre-commit-FAQ |
…ySDM_END into monte-carlo-impl
No description provided.