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

Linear elastic FFT solver #441

Merged
merged 15 commits into from
Oct 6, 2020
Merged

Linear elastic FFT solver #441

merged 15 commits into from
Oct 6, 2020

Conversation

recuero
Copy link
Collaborator

@recuero recuero commented Aug 3, 2020

This PR aims at solving a periodic small deformation problem with multiple phases using a spectral approach.

  • Initial comparison with FE solution
  • Verify for various global strains
  • Establish a convergence criterion and include it in the test
  • Handle graciously the various strain modes, give option to user
  • Let the user choose a "homogeneized" material ratio for the solver

Refs. #401

To make this run, explicit instantiation of

auto RankFourTensorTempl<T>::operator*(const Tensor<T2> & b) const

is required in MOOSE's RankFourTensor.h
@recuero recuero force-pushed the smallElasticFFT branch 2 times, most recently from 4e68f33 to 786951b Compare August 7, 2020 21:31
@moosebuild
Copy link

Job Precheck on 786951b wanted to post the following:

Your code requires style changes.

A patch was auto generated and copied here
You can directly apply the patch by running, in the top level of your repository:

curl -s https://mooseframework.inl.gov/magpie/docs/PRs/441/style.patch | git apply -v

Alternatively, with your repository up to date and in the top level of your repository:

git clang-format c19bd79fd44acf91a4276b80db1d1275daa319ee

@moosebuild
Copy link

moosebuild commented Aug 7, 2020

Job Test on 4383ed8 wanted to post the following:

View the site here

This comment will be updated on new commits.

@recuero recuero marked this pull request as ready for review August 7, 2020 22:44
@recuero recuero requested a review from dschwen August 7, 2020 22:45
youngs_modulus = 1.0e4
poissons_ratio = 0.3
elasticity_tensor_prefactor = young_modulus_function
# Is the line above during the righ thing by scaling the young modulus?
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Is the line above during the righ thing by scaling the young modulus?
# Is the line above during the right thing by scaling the young modulus?

Copy link
Member

Choose a reason for hiding this comment

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

Yes

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That comment shouldn't be there anymore...

Comment on lines 70 to 77
for (int freq_x = 0; freq_x < ni; ++freq_x)
for (int freq_y = 0; freq_y < nj; ++freq_y)
for (int freq_z = 0; freq_z < nk; ++freq_z)
{
epsilon_buffer.realSpace()[index] = _initial_strain_tensor;
index++;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

how about we add

FFTData<T> & operator=(const T & rhs);

which internally uses std::fill to set the data. This will be more compact and easier to optimize for the compiler.

Copy link
Member

Choose a reason for hiding this comment

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

The code in this file would then just be

epsilon_buffer.realSpace() = _initial_strain_tensor;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes it much cleaner. The meaning of the operation becomes more implicit at first sight though.


const int ni = grid[0];
const int nj = grid[1];
const int nk = (grid[2] >> 1) + 1;
Copy link
Member

Choose a reason for hiding this comment

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

should probably be gamma_hat.kTable(2).size(), right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope. We are mindlessly resizing and filling out the last dimension of _k_table (should we change this?). In addition, it should not make a difference for this problem as all variables and parameters are defined on the same grid.

 // precompute kvector components for current direction
    _k_table[i].resize(_grid[i]);
    for (int j = 0; j < _grid[i]; ++j)
      _k_table[i][j] = 2.0 * libMesh::pi *
                       ((j < (_grid[i] >> 1) + 1) ? Real(j) : (Real(j) - _grid[i])) / _box_size(i);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess I am changing it to address other comments

Complex q_square = freq[0] * freq[0] + freq[1] * freq[1] + freq[2] * freq[2];
if (std::abs(q_square) > 1.0e-12)
{
gamma_hat.reciprocalSpace()[index](i, j, k, l) =
Copy link
Member

Choose a reason for hiding this comment

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

it might be best to grab a reference to gamma_hat.reciprocalSpace() outside of the loop.

auto & gamma_hat_F = gamma_hat.reciprocalSpace()

(or whatever naming convention you want to adopt for recoprocal fields)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would just be saving the function call, right?

Copy link
Member

Choose a reason for hiding this comment

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

A function call in a tight inner loop. As that function is implemented in the header file there is a very good chance the compiler will inline it, however we might as well be explicit about it.

Comment on lines 152 to 159
for (int freq_x = 0; freq_x < ni; ++freq_x)
for (int freq_y = 0; freq_y < nj; ++freq_y)
for (int freq_z = 0; freq_z < nk; ++freq_z)
{
stress_buffer.realSpace()[index] =
elastic_tensor.realSpace()[index] * epsilon_buffer.realSpace()[index];
index++;
}
Copy link
Member

@dschwen dschwen Aug 10, 2020

Choose a reason for hiding this comment

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

We should have a templated helper function to do these types multiplications in FFTData

template <typename T>
template <typename T1, typename T2>
void FFTData<T>::setToProduct(const FFTData<T1> & m1, const FFTData<T2> & m2)
{
  // assert that grid sizes are identical
  // loop over indices and multiply
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Grid size information is in FFTBuffer. In FFTData you have either a real or a complex buffer. I am adding a version of your suggestion.

epsilon_buffer.reciprocalSpace()[index] = _initial_strain_tensor;
else
epsilon_buffer.reciprocalSpace()[index] -=
gamma_hat.reciprocalSpace()[index] * stress_buffer.reciprocalSpace()[index];
Copy link
Member

Choose a reason for hiding this comment

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

For this loop we could provide a helper function that takes a Lambda as an argument

template<typename T>
template <typename T1>
FFTData<T>::apply(T1 lambda)
{
  size_t index = 0;
  for (auto ivec : kTable(0))
    for (auto jvec : kTable(1))
      for (auto kvec : kTable(2))
        _buffer[index++] = lambda(ivec, jvec, kvec, index);
}

(unfortunately FFTData does not have access to the kTable yet, but we can probably store a referece to the FFTBufferBase object that owns the FFTData.)

so the call would look something like

epsilon_buffer.reciprocalSpace().apply([](Real ivec, Real jvec, Real kvec, std::size_t index) {
    if (ivec == 0 && jvec == 0 && kvec == 0)
      return _initial_strain_tensor;
    else
      return gamma_hat.reciprocalSpace()[index] * stress_buffer.reciprocalSpace()[index];
} );

Since we are using templating to pass in the lambda the compiler will inline it and should produce very fast code.

Also you ultimately will want to do

epsilon_buffer.reciprocalSpace().apply([](Real ivec, Real jvec, Real kvec, std::size_t index) {
    return gamma_hat.reciprocalSpace()[index] * stress_buffer.reciprocalSpace()[index];
} );
epsilon_buffer.setKZero(_initial_strain_tensor);

where setKZero needs to be implemented (and will simply write to the index that corresponds to the (0,0,0) k-vector. That way you'll write twice to that location but avoid looping over the conditional. This is likely much faster.

for (int freq_z = 0; freq_z < nk; ++freq_z)
{
stress_buffer.realSpace()[index] =
(elastic_tensor.realSpace()[index]) * epsilon_buffer.realSpace()[index];
Copy link
Member

@dschwen dschwen Aug 10, 2020

Choose a reason for hiding this comment

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

see above for the setToProduct function

for (int freq_z = 0; freq_z < nk; ++freq_z)
{
// Define elastic tensor
std::vector<Real> iso_const(2);
Copy link
Member

Choose a reason for hiding this comment

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

avoid creating short lived temporary vectors like the plague. They entail heap allocations and are thus very slow.

Copy link
Member

Choose a reason for hiding this comment

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

I.e. move it out of the loop and since the size is known at compile time we could also think about using arrays (and templating the fillFormVector routines).

stress.reciprocalSpace()[index](2, 2) * freq[2];

error_n += kvector_stress[0] * kvector_stress[0] + kvector_stress[1] * kvector_stress[1] +
kvector_stress[1] * kvector_stress[1];
Copy link
Member

Choose a reason for hiding this comment

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

is this a typo? I'd expect some [2] here

{
const std::array<Complex, 3> freq{ivec[freq_x] * I, jvec[freq_y] * I, kvec[freq_z] * I};

std::array<Complex, 3> kvector_stress;
Copy link
Member

Choose a reason for hiding this comment

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

If you used ComplexVectorValue this would be a lot more compact.

Mostly delegate some buffer operations to FFTData<T>.
Use of lambdas.
Comment on lines 208 to 217
ComplexVectorValue kvector_stress;
kvector_stress(0) = stress.reciprocalSpace()[index](0, 0) * freq(0) +
stress.reciprocalSpace()[index](1, 0) * freq(1) +
stress.reciprocalSpace()[index](2, 0) * freq(2);
kvector_stress(1) = stress.reciprocalSpace()[index](0, 1) * freq(0) +
stress.reciprocalSpace()[index](1, 1) * freq(1) +
stress.reciprocalSpace()[index](2, 1) * freq(2);
kvector_stress(2) = stress.reciprocalSpace()[index](0, 2) * freq(0) +
stress.reciprocalSpace()[index](1, 2) * freq(1) +
stress.reciprocalSpace()[index](2, 2) * freq(2);
Copy link
Member

Choose a reason for hiding this comment

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

isn't that just

kvector_stress = stress.reciprocalSpace()[index] * freq;

??

Comment on lines +219 to +220
error_n += kvector_stress(0) * kvector_stress(0) + kvector_stress(1) * kvector_stress(1) +
kvector_stress(2) * kvector_stress(2);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
error_n += kvector_stress(0) * kvector_stress(0) + kvector_stress(1) * kvector_stress(1) +
kvector_stress(2) * kvector_stress(2);
error_n += kvector_stress.norm_sq();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

.norm_sq() takes the norm of each component's complex number, not what we look for here.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, it would be the component times its complex conjugate rather than component square? Thats really not what is wanted here?

Copy link
Collaborator Author

@recuero recuero Aug 12, 2020

Choose a reason for hiding this comment

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

norm_sq obtains the norm of each of the components and sums them up. The error formula I am basing this off takes the norm of the entire vector. Results are different.

There is more work to do in terms of convergence checks. The formula implemented here captures stress equilibrium. But we could add BC error and others. We can leave all this for next revision.

for (int i = 0; i < ndim; i++)
for (int j = 0; j < ndim; j++)
error_0 += stress.reciprocalSpace()[0](i, j) * stress.reciprocalSpace()[0](i, j);
error_0 = stress.reciprocalSpace()[0].L2norm() * stress.reciprocalSpace()[0].L2norm();
Copy link
Member

Choose a reason for hiding this comment

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

not to self: add RankTwoTensorTempl<T>::L2norm_sq() analogous to norm_sq()

{
mooseAssert(grid.size() == 3, "Product defined for 3 dimensions");

for (unsigned int index = 0; index < grid[0] * grid[1] * grid[2]; index++)
Copy link
Member

Choose a reason for hiding this comment

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

passing in grid seems unnecessary if you can just evaluate _buffer.size()

@dschwen dschwen self-assigned this Aug 24, 2020
@dschwen dschwen merged commit 1c702b2 into idaholab:devel Oct 6, 2020
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.

3 participants