Adding Chebyshev anisotropic pair potential#121
Conversation
mphoward
left a comment
There was a problem hiding this comment.
This is a good start! Here are some comments, let me know what questions you have.
mphoward
left a comment
There was a problem hiding this comment.
This is going in a good direction. Let's finalize the interface, especially the constructor, then start to implement that.
| const std::vector<Scalar>& r0_data, | ||
| const std::vector<unsigned int>& terms, | ||
| const std::vector<Scalar>& coeffs); | ||
|
|
There was a problem hiding this comment.
r0_data should be a raw pointer to Scalar so that we can copy the data in-place from a NumPy array. You will need a second raw pointer or a std::vector that stores the dimensionality of r0_data along each of the 5 dimensions.
terms and coeffs also need to be raw pointers, and you'll need one more variable that is the number of terms.
Last, domain should probably also be a raw pointer to a regular Scalar so you can accept a NumPy array that is (6,2) in shape. Otherwise, it can be a std::vector<Scalar2>, and you can copy into it.
| } | ||
|
|
||
| /// r0 data (theta, phi, alpha, beta, gamma) (N x 6) | ||
| const GPUArray<Scalar>& getR0Data() const |
There was a problem hiding this comment.
Do you want to be expose the R0Data data directly after you construct the class? Same questions go for some of the members below that return GPUArray. You have to decide whether you are OK with the user modifying their contents or not.
| /// Allocate storage for term list and coefficients. | ||
| void resizeTerms(unsigned int Nterms); |
There was a problem hiding this comment.
Is this something we want the user to be able to do? It seems like you wouldn't want them to do that themselves, most likely. If it's a method the class needs to trigger, though, it can be protected.
| protected: | ||
| void computeForces(uint64_t timestep) override; | ||
|
|
||
| private: |
There was a problem hiding this comment.
Most of the members in this section will need to be protected not private if the GPU version of the class is going to access them.
mphoward
left a comment
There was a problem hiding this comment.
Looks good to me, let's start on the implementation!
| unsigned int Nterms, | ||
| const unsigned int* terms, | ||
| const Scalar* coeffs); |
There was a problem hiding this comment.
I like to follow the convention that the number of items comes after the pointers
| unsigned int Nterms, | |
| const unsigned int* terms, | |
| const Scalar* coeffs); | |
| const unsigned int* terms, | |
| const Scalar* coeffs, | |
| unsigned int Nterms); |
| const Scalar* r0_data, | ||
| const unsigned int* r0_shape, |
There was a problem hiding this comment.
Logically, should these come before the things related to the potential approximation or after? It doesn't really matter, beyond which way it makes more sense for you to read them.
| // neighbor list object | ||
| std::shared_ptr<hoomd::md::NeighborList> m_nlist; |
There was a problem hiding this comment.
Member variables should be documented like this in doxygen style
| // neighbor list object | |
| std::shared_ptr<hoomd::md::NeighborList> m_nlist; | |
| std::shared_ptr<hoomd::md::NeighborList> m_nlist; //!< Neighbor list |
| // intenal r0 linear interpolator | ||
| std::unique_ptr<LinearInterpolator5D> m_r0_interp; | ||
|
|
||
| // r0_data | ||
| GPUArray<Scalar> m_r0_data; | ||
|
|
||
| std::array<unsigned int, 5> m_r0_shape; |
There was a problem hiding this comment.
We should discuss how the linear interpolator will work. It may not be necessary to store a separate variable for it, and instead just construct it each time it is needed because it will be a lightweight wrapper around the data and the shape.
Noting: if the shape is stored in an array, it will need to be copied into the interpolator to pass it to the GPU.
| GPUArray<Scalar> m_coeffs; | ||
|
|
||
| // number of terms | ||
| unsigned int m_Nterms = 0; |
There was a problem hiding this comment.
This doesn't need a default value assigned to it
| unsigned int m_Nterms = 0; | |
| unsigned int m_Nterms; |
mphoward
left a comment
There was a problem hiding this comment.
This is moving in a good direction! Please see my comments and let me know what questions you have.
| domain = numpy.zeros((5, 2), dtype=numpy.float64) | ||
| terms = numpy.zeros((2, 6), dtype=numpy.uint32) |
There was a problem hiding this comment.
I would suggest giving the domain and terms some real values, not just zeros, as it may cause problems later once we start implementing things.
| assert pot._cpp_obj.n_terms == 2 | ||
| assert numpy.isclose(pot._cpp_obj.r_cut, r_cut) |
There was a problem hiding this comment.
_cpp_obj is an implementation detail, so we should not test it in detail. The check above that it exists and is not None is OK though.
| assert pot._cpp_obj.n_terms == 2 | |
| assert numpy.isclose(pot._cpp_obj.r_cut, r_cut) |
| @property | ||
| def r_cut(self): | ||
| """Cut-off distance in approximation domain""" | ||
| return self._r_cut |
There was a problem hiding this comment.
You shouldn't need a property for the ones coming from C++, but you need to use the ParameterDict correctly for those.
| @property | |
| def r_cut(self): | |
| """Cut-off distance in approximation domain""" | |
| return self._r_cut |
| @property | ||
| def n_terms(self): | ||
| """Number of terms.""" | ||
| return int(self._terms.shape[0]) |
There was a problem hiding this comment.
Same here, this should come from C++ automatically.
| @property | |
| def n_terms(self): | |
| """Number of terms.""" | |
| return int(self._terms.shape[0]) |
| @property | ||
| def r0_shape(self): | ||
| """r0 table shape.""" | ||
| return tuple(int(x) for x in self._r0_data.shape) |
There was a problem hiding this comment.
We will want to discuss how to expose the array quantities later on. However, I note for now that we don't want to have a special shape property for r0. It should be a NumPy inside this class, so it should work like r0.shape.
mphoward
left a comment
There was a problem hiding this comment.
This is moving in a very good direction!
| } | ||
|
|
||
| { | ||
| GPUArray<Scalar> r0_arr(n_r0, m_exec_conf); |
There was a problem hiding this comment.
Is GPUArray OK with a size_t number (no warnings)?
| // need to start from a zero force and torque | ||
| m_force.zeroFill(); | ||
| m_torque.zeroFill(); | ||
|
|
||
| ArrayHandle<Scalar4> h_force(m_force, access_location::host, access_mode::readwrite); | ||
| ArrayHandle<Scalar4> h_torque(m_torque, access_location::host, access_mode::readwrite); |
There was a problem hiding this comment.
Is this the same pattern used in PotentialPair? A more efficient memory access could be to grab these in overwrite, then memset to zero. But, Josh changed alot of those calls to use this zeroFill method, so we should follow the same pattern as him. Just double check.
There was a problem hiding this comment.
zeroFill is used in PotentialPair. I kept it the same.
mphoward
left a comment
There was a problem hiding this comment.
This is really nicely done! Here are some suggestions, but they don't all necessarily need to be done now. Take a look and let me know what questions you have, then I think we should try the GPU implementation if you are happy with the CPU one.
| Scalar m_nlist_r_cut; //!< Neighbor-list cutoff = ceil(max(r0) + r_cut) | ||
|
|
||
| std::shared_ptr<GPUArray<Scalar>> m_r_cut_nlist; //!< r_cut matrix shared with nlist | ||
| bool m_attached = true; //!< Whether attached to the simulation |
There was a problem hiding this comment.
Why is this being declared here? I am not used to seeing this in HOOMD code for force computes, but maybe it was added later to code that uses neighbor lists.
There was a problem hiding this comment.
I think PotentialPair is using the same pattern.
| Scalar max_r0 = *std::max_element(r0_data, r0_data + n_r0); | ||
| m_nlist_r_cut = std::ceil(max_r0 + m_r_cut); | ||
|
|
||
| m_r_cut_nlist = std::make_shared<GPUArray<Scalar>>(1, m_exec_conf); |
There was a problem hiding this comment.
Doesn't this need to be sized large enough to be all the types in the system? Probably you need to do that here and fill them all in.
Longer term, we should make a plan for how we deal with which particles this potential applies to.
| // Determine the maximum Chebyshev degree needed for each of the 6 coordinates | ||
| unsigned int max_deg[6] = {0, 0, 0, 0, 0, 0}; | ||
| for (unsigned int t = 0; t < m_Nterms; ++t) | ||
| { | ||
| for (unsigned int c = 0; c < 6; ++c) | ||
| { | ||
| const unsigned int deg = h_terms.data[t * 6 + c]; | ||
| if (deg > max_deg[c]) | ||
| max_deg[c] = deg; | ||
| } | ||
| } | ||
|
|
||
| // Chain-rule scale factors: d(x_scaled)/d(x) = 2 / (hi - lo) | ||
| Scalar cheb_scale[6]; | ||
| cheb_scale[0] = Scalar(2); | ||
| for (unsigned int d = 0; d < 5; ++d) | ||
| { | ||
| cheb_scale[d + 1] = Scalar(2) / (h_domain.data[d].y - h_domain.data[d].x); | ||
| } | ||
|
|
||
| // Flat 1D Chebyshev storage | ||
| unsigned int max_deg_global = 0; | ||
| for (unsigned int c = 0; c < 6; ++c) | ||
| { | ||
| if (max_deg[c] > max_deg_global) | ||
| max_deg_global = max_deg[c]; | ||
| } | ||
|
|
||
| const Index2D cheb_idx(max_deg_global + 1, 6); | ||
| std::vector<Scalar> cheb_T_flat(cheb_idx.getNumElements()); | ||
| std::vector<Scalar> cheb_dT_flat(cheb_idx.getNumElements()); |
There was a problem hiding this comment.
This should be done during setup since it does not change. You can add a separate method if it is needed to keep the code cleaner.
Also, noting that the Index2D solution is fine for now, but we will probably want to use a different structure later to save memory on the GPU. These storage requirements will be much larger than necessary if you have an anisotropic grid.
| //! beta threshold for the Jacobian (avoids 1/sin(beta) singulrity). | ||
| const Scalar beta_tol = Scalar(1e-5); |
There was a problem hiding this comment.
This is coded into the lower bounds of the domain, no? Can probably remove and use that.
Or, maybe not—should the lower bounds of the domain just be the true 0s (for sampling purposes), and then the nonzero values are used here for evaluating?
There was a problem hiding this comment.
Since we are representing orientation with ZXZ Euler angles and reducing configurations in the same coordinates, the force and torque expressions in these variables become singular at beta = 0 and beta = pi, where sin(beta) = 0. Because the interpolator is also constructed on a domain that excludes those singular endpoints, I think it is safest to keep the current nonzero lower bound in the symmetry class and use that same bound consistently during evaluation.
No description provided.