Minor updates to oneD StFlow files.#1595
Conversation
|
I was reviewing some of the less invasive changes that I had intertwined with the two-point continuation PR, and decided that I would try to get them together into a separate PR. I believe these changes do help with the overall readability of the StFlow methods regarding residual evaluation. One particular fix that I wanted to do was to move the continuity and right side residual functions away from the very bottom of the file closer to where they are called. After spending some time in the StFlow.cpp file, scrolling up and down to examine those functions was becoming tiresome. |
|
@wandadars ... thanks for sorting through some of this code. Now that Cantera 3.0 is released, I don't anticipate any new merge conflicts. While I am a little hesitant overall (the pre-existing code is somewhat brittle and can be easily broken), I think that there is potential for significant simplifications here. One change I am requesting is to move the Line 406 in e2c446c from the evalResidual function into individual "helper" functions. That way, you'd eliminate the need for separate evalRight/LeftBoundary methods. The reason I am suggesting this is that each method will implement exactly one governing equation that is 'complete' (while also avoiding function calls for every single point of the domain, plus making docstrings straight-forward). You likely will be able to call those functions directly from StFlow::eval, rendering StFlow::evalResiduals obsolete. As all of these methods are de-facto internal (they are not declared as protected, but probably should be), you can eliminate/modify them without having to go through deprecations.
As an aside, I'd recommend starting a new commit for each new helper function introduced (and making sure PS: there are VScode settings that remove trailing whitespace automatically whenever you save. |
|
Thanks for the suggestions @ischoegl. I just want to be sure I understand your suggestion before jumping back into this pull request again. Are you suggesting that we have a helper methods like: And that inside of each of these would be the for loop that would loop over every point in the 1D domain, and then update the appropriate entry in the |
9ffbbd5 to
006180a
Compare
👍 it indeed is! The signature of virtual void evalContinuity(size_t j, double* x, double* r, int* diag, double rdt);you'd need virtual void evalContinuity(double* x, double* rsd, int* diag,
double rdt, size_t jmin, size_t jmax);or equivalent. The other new methods, - I had stared at the code for a while and could not see any interactions that prevent this approach. PS: I realize that this is more than a 'minor' update, although it's not too complicated - don't hesitate to reach out |
|
I would just say, be careful about looping through the grid points multiple times. If you do that, I think you need to make sure all properties from the |
@speth ... definitely a fair point, but the overall approach appears to be that property data are precalculated? (there is the separate call to It is absolutely true that the calls to |
|
@wandadars ... let me know if you want to give this a go. Otherwise, I may go ahead and take a stab myself. |
|
@ischoegl I'm currently giving these extra updates that you suggested an attempt. I have them in, but I'm troubleshooting a failing Python one dim test. |
Should have looked at the commits in a little more detail. Sorry! |
I think the source of my issue was when I re-ordered the evaluation of the governing equations to u,V,T,Y,L,E, and the species equation made a call to getWdot() that must have populated the m_wdot variable that is then used by the energy equation, thus moving the energy equation before the species equation results in bad values being used possibly. Should something like the getWdot() be called in the updateProperties() method or one of the methods that this method calls? Edit: For the time being I just swapped the order that I called the residual methods evalSpecies() and evalEnergy() to put the species equation first, and it resolved my unit test issue. We have: I know the production rate of a species isn't a thermodynamic quantity, but would it make sense to have the logic of the getWdot() method put into the updateProperties() method that is used to call the updateThermo() method? For reference: I know that the goal is to avoid having both the species and energy equations calling getWdot() as it just duplicates the work. I'm also wary of having implicit relation between the species and energy residual equations such that one must always be evaluated before the other. |
Codecov Report
@@ Coverage Diff @@
## main #1595 +/- ##
==========================================
+ Coverage 72.70% 72.71% +0.01%
==========================================
Files 370 370
Lines 56257 56286 +29
Branches 20351 20368 +17
==========================================
+ Hits 40903 40931 +28
- Misses 12357 12358 +1
Partials 2997 2997
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
Yes, this is the right approach. |
|
Regarding your question where to update void updateThermo(const double* x, size_t j0, size_t j1) {
for (size_t j = j0; j <= j1; j++) {
setGas(x, j);
m_rho[j] = m_thermo->density();
m_wtm[j] = m_thermo->meanMolecularWeight();
m_cp[j] = m_thermo->cp_mass();
m_thermo->getPartialMolarEnthalpies(&m_hk(0, j));
m_kin->getNetProductionRates(&m_wdot(0, j)); // <----- here
}
}The key is to not call |
|
I have made those changes. I also had to make a change to the IonFlow class, as it used the removed evalResidual() method. The IonFlow class now overloads the evalSpecies() and evalElectricField() methods, and in the overloaded methods, it calls the base StFlow class' methods before extending them. The unit tests pass. With regards to the |
If you look at the docstring of |
I'm 👎 on |
@speth ... ok. The proposed Edit: what about |
|
@wandadars ... thanks. The docstrings themselves look good, but Cantera's convention is to put them in the Regarding @speth's dislike of |
Sorry for the terse response earlier. I recognize that the solver is very poorly documented at present, but I think that makes it even more important to keep certain things in place where they do actually have a connection to the mathematics behind the solver. In the transient mode, where the problem being solved is a DAE, |
|
@speth ... thank you for providing the context. This is definitely a decisive reason to stick with |
|
I know this is not the topic of this PR, but one thing that I noticed in the continuity residual equation was the following: In particular, the comparison of grid(j) == m_zfixed, a floating point comparison testing for equality. Is there some underlying situation that makes this a permissible operation? I ran into this issue with the 2-point continuation algorithm and I ended up just using an epsilon to test for floating point equality. Should this be done here too? |
It is absolutely true that exact floating point comparisons are problematic. My interpretation for why what you point to works / is legitimate is that |
To elaborate, the reason an exact comparison is possible is because the value of |
|
Thanks for the clarification @speth ! |
|
Once this PR is completed, I plan to open a PR for the two-point control method that will be build upon the changes in this PR. |
ischoegl
left a comment
There was a problem hiding this comment.
@wandadars ... apart from the trailing whitespace that the linter is complaining about, your logic in evalContinuity still runs some loops that should be skipped.
Please have a look at the following code (same as what I posted yesterday), which avoids the remaining issues pointed out in the review comments.
void StFlow::evalContinuity(double* x, double* rsd, int* diag,
double rdt, size_t jmin, size_t jmax)
{
if (jmin == 0) { // left boundary
rsd[index(c_offset_U, 0)] =
- (rho_u(x,1) - rho_u(x,0)) / m_dz[0]
- density(1)*V(x,1) + density(0)*V(x,0);
diag[index(c_offset_U, 0)] = 0; // Algebraic constraint
}
if (jmax == m_points - 1) { // right boundary
if (m_usesLambda) {
rsd[index(c_offset_U, jmax)] = rho_u(x, jmax);
} else {
rsd[index(c_offset_U, jmax)] = rho_u(x, jmax) - rho_u(x, jmax-1);
}
diag[index(c_offset_U, jmax)] = 0; // Algebraic constraint
}
size_t j0 = std::max<size_t>(jmin, 1);
size_t j1 = std::min(jmax, m_points - 2);
if (m_usesLambda) { // "axisymmetric_flow"
// Note that this propagates the mass flow rate information to the left
// (j+1 -> j) from the value specified at the right boundary. The
// lambda information propagates in the opposite direction.
for (size_t j = j0; j <= j1; j++) {
rsd[index(c_offset_U,j)] =
-(rho_u(x,j+1) - rho_u(x,j)) / m_dz[j]
-(density(j+1)*V(x,j+1) + density(j)*V(x,j));
diag[index(c_offset_U, j)] = 0; // Algebraic constraint
}
} else if (m_isFree) { // "free-flow"
for (size_t j = j0; j <= j1; j++) {
// terms involving V are zero as V=0 by definition
if (grid(j) > m_zfixed) {
rsd[index(c_offset_U,j)] = -(rho_u(x,j) - rho_u(x,j-1)) / m_dz[j-1];
} else if (grid(j) == m_zfixed) {
if (m_do_energy[j]) {
rsd[index(c_offset_U, j)] = T(x,j) - m_tfixed;
} else {
rsd[index(c_offset_U, j)] = rho_u(x,j) - m_rho[0]*0.3; // why 0.3?
}
} else { // grid(j) < m_zfixed
rsd[index(c_offset_U, j)] = -(rho_u(x,j+1) - rho_u(x,j)) / m_dz[j];
}
diag[index(c_offset_U, j)] = 0; // Algebraic constraint
}
} else { // "unstrained-flow" (fixed mass flow rate)
for (size_t j = j0; j <= j1; j++) {
rsd[index(c_offset_U, j)] = rho_u(x,j) - rho_u(x,j-1);
diag[index(c_offset_U, j)] = 0; // Algebraic constraint
}
}
}
Thanks @speth. It's good to know that it is defaulted to zero. My original thought was that if activating the pseudo-time was the action that had to be explicit, then it would remove a lot of lines of code. I checked and we set the diag variable to |
ischoegl
left a comment
There was a problem hiding this comment.
@wandadars - thank you for taking care of all the change requests I had. From my perspective, I believe the code is in good shape.
I ran some rudimentary benchmarks based on samples/cxx/flamespeed, where I wrapped the solve step like so:
auto t0 = std::chrono::high_resolution_clock::now();
flame.solve(loglevel,refine_grid);
auto t1 = std::chrono::high_resolution_clock::now();
double dt = std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count();
std::cout << std::setprecision(5)
<< "Mixture-averaged: solved in " << dt / 1.e9 << " s" << std::endl;(and similar for multi-component). Results with phi=1 on Apple Silicon M2 are:
Updated code:
Mixture-averaged: solved in 4.4339 s
Flame speed with mixture-averaged transport: 0.38022169937261846 m/s
[...]
Multicomponent: solved in 0.60851 s
Flame speed with multicomponent transport: 0.38210582549411254 m/s
Cantera 3.0:
Mixture-averaged: solved in 4.4734 s
Flame speed with mixture-averaged transport: 0.38022169937261846 m/s
[...]
Multicomponent: solved in 0.61325 s
Flame speed with multicomponent transport: 0.38210582549411254 m/s
This basic test indicates that speed differences aren't statistically significant (in theory, the loops should be a little more efficient, but it's likely not where time is spent).
At this point, I am passing things over to @speth, who indicated that he wanted to review the PR as well.
|
Thank you @ischoegl . I appreciate you walking me through the Github PR process for this one. |
|
@speth ... in order to add a deprecation process for |
speth
left a comment
There was a problem hiding this comment.
Thanks, @wandadars, for working on this and being so responsive to the suggestions so far. I have a few more (mostly minor) updates for your consideration.
|
I've been hitting the "resolve conversation" button on review items that I have made the requested changes for. Is it that the proper way to use that Github feature? |
Yes, that's how we use it, at least, for anything that's straightforward. |
ischoegl
left a comment
There was a problem hiding this comment.
Thanks, @wandadars! This looks good to me.
|
I missed one of @speth suggestions for the docstrings, so I just pushed the update. I think other than that I believe I have covered all the suggestions. |
- pull radiation computation into a separate method - Various comment formatting/cleaning up - eliminate evalResidual and evalRightBoundary methods - moved wdot eval to update_thermo() method - removed setGas() call as it is redundant
improved comments for the residual evaluation equations
moved computeRadiation to original location updates to logic for residual eval methods & better argument names for eval() method
7c5381f to
71db3af
Compare
speth
left a comment
There was a problem hiding this comment.
Thanks for responding to all the review feedback, @wandadars. This all looks good to me, and I think this will help simplify some modifications to the 1D solver that users might want to undertake, as well as making it somewhat easier to follow.
I cleaned up the commit history and squashed a number of commits, as there were a lot of intermediate commits that did either did not compile or did not pass the test suite, which can cause problems later (for example, when trying to use git bisect). We can merge as soon as this passes the tests one last time.
Moved methods around to be closer to their contextual counterparts, various comment formatting/cleaning up, pulled radiation computation into a separate method, tried to make residual equation evaluation order consistent across the boundary/interior sections of code that evaluate them.
If applicable, fill in the issue number this pull request is fixing
Checklist
scons build&scons test) and unit tests address code coverage