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

Extensible jacobian and network jacobian interfaces #1634

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

anthony-walker
Copy link
Contributor

Changes proposed in this pull request

In this pull request I have setup basic structure for adding contributions to the jacobian from flow devices and walls.
I have also added an extensible jacobian interface to the ExtensibleReactor system and modified the existing jacobian system to pass a vector of triplets as is needed in lieu of creating a large number of separate sparse matrices.

@speth @ischoegl some additional tests are needed for the wall and flow contributions but I wanted to get the content officially into a PR.

  • Jacobian contributions from walls and flow devices
  • Extensible jacobian interface
  • Enhancements to existing jacobian backend

Checklist

  • The pull request includes a clear description of this code change
  • Commit messages have short titles and reference relevant issues
  • Build passes (scons build & scons test) and unit tests address code coverage
  • Style & formatting of contributed code follows contributing guidelines
  • The pull request is ready for review

@codecov
Copy link

codecov bot commented Oct 18, 2023

Codecov Report

Attention: Patch coverage is 79.90196% with 41 lines in your changes missing coverage. Please review.

Project coverage is 72.86%. Comparing base (dd56a70) to head (cf394c3).
Report is 2 commits behind head on main.

Files Patch % Lines
src/zeroD/ReactorNet.cpp 84.61% 5 Missing and 5 partials ⚠️
include/cantera/zeroD/ReactorBase.h 25.00% 6 Missing ⚠️
include/cantera/zeroD/Wall.h 0.00% 4 Missing ⚠️
interfaces/cython/cantera/delegator.pyx 66.66% 4 Missing ⚠️
src/zeroD/Reactor.cpp 77.77% 2 Missing and 2 partials ⚠️
include/cantera/base/Delegator.h 62.50% 2 Missing and 1 partial ⚠️
src/zeroD/Wall.cpp 91.42% 0 Missing and 3 partials ⚠️
include/cantera/zeroD/Reactor.h 0.00% 2 Missing ⚠️
src/zeroD/IdealGasConstPressureMoleReactor.cpp 77.77% 2 Missing ⚠️
src/zeroD/IdealGasMoleReactor.cpp 77.77% 2 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1634      +/-   ##
==========================================
+ Coverage   72.80%   72.86%   +0.06%     
==========================================
  Files         381      382       +1     
  Lines       54020    54204     +184     
  Branches     9210     9238      +28     
==========================================
+ Hits        39328    39495     +167     
- Misses      11707    11714       +7     
- Partials     2985     2995      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@anthony-walker
Copy link
Contributor Author

anthony-walker commented Oct 24, 2023

@speth @ischoegl I am not entirely sure why the .NET tests are failing but it doesn't seem to be with this PR, is this a known issue?

Otherwise I believe this code is ready for review.

Copy link
Member

@ischoegl ischoegl left a comment

Choose a reason for hiding this comment

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

While I don't have enough experience in extensible things for a complete review, I noticed that a lot of test are failing with:

*******************************************************************************
NotImplementedError thrown by MassFlowController::buildReactorJacobian:
Not implemented.
*******************************************************************************

which doesn't appear to be .NET related.

@anthony-walker
Copy link
Contributor Author

anthony-walker commented Oct 24, 2023

@ischoegl these failures are from my latest bug fixes there is a separate commit which addresses the .NET failure issue, 0a34092.

@speth
Copy link
Member

speth commented Dec 22, 2023

Before you get any further, I just wanted to suggest undoing the merge commit and rebasing onto the current main branch instead.

Copy link
Member

@speth speth left a comment

Choose a reason for hiding this comment

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

Thanks for putting this together, @anthony-walker. I think it helps in laying out the necessary pieces for handling the cross-reactor terms in the Jacobian -- in particular, the mechanics surrounding Wall::buildNetworkJacobian seem like a good basis to keep building on.

I think there is some room for simplification of this structure, which I've left specific comments on. However, I think there is a significant issue with sorting out exactly which derivatives need to be computed and what the right formulas are for the different cases. The challenge I see us needing to resolve to generalize this across the different reactor types is that what derivative is needed for a single interaction (for example, wall heat transfer) depends on both reactor types and what variables they use to represent each term, and we need a way of selecting the correct formula for each Jacobian element based on that pair of reactor types.

include/cantera/zeroD/FlowDevice.h Outdated Show resolved Hide resolved
include/cantera/zeroD/ReactorBase.h Outdated Show resolved Hide resolved
include/cantera/zeroD/ReactorBase.h Outdated Show resolved Hide resolved
src/zeroD/IdealGasMoleReactor.cpp Outdated Show resolved Hide resolved
src/zeroD/IdealGasMoleReactor.cpp Outdated Show resolved Hide resolved
// derivative of temperature transformed by ideal gas law
vector<double> moles(m_nsp);
getMoles(moles.data());
double dTdni = pressure() * m_vol / GasConstant / std::accumulate(moles.begin(), moles.end(), 0.0);
Copy link
Member

Choose a reason for hiding this comment

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

Like in IdealGasMoleReactor, $T$ is an independent variable. Here, a change in $n_i$ will change the volume, not the temperature.

Comment on lines 1476 to 1483
# check for values
for i in range(gas1.n_species):
assert np.isclose(jac[0, i + 1 + r1.n_vars], - U * A * gas2.T)
assert np.isclose(jac[r1.n_vars, i + 1], U * A * gas1.T)
if (i + 1 != 2):
assert np.isclose(jac[0, i + 1], U * A * gas1.T, rtol=1e-2)
assert np.isclose(jac[r1.n_vars, i + 1 + r1.n_vars], - U * A * gas2.T, rtol=1e-2)
Copy link
Member

Choose a reason for hiding this comment

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

I think you should check against the finite difference Jacobian. This test is just checking against the same incorrect formulas in Python as what's in the C++ implementation.

gas2 = ct.Solution("h2o2.yaml", "ohmech")
gas2.TPX = 300, ct.one_atm, "O2:1.0"
r2 = self.reactorClass(gas2)
r2.chemistry_enabled = False
Copy link
Member

Choose a reason for hiding this comment

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

The analytical Jacobian doesn't currently respect the chemistry_enabled flag, so there are a lot of non-zero elements here that should be zero based on the setting of this flag.

test/zeroD/test_zeroD.cpp Outdated Show resolved Hide resolved
test/zeroD/test_zeroD.cpp Outdated Show resolved Hide resolved
@speth
Copy link
Member

speth commented Mar 10, 2024

For an example of where the current derivatives are clearly not correct, consider the following example, adapted from one of your new tests:

import cantera as ct
import numpy as np

# create first reactor
gas1 = ct.Solution("h2o2.yaml", "ohmech")
gas1.TPX = 600, ct.one_atm, "O2:1.0"
r1 = ct.IdealGasMoleReactor(gas1)
r1.name = 'R1'
gas1.set_multiplier(0.0)

# create second reactor
gas2 = ct.Solution("h2o2.yaml", "ohmech")
gas2.TPX = 300, ct.one_atm, "O2:1.0"
gas2.set_multiplier(0.0)
r2 = ct.IdealGasMoleReactor(gas2)
r2.name = 'R2'

# create wall
U = 500.0
A = 3.0
w = ct.Wall(r1, r2, U=U, A=A)
net = ct.ReactorNet([r1, r2])

def print_jac(J):
    for i,j in zip(*np.nonzero(J)):
        name_i = net.component_name(i).replace('temperature', 'T')
        name_j = net.component_name(j).replace('temperature', 'T')
        net.component_name(i).replace('temperature', 'T')
        #print(f"J[{i: 3d}, {j: 3d}] = {J[i,j]:.3e}")
        print(f"J[{name_i:8s}, {name_j:8s}] = {J[i,j]:.3e}")

print('Semi-analytical Jacobian:')
print_jac(net.jacobian)
print('\nFinite difference Jacobian:')
print_jac(net.finite_difference_jacobian)

which outputs:

Semi-analytical Jacobian:
J[R1: T   , R1: T   ] = -2.727e+00
J[R1: T   , R1: H2  ] = 9.000e+05
J[R1: T   , R1: H   ] = 9.000e+05
J[R1: T   , R1: O   ] = 9.000e+05
J[R1: T   , R1: O2  ] = 9.000e+05
J[R1: T   , R1: OH  ] = 9.000e+05
J[R1: T   , R1: H2O ] = 9.000e+05
J[R1: T   , R1: HO2 ] = 9.000e+05
J[R1: T   , R1: H2O2] = 9.000e+05
J[R1: T   , R1: AR  ] = 9.000e+05
J[R1: T   , R1: N2  ] = 9.000e+05
J[R1: T   , R2: H2  ] = -4.500e+05
J[R1: T   , R2: H   ] = -4.500e+05
J[R1: T   , R2: O   ] = -4.500e+05
J[R1: T   , R2: O2  ] = -4.500e+05
J[R1: T   , R2: OH  ] = -4.500e+05
J[R1: T   , R2: H2O ] = -4.500e+05
J[R1: T   , R2: HO2 ] = -4.500e+05
J[R1: T   , R2: H2O2] = -4.500e+05
J[R1: T   , R2: AR  ] = -4.500e+05
J[R1: T   , R2: N2  ] = -4.500e+05
J[R2: T   , R1: H2  ] = 9.000e+05
J[R2: T   , R1: H   ] = 9.000e+05
J[R2: T   , R1: O   ] = 9.000e+05
J[R2: T   , R1: O2  ] = 9.000e+05
J[R2: T   , R1: OH  ] = 9.000e+05
J[R2: T   , R1: H2O ] = 9.000e+05
J[R2: T   , R1: HO2 ] = 9.000e+05
J[R2: T   , R1: H2O2] = 9.000e+05
J[R2: T   , R1: AR  ] = 9.000e+05
J[R2: T   , R1: N2  ] = 9.000e+05
J[R2: T   , R2: T   ] = -1.887e+00
J[R2: T   , R2: H2  ] = -4.500e+05
J[R2: T   , R2: H   ] = -4.500e+05
J[R2: T   , R2: O   ] = -4.500e+05
J[R2: T   , R2: O2  ] = -4.500e+05
J[R2: T   , R2: OH  ] = -4.500e+05
J[R2: T   , R2: H2O ] = -4.500e+05
J[R2: T   , R2: HO2 ] = -4.500e+05
J[R2: T   , R2: H2O2] = -4.500e+05
J[R2: T   , R2: AR  ] = -4.500e+05
J[R2: T   , R2: N2  ] = -4.500e+05

Finite difference Jacobian:
J[R1: T   , R1: T   ] = -2.727e+00
J[R1: T   , R1: O2  ] = 4.589e+04
J[R1: T   , R2: T   ] = 3.107e+00
J[R2: T   , R1: T   ] = 1.752e+00
J[R2: T   , R2: T   ] = -1.887e+00
J[R2: T   , R2: O2  ] = -1.294e+04

@anthony-walker
Copy link
Contributor Author

@speth apologies for the delay, I have made some updates but still need to do some of the larger changes. It is still on my radar.

It can sometimes be useful to add on to the default evaluation in such a
way that the standard before or after delegations cannot handle. A default
evaluation function allows for this.
in regards to equations, handling volumes, etc.
@anthony-walker
Copy link
Contributor Author

@speth pushed up some more changes, I will have to put a bit of thought into some of the other changes.

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