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

Water/steam model for PorousFlow #13163

Merged
merged 24 commits into from
May 9, 2019
Merged

Water/steam model for PorousFlow #13163

merged 24 commits into from
May 9, 2019

Conversation

cpgr
Copy link
Contributor

@cpgr cpgr commented Apr 3, 2019

Implements a water/steam model based on pressure and enthalpy using AD to compute all of the tricky derivatives.

A lot of this is some machinery to make it easier to implement this model - the actual water/steam model is only a few hundred lines - as well as documentation and tests

This model will be useful for geothermal, as it allows phase changes to occur (from water to vapour and vice-versa).

Closes #13108

@moosebuild
Copy link
Contributor

moosebuild commented Apr 3, 2019

Job Documentation on 5329425 wanted to post the following:

View the site here

This comment will be updated on new commits.

@cpgr
Copy link
Contributor Author

cpgr commented Apr 3, 2019

Hi @lindsayad

I ran into an issue when I had something like std::pow(eta - 2.1, 0) where eta is a DualReal. When eta - 2.1 went negative, I hit a FPE in dbg, and can't figure out why.

I verified that doing something like

DualReal x = -0.1;

std::pow(x, 0)

works as expected (outputs 1).

The stack trace looks like:

Floating point exception signaled (floating point inexact result)!

To track this down, compile in debug mode, then in gdb do:
  break libmesh_handleFPE
  run ...
  bt
Stack frames: 36
0: 0   libmesh_dbg.0.dylib                 0x00000001099a8800 libMesh::print_trace(std::__1::basic_ostream<char, std::__1::char_traits<char> >&) + 2192
1: 1   libmesh_dbg.0.dylib                 0x000000010999299e libMesh::MacroFunctions::report_error(char const*, int, char const*, char const*) + 366
2: 2   libmesh_dbg.0.dylib                 0x000000010994c410 (anonymous namespace)::libmesh_handleFPE(int, __siginfo*, void*) + 1968
3: 3   libsystem_platform.dylib            0x00007fffa82e6b3a _sigtramp + 26
4: 4   ???                                 0x0000000000000000 0x0 + 0
5: 5   libbilby-dbg.0.dylib                0x0000000101c7c295 MetaPhysicL::CompareTypes<MetaPhysicL::DualNumber<double, MetaPhysicL::NumberArray<50ul, double> >, int, false, void>::supertype std::pow<double, int, MetaPhysicL::NumberArray<50ul, double> >(MetaPhysicL::DualNumber<double, MetaPhysicL::NumberArray<50ul, double> > const&, int const&) + 85
6: 6   libfluid_properties-dbg.0.dylib     0x0000000104ed183a Water97FluidProperties::temperature_from_ph2a(MetaPhysicL::DualNumber<double, MetaPhysicL::NumberArray<50ul, double> >, MetaPhysicL::DualNumber<double, MetaPhysicL::NumberArray<50ul, double> >) const + 650
7: 7   libfluid_properties-dbg.0.dylib     0x0000000104ed0b31 Water97FluidProperties::T_from_p_h(MetaPhysicL::DualNumber<double, MetaPhysicL::NumberArray<50ul, double> >, MetaPhysicL::DualNumber<double, MetaPhysicL::NumberArray<50ul, double> >) const + 913
8: 8   libporous_flow-dbg.0.dylib          0x0000000102174490 PorousFlowWaterVapor::thermophysicalProperties(double, double, unsigned int, FluidStatePhaseEnum&, std::__1::vector<FluidStatePropertiesAD, std::__1::allocator<FluidStatePropertiesAD> >&) const + 2336
9: 9   libporous_flow-dbg.0.dylib          0x00000001020758b1 PorousFlowFluidStateSingleComponent::thermophysicalProperties() + 177
10: 10  libporous_flow-dbg.0.dylib          0x000000010207665c PorousFlowFluidStateSingleComponent::computeQpProperties() + 92
11: 11  libmoose-dbg.0.dylib                0x000000010677435a Material::computeProperties() + 2490
12: 12  libporous_flow-dbg.0.dylib          0x00000001020889bd PorousFlowMaterial::computeProperties() + 157
13: 13  libmoose-dbg.0.dylib                0x0000000106775d7d MaterialData::reinit(std::__1::vector<std::__1::shared_ptr<Material>, std::__1::allocator<std::__1::shared_ptr<Material> > > const&) + 413
14: 14  libmoose-dbg.0.dylib                0x000000010700082a FEProblemBase::reinitMaterials(unsigned short, unsigned int, bool) + 1018
15: 15  libmoose-dbg.0.dylib                0x00000001066786fe ComputeResidualThread::onElement(libMesh::Elem const*) + 270
16: 16  libmoose-dbg.0.dylib                0x00000001068f47f3 ThreadedElementLoopBase<libMesh::StoredRange<libMesh::MeshBase::const_element_iterator, libMesh::Elem const*> >::operator()(libMesh::StoredRange<libMesh::MeshBase::const_element_iterator, libMesh::Elem const*> const&, bool) + 707
17: 17  libmoose-dbg.0.dylib                0x00000001072b608a void libMesh::Threads::parallel_reduce<libMesh::StoredRange<libMesh::MeshBase::const_element_iterator, libMesh::Elem const*>, ComputeResidualThread>(libMesh::StoredRange<libMesh::MeshBase::const_element_iterator, libMesh::Elem const*> const&, ComputeResidualThread&) + 138
18: 18  libmoose-dbg.0.dylib                0x00000001072aaf87 NonlinearSystemBase::computeResidualInternal(std::__1::set<unsigned int, std::__1::less<unsigned int>, std::__1::allocator<unsigned int> > const&) + 839
19: 19  libmoose-dbg.0.dylib                0x00000001072aa7c3 NonlinearSystemBase::computeResidualTags(std::__1::set<unsigned int, std::__1::less<unsigned int>, std::__1::allocator<unsigned int> > const&) + 1267
20: 20  libmoose-dbg.0.dylib                0x0000000107032d1f FEProblemBase::computeResidualTags(std::__1::set<unsigned int, std::__1::less<unsigned int>, std::__1::allocator<unsigned int> > const&) + 2719
21: 21  libmoose-dbg.0.dylib                0x00000001070319db FEProblemBase::computeResidualInternal(libMesh::NumericVector<double> const&, libMesh::NumericVector<double>&, std::__1::set<unsigned int, std::__1::less<unsigned int>, std::__1::allocator<unsigned int> > const&) + 283
22: 22  libmoose-dbg.0.dylib                0x000000010703140f FEProblemBase::computeResidual(libMesh::NumericVector<double> const&, libMesh::NumericVector<double>&) + 1439
23: 23  libmoose-dbg.0.dylib                0x0000000107030d82 FEProblemBase::computeResidualSys(libMesh::NonlinearImplicitSystem&, libMesh::NumericVector<double> const&, libMesh::NumericVector<double>&) + 98
24: 24  libmoose-dbg.0.dylib                0x0000000107291ad2 NonlinearSystem::solve() + 322
25: 25  libmoose-dbg.0.dylib                0x000000010702c735 FEProblemBase::solve() + 165
26: 26  libmoose-dbg.0.dylib                0x0000000106341cf4 FEProblemSolve::solve() + 36
27: 27  libmoose-dbg.0.dylib                0x000000010634b6da PicardSolve::solveStep(double, double&, double, double&, bool, std::__1::set<unsigned int, std::__1::less<unsigned int>, std::__1::allocator<unsigned int> > const&) + 1882
28: 28  libmoose-dbg.0.dylib                0x00000001063494be PicardSolve::solve() + 5262
29: 29  libmoose-dbg.0.dylib                0x000000010747ab34 TimeStepper::step() + 52
30: 30  libmoose-dbg.0.dylib                0x000000010635763c Transient::takeStep(double) + 284
31: 31  libmoose-dbg.0.dylib                0x0000000106356d91 Transient::execute() + 145
32: 32  libmoose-dbg.0.dylib                0x00000001077be8eb MooseApp::executeExecutioner() + 235
33: 33  libmoose-dbg.0.dylib                0x00000001077c011f MooseApp::run() + 335
34: 34  bilby-dbg                           0x0000000101be0993 main + 275
35: 35  libdyld.dylib                       0x00007fffa80d7235 start + 1

@lindsayad
Copy link
Member

What's the easiest way for me to reproduce?

@cpgr
Copy link
Contributor Author

cpgr commented Apr 4, 2019

Hi @WilkAndy,

I added some write up of some tests that I have added - see https://mooseframework.inl.gov/docs/PRs/13163/site/modules/porous_flow/verification_problems/index.html

Is this the sort of documentation that you were thinking of for these tests that have analytical or benchmark solutions?

@WilkAndy
Copy link
Contributor

WilkAndy commented Apr 4, 2019

Yea, this looks excellent (great agreement with external sources too!). My current setup is: for all tests in porous_flow/test/tests/some_dir, i write a markdown document porous_flow/doc/content/modules/porous_flow/tests/some_dir/some_dir_tests.md that describes all the tests. In that "doc" directory i also keep all the png files and python files needed to create those png files for the some_dir tests.

By the way, is it possible to compare the CPU time in MOOSE with TOUGH for these tests, or is that annoying/impossible?

@cpgr
Copy link
Contributor Author

cpgr commented Apr 4, 2019

Shouldn't be too hard to do

@cpgr
Copy link
Contributor Author

cpgr commented Apr 4, 2019

@lindsayad - I haven't been able to come up with a simple example yet. I'll try again tomorrow

@cpgr
Copy link
Contributor Author

cpgr commented Apr 4, 2019

Hi @lindsayad

I can't replicate this fpe, so I guess that it is happening somewhere else.

In case you wanted to take a look, you can grab this branch and apply the following patch:

diff --git a/modules/fluid_properties/src/userobjects/Water97FluidProperties.C b/modules/fluid_properties/src/userobjects/Water97FluidProperties.C
index 6c126fc106..260880bac1 100644
--- a/modules/fluid_properties/src/userobjects/Water97FluidProperties.C
+++ b/modules/fluid_properties/src/userobjects/Water97FluidProperties.C
@@ -1772,6 +1772,10 @@ Water97FluidProperties::temperature_from_ph2a(const FPDualReal & pressure,
   FPDualReal sum = 0.0;

   // Factor out the negative in std::pow(eta - 2.1, _Jph2a[i]) to avoid fpe in dbg (see #13163)
+  _console << "eta " << eta.value() << std::endl;
+  _console << "_Jph2a[0] " << _Jph2a[0] << std::endl;
+  _console << "std::pow(eta - 2.1, _Jph2a[0]) " << std::pow(eta - 2.1, _Jph2a[0]) << std::endl;
+
   const Real sgn = MathUtils::sign(eta.value() - 2.1);

   for (std::size_t i = 0; i < _nph2a.size(); ++i)
diff --git a/modules/porous_flow/test/tests/fluidstate/water_vapor.i b/modules/porous_flow/test/tests/fluidstate/water_vapor.i
index 1c658d09ce..40e86e84e1 100644
--- a/modules/porous_flow/test/tests/fluidstate/water_vapor.i
+++ b/modules/porous_flow/test/tests/fluidstate/water_vapor.i
@@ -14,7 +14,7 @@
     initial_condition = 1e6
   [../]
   [./h]
-    initial_condition = 8e5
+    initial_condition = 3e6
     scaling = 1e-3
   [../]
 []

Then compile in dbg. The test modules/porous_flow/test/tests/fluidstate/water_vapor.i should then trip the fpe with the stack trace above

@cpgr cpgr force-pushed the pf_watervapor branch 3 times, most recently from c932036 to d9ed1c4 Compare April 5, 2019 02:09
@permcody permcody requested a review from andrsd April 5, 2019 15:14
design = 'PorousFlowFluidPropertyIC.md'
issues = '#13108'
[../]
[./fluidpropic_celcius]
Copy link
Contributor

Choose a reason for hiding this comment

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

s/celcius/celsius/

[../]
[./fluidpropic_celcius]
type = 'CSVDiff'
input = 'fluidpropic_celcius.i'
Copy link
Contributor

Choose a reason for hiding this comment

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

s/celcius/celsius/

# Test the correct calculation of fluid properties using PorousFlwoFluidPropertyIC
#
# Variables:
# Pressure: 1 Mpa
Copy link
Contributor

Choose a reason for hiding this comment

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

Mpa -> MPa, also in the other file in the patch

@@ -0,0 +1,114 @@
# Test the correct calculation of fluid properties using PorousFlwoFluidPropertyIC
Copy link
Contributor

Choose a reason for hiding this comment

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

PorousFlwoFluidPropertyIC -> PorousFlowFluidPropertyIC

params.addRequiredParam<UserObjectName>("fp", "The name of the user object for the fluid");
MooseEnum property_enum("enthalpy internal_energy density");
params.addRequiredParam<MooseEnum>(
"property", property_enum, "The fluid property that this auxillary kernel is to calculate");
Copy link
Contributor

Choose a reason for hiding this comment

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

You mean "initial condition" not "auxiliary kernel", right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Bad copy and paste...


/**
* Calculates all required thermophysical properties and derivatives for each phase
* and fluid component. Must override in all derived classes.
Copy link
Contributor

Choose a reason for hiding this comment

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

You say this must be overridden, but you provide an implementation. That can confuse your users...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. This was a copy and paste as well, but the other one should be fixed as well. This method doesn't need to be overridden in most cases

@@ -277,7 +277,7 @@ class SinglePhaseFluidProperties : public FluidProperties
* @return internal energy (J/kg)
*/
virtual Real e_from_p_T(Real p, Real T) const;
virtual DualReal e_from_p_T(DualReal p, DualReal T) const;
DualReal e_from_p_T(const DualReal & p, const DualReal & T) const;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these not virtual, because the way you compute it is always the same and you actually rely on calling the virtual versions inside the methods? If so, we need this to be mentioned in the doco for this class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this to be consistent with the definitions in SinglePhaseFluidProperties.h in the propfunc macro for the other methods.

I'm not sure that this is the most flexible design though. For the simple fluids like the ideal gas, it is trivial to do the derivatives, so this idea makes sense. For more complicated fluids (like the ones I use), the derivatives are a pain to code, so it might be nice to code the DualReal version instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let me demonstrate the idea behind propfunc:

  1. let's say we need to compute e_from_p_T and our primary variables are rho, rhou, rhoE.
  2. The DualReal for p has this structure: (value of p, dp_drho, dp_drhou, dp_rhoE), similarly for T.
  3. The value we need back for e is: (value of e, de_drho, de_drhou, de_drhoE).
  4. So we need to compute: (e_from_p_T, de_dp * dp_drho + de_dT * dT_drho, de_dp * dp_drhou + de_dT * dT_drhou, de_dp * dp_drhoE + de_dT * dT_drhoE).

The d{p|T}_d{rho|rhou|rhoE} is coming from the AD system and de_dp and de_dT must be computed.

If de_dp and de_dT are difficult to compute (as you say), I am not sure that computing the DualReal version would be easier.

Also, if we are solving in terms of p and T, then the dp_dp and dT_dT from the AD system should be 1 and the chain rule still holds.

Keep in mind that this API has to be independent from the primary variables used. It looks a little bit that you are trying to invert the design and use the AD system to compute the non-AD value, like in T_from_p_h. That would create an inconsistency and this API would be usable only with p, h as primary variables. We do not want that.

May be, we need to look at what you are trying to do and how to fit it into this class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I added here is basically a copy and paste from the definition in propfunc but with p1 and p2 renamed p and T. Are you just saying that it should be declared as

 DualReal e_from_p_T(const DualReal & p1, const DualReal & p2) const

etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have just had a look at what I did here and I think that it implements exactly what you describe, and what is implemented in propfunc for all of the other methods. The input DualReals p and T can be functions of whatever with all of the derivatives carried through, and they are multiplied by the derivatives de_dp and de_dT computed in the e_from_p_T(p, T, e, de_dp, de_dT) method.

e_from_p_T and T_from_p_h aren't currently declared using propfunc as they had default implementations, so currently there is no AD version of these methods (I guess that they aren't declared using propfunc(e,p,T) as you can't declare the methods twice?)

I do compute the (Real) derivatives in WaterFluidProperties::T_from_p_h(p, h, T, dT_dp, dT_dh) using a seperate AD method to avoid coding all the derivatives for all of the subregions, but the public AD method T_from_p_h(p, h) is unchanged, so should work with p and h defined in terms of other variables as you described above.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that it implements exactly what you describe, and what is implemented in propfunc for all of the other methods.

So, why did not you use the propfunc macro? When we add the derivative version, then the macro should be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because these methods are implemented in SinglePhaseFluidProperties (with a default implementation), they can't be declared using propfunc because they would be defined twice in the same class.

I could add a SinglePhaseFluidPropertiesBase that has all of the methods declared using propfunc and then override the ones with default implementations in SinglePhaseFluidProperties to get around this I guess.

Copy link
Contributor

Choose a reason for hiding this comment

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

I finally understand why you did it this way. I forgot that the default implementation of e_from_p_T is actually a useful one. It does not throw the error of not being implemented...

Sorry it took so long...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries David

@permcody
Copy link
Member

@cpgr - David is on vacation for the remainder of this week. If you need a review on this sooner. Please let me know.

@cpgr
Copy link
Contributor Author

cpgr commented Apr 16, 2019

No rush @permcody - it is my fault as I was away all of last week.

@lindsayad
Copy link
Member

lindsayad commented Apr 17, 2019

I ran into an issue when I had something like std::pow(eta - 2.1, 0) where eta is a DualReal. When eta - 2.1 went negative, I hit a FPE in dbg, and can't figure out why.

I verified that doing something like

DualReal x = -0.1;

std::pow(x, 0)

works as expected (outputs 1).

Did you also do this verification in dbg mode? Because the simple code you pasted above should also have triggered the floating point exception. If you run in an optimized version, everything should work as you expect, whether as you say for the code above or for your actual production code. The problem is this code in MetaPhysicL:

// if_else is necessary here to handle cases where a is negative but b
// is 0; we should have a contribution of 0 from those, not NaN.
DualNumber_std_binary(pow,
  funcval * (b.value() * a.derivatives() / a.value() +
  MetaPhysicL::if_else(b.derivatives(), b.derivatives() * std::log(a.value()), b.derivatives())))

So the if_else free function will do the right thing, e.g. in the cases you have where b.derivatives() is full of zeros, we will indeed end up contributing zeros instead of NaNs, but we're still going to invoke the std::log function with a negative argument which throws the floating point exception. @roystgnr this doesn't seem ideal...the user has asked for nothing unreasonable here but if they're running in a debug version, they're going to get an exception that isn't their fault. Perhaps we should not define pow using the convenience macro and manually do the if-else checks so we don't invoke std::log when we don't need to?

@roystgnr
Copy link
Contributor

Wow, isn't that a sneaky one. I hadn't thought to test MetaPhysicL with FPEs enabled, since I usually just use tests for NaN myself. And even if I had turned FPEs on, I'm not certain we have internal test coverage for pow(-x,0).

The reason for MetaPhysicL::if_else in general isn't to avoid manual if() {} else {} code, it's to allow that code to automatically work on both scalars and vectors. And that applies here, since we have derivatives w.r.t. vectors all the time. When b.derivatives() is a vector that is zero in some entries but not others, then we want the b==0 entries to force pow(a,b)' == 0 even while we're actually evaluating log(a) at non-zero b.

Without expression-template-based types (which would be too hard to make happen on every class we want to use) there's no way to avoid making that log call there. And all the other options I can see are lousy.

We could have a compile-time option to temporarily disable FPEs for that call, but that would mean multiple system calls trashing performance for scalar types (although if you're only enabling FPEs in debug modes maybe that's acceptable??).

We could truncate the range of a before calling the log to get effectively a log_without_FPEs, then examine a in an outer if_else call and put quiet_NaNs back in place where needed, but that would mean having to fall back on NaN infectiousness to avoid falsely trusting your output, losing that FPE signal and its help debugging...

cpgr added 4 commits May 1, 2019 11:19
In preparation for a water/vapor EOS, enable AD calculation
of some other fluid properties

Refs idaholab#13108
A single component fluid state does not need to have a compositional
flash, so rearrange the base classes to make the inheritance more
suitable for both single and multicomponent fluid states.

Includes a new multicomponent base class that new multicomponent
fluid states should derive from.

Refs idaholab#13108
cpgr added 19 commits May 1, 2019 11:19
Takes the formulation in a single component fluid state, like
PorousFlowWaterVapor, and translate the fluid properties into
material properties for use in the existing kernels.

Refs idaholab#13108
Given a pressure and temperature, set an IC for a fluid property
(such as enthalpy) using a FluidProperties UserObject.

Refs idaholab#13108
Simplified tests of verification problems

Refs idaholab#13108
An fpe was thrown in dbg when std::pow(-0.x, 0) was
encountered. Factoring out the negative seems to work
around this.

Refs idaholab#13108
Calculate the AD version in the SinglePhaseFluidProperties
base class as with the other properties

Refs idaholab#13108
@andrsd andrsd merged commit 709163a into idaholab:next May 9, 2019
@cpgr cpgr deleted the pf_watervapor branch August 7, 2019 09:18
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.

Implement water-steam model in PorousFlow
7 participants