Skip to content
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

Return correct shapes from Correlator #3848

Merged
merged 11 commits into from
Aug 11, 2020
Merged

Conversation

jngrad
Copy link
Member

@jngrad jngrad commented Aug 3, 2020

Fixes #3577

Description of changes:

  • reshape Correlator output arrays based on the dimensions of the input observables
  • return only the correlated values from the Correlator observable
  • get time lags and sample sizes from dedicated class methods

@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

Review Jupyter notebook visual diffs & provide feedback on notebooks.


Powered by ReviewNB

@jngrad jngrad force-pushed the fix-3577 branch 2 times, most recently from 3760cda to a1c5795 Compare August 3, 2020 21:01
Use the shape(s) of the correlated observable(s) to deduce the shape
of the correlation output. Extract time lags and sample size from
the correlation matrix and provide methods `correlation_lags()`
and `correlation_sizes()` instead.
Used to throw cryptic runtime_errors when one of the test used an
observable tracking two particles (e.g. `No data can be added after
finalize() was called.` or `Particle node for id 1 not found!`).
@jngrad
Copy link
Member Author

jngrad commented Aug 3, 2020

@KaiSzuttor while working on this PR, I realized the output of the Correlator class could be ambiguous. From the user perspective, correlation happens on observables with shapes, but internally we correlate flattened arrays, which can lead to different results. For example, if we correlate the positions of particles with square_distance_componentwise resp. scalar_product, we get shapes [X, N, 3] resp. [X, 1] (instead of [X, N, 1]). Below is a MWE on a modified version of this PR, where the two exceptions scalar_product requires vectors, but observable ... is a matrix in src/core/accumulators/Correlator.cpp were removed:

import espressomd
from espressomd.accumulators import Correlator
from espressomd.observables import ParticlePositions
import numpy as np

system = espressomd.System(box_l=[1.0, 1.0, 1.0])
system.time_step = 0.05
system.cell_system.skin = 0.4
system.part.add(pos=[(0, 0, 0), (0.5, .5, 0)])
system.thermostat.set_langevin(kT=1.37, gamma=2.4, seed=42)

pos_obs = ParticlePositions(ids=(0,1))

c_pos1 = Correlator(obs1=pos_obs, tau_lin=16, tau_max=20., delta_N=10,
                   corr_operation="square_distance_componentwise",
                   compress1="discard1")
c_pos2 = Correlator(obs1=pos_obs, tau_lin=16, tau_max=20., delta_N=10,
                   corr_operation="scalar_product", compress1="discard1")
system.auto_update_accumulators.add(c_pos1)
system.auto_update_accumulators.add(c_pos2)

system.integrator.run(10000)

c_pos1.finalize()
c_pos2.finalize()
print(c_pos1.result().shape)  # output: (33, 2, 3)
print(c_pos2.result().shape)  # output: (33, 1)

For the tensor_product resp. scalar_product I've added extra checks to prevent passing two matrices as input, as we don't actually do Kronecker products resp. per-axis scalar products. Do you think it's the correct approach? Do we have a use for implementing the Kronecker product and per-axis scalar product (which is a simpler form of fcs_acf)?

@KaiSzuttor
Copy link
Member

so you mean the shape is a function not only of the observable type but also of the operation? Isn't that expected?

@jngrad
Copy link
Member Author

jngrad commented Aug 4, 2020

sure, but the scalar_product currently ignores the observable shape:

} else if (corr_operation_name == "scalar_product") {
m_dim_corr = 1;

this is surprising given that we already have the code logic to take the observable shape into account

@KaiSzuttor
Copy link
Member

So, the correlator has a bug which needs a fix before we can go on with returning the correct shape, right?

@jngrad
Copy link
Member Author

jngrad commented Aug 4, 2020

I just tried to implement a scalar_product_last_axis that would allow us to take e.g. a ParticlePositions(ids=(0,1)) and return the scalar product of the individual particles, e.g. a correlation shape of [N_tau, 2, 1], but it can't be done with our current infrastructure. The correlation functions take two flat arrays as input, so we cannot know whether we have two particles with 3 coordinates or 3 particles with 2 coordinates. The fcs_acf function "trusts" that the last dimension has size 3. It also checks the array dimension is a multiple of 3, but it's to avoid out-of-bounds errors, not to guarantee the user took the correct observable.
We also cannot provide information about the flat array dimensions as additional function arguments, because we would have to provide the same arguments to all other functions (they need to have the same signature because we store a function pointer).

This gives us 2 options: throw an error when doing a scalar product of matrices (that's currently implemented in acfa0b4) which is an API breaking change, or not throw an error and trust the user can figure out why the correlation shape is 1. Same thing for tensor product.

@KaiSzuttor
Copy link
Member

so our correlation function behaves like numpy correlate and assumes 1d data... I think there is no generic solution to correlating multidimensional data because the algorithms are not implemented in a generic fashion.

@jngrad
Copy link
Member Author

jngrad commented Aug 4, 2020

What should we do then? Right now I throw an error if the user accidentally passes a matrix instead of a vector to the scalar product, because it was not originally designed to handle this case. There was no mechanism to prevent it because it didn't really make sense back then to calculate the scalar product of a ParticlePositions(ids=(0,1)), as the user guide made it clear these positions were flattened. But now, the user guide shows these positions are stored in 2D matrices (but only at the script interface level!), which can give the impression to users that they should be able to "scalar product" these matrices.

I cannot think of an application where one would want to do a scalar product on flattened matrices, but maybe there is one, in which case this would no longer be possible because we do not offer the possibility to reshape an observable before passing it to the correlator.

We already have an m_correlation_args designed to help with this kind of situation, unfortunately it's constrained to a Vector3d:

  • using a boost::variant instead of Vector3d (aa0b4f8) won't work because the script interface would need a visitor pattern
  • storing the dimensionality of the data in e.g. the first index m_correlation_args would work, although it is now visible to the user and mutable (which is not good design)
  • replacing the function pointer by a functor means the correlation functions have a state that has to be serialized
  • adding extra parameters to all correlation functions doesn't scale; the core of the problem lies in the tight coupling between the Correlator class and its operators; right now they are implemented as free functions to be completely decoupled, which prevents us from generalizing the infrastructure

@KaiSzuttor
Copy link
Member

i'm not so sure if this is a cat 1 issue to be resolved next... let's wait for @RudolfWeeber

@jngrad
Copy link
Member Author

jngrad commented Aug 7, 2020

i'm not so sure if this is a cat 1 issue to be resolved next...

Agreed, refactoring the Correlator framework is out of scope for this PR. I've removed the API-breaking matrix assertions in 56764cb, we can always get back to that idea in the future. Let's move forward with this PR.

@jngrad jngrad marked this pull request as ready for review August 7, 2020 09:50
@jngrad jngrad added this to the Espresso 4.2 milestone Aug 7, 2020
@@ -138,7 +139,9 @@
" for i in range(LOOPS):\n",
" system.integrator.run(STEPS)\n",
" correlator.finalize()\n",
" msd_results.append(correlator.result())\n",
" msd_results.append(np.column_stack((correlator.lag_times(),\n",
Copy link
Member

Choose a reason for hiding this comment

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

@@ -385,7 +385,10 @@
"# Finalize the correlator and write to disk\n",
"system.auto_update_accumulators.remove(msd)\n",
"msd.finalize()\n",
"numpy.savetxt(\"output.dat\", msd.result())\n",
"numpy.savetxt(\"output.dat\",\n",
" numpy.column_stack((msd.lag_times(),\n",
Copy link
Member

Choose a reason for hiding this comment

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

KaiSzuttor
KaiSzuttor previously approved these changes Aug 10, 2020
Copy link
Member

@KaiSzuttor KaiSzuttor left a comment

Choose a reason for hiding this comment

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

LGTM

src/core/accumulators/Correlator.hpp Outdated Show resolved Hide resolved
@KaiSzuttor KaiSzuttor added the automerge Merge with kodiak label Aug 11, 2020
@kodiakhq kodiakhq bot merged commit 403df2d into espressomd:python Aug 11, 2020
@jngrad jngrad deleted the fix-3577 branch January 18, 2022 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

observable accumulators do not return correct shape
2 participants