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

Extend GIST to mixtures and non-water solvents #961

Merged
merged 41 commits into from
Jun 7, 2022

Conversation

fwaibl
Copy link
Contributor

@fwaibl fwaibl commented Apr 4, 2022

This pull request extends GIST towards solvents other than water, and, to some extent, towards solvent mixtures (especially salt-water mixtures). Furthermore, it parallelizes the GIST entropy calculation using OpenMP.

Summary of changes:

  • remove the excludeions option. This does not limit the usability since ions can be excluded also by using "strip".
  • Refactor dataset creation into a function.
  • If the solvent is not water, warn rather than error.
  • Manage two arrays of datasets. One contains the density of each chemical element contained in the main solvent (i.e., g_O and g_H for water, as well as g_XP for 4-point water models). The other contains molecular densities for a list of solvent molecules that is specified by the user (by default nothing, but can e.g. be g_mol_WAT, g_mol_NA, g_mol_CL). This is specified by the user (option "solventmols"), and the first of "solventmols" is the main solvent (for which atomic densities are computed)
  • Change how solvent centers and quaternions are computed. The center is now the center of mass by default, but the old behavior can be obtained by specifying nocom. The quaternion is calculated using three atoms, which can be specified by the user (flag "rigidatoms"), and define a "rigid substructure" of the main solvent. If the user does not specify anything, those atoms are chosen using a simple heuristic (the atom with the most bonds, and two of its bond partners), which results in H1-O-H2 for water.
  • Energy calculation is done based on all solvents, entropy, neighbor, and order are based on the main solvent.
  • Refactor code for normalization of datasets. During the action, only track the total (neighbors, dTSorient, dTStrans, etc.), then compute the _dens and _norm values in the end. Also, use the polymorphic Dataset3D methods rather than casting to the double and float versions. There are some functions implementing DataSet math. (i.e., scale a dataset or divide by an array)
  • Update output printing. There is a fancy (maybe too fancy?) little DataFilePrinter class inside the Action_GIST class. This automatically chooses the format string for int / float columns. Output version is updated to v4. The output is consistent to the old output when not using the new features, but I thought since the columns are now created dynamically, it should be a new output version as well.
  • Then entropy calculation is now parallel using OpenMP. All the entropy calculation can be done independently for each voxel, but there is a critical section when writing the output into the output arrays. Furthermore, the large voxel_xyz_ array is not allocated (or at least only the first dimension is allocated) when using skipS_.

Changes in cpptraj tests

  • update GIST output version to v4
  • slight changes in many test files. Several output data are not numerically identical to the old version.
  • The PME tests are now passing when compiled with GPU support.
  • All tests use the nocom option.

To-Do
Several things can/should still be updated.

  • Add tests for other solvents and salt-water mixtures. I already tested on some old simulations of mine, and I am already using it for "production" calculations, but there are no small test cases yet.
  • Add a check for linearity of the solvent molecules.

Other problems

  • The neighbor and order calculations are probably not working correctly with non-water solvents. Furthermore, they are not parameterized for other solvents, so they would not make much sense even when implemented without any errors. Therefore, I don't really plan to update them, and instead keep a warning message in the code.
  • The code does not work with solvents with 1 or 2 atoms. The option with 1 atom would be easy to implement (simply skip the 6D entropy or set all quaternions to a default value), but the 2 atom option would be more tricky (would need a separate implementation).

@drroe @tkurtzman what do you think about it? I decided to send a pull request even though it is not completely finished, so I can get some feedback already. Do you have suggestions regarding the changed user interface options, or should I revert some of the changes?
Should we add separate Eww output for components of a solvent mixture? This would make the post-processing a lot easier, but I did not implement it yet because I also did not have it for my publication on salt-water mixtures in GIST, so I am used to work with the single energy output. (The problem is that we need a separate reference energy for each component)

The entropy is currently only calculated for the main solvent. This is no big problem because the 1st order entropy is independent for multiple solvents, so it is cheap to run multiple calculations for multiple solvents. For the energy, this is not an option (at least, it requires many GIST runs)

fwaibl added 16 commits April 4, 2022 16:44
Removes
* the "excludeions" option
* the Gist::includeIons_ variable
* the Gist::A_idxs_ array, which tracked the solute+solvent atoms, and
  simply contained all atom indices unless excludeions was specified.
* Two new methods Action_GIST::setSolventProperties and
  Action_GIST::checkSolventProperties
* Removes checks for water is a solvent, but there is still a warning,
  and it is checked that all solvents are the same molecule.
* Molecules are currently binned by their center of mass (COM), but it
  is planned to add a "com" or "nocom" option to let the user choose.
* The user can choose 3 atoms that form a rigid subunit of the molecule
  for the quaternion construction
* There are some further refactorings: datasets are not cast to
  DataSet_GridFlt or ...Dbl, instead the polymorphic DataSet_3D is kept.
  I changed this after debugging for a while because I had accidently
  changed a dataset from double to float. The polymorphic option is more
  robust, but the downside is that DataSet_3D.operator[] is not
  writable, so we need to use the SetGrid method.

Currenty unfinished
* density output (g_... columns) for non-water solvents!!!
* order calculation (not planned for non-water solvents)
* neighbor calculation (not sure if I will implement this, it probably
  conflicts with the "com" option
* Remove individual dataset pointers from Action_GIST class.
* Instead, manage an std::map to call them by their name. For fast
  access (e.g., within DoAction), the pointer is re-used.
* Dynamically add g_H, g_H, g_C etc. datasets for each element in the
  solvent.
* Add a struct SolventInfo (instance name solventInfo_) that tracks the
  elements in the solvent, and a method "getDensityDatasets" that
  returns a vector of pointers to the respective density datasets.
  solventInfo_ is filled by the analyzeSolventElements method during
  Action Setup.
* Add a class DataFilePrinter (private to Action_GIST), that prints
  whitespace separated values to a CpptrajFile. This replaces the long
  format string in Action_GIST::Print().
* implement some dataset math that will replace the CalcAvgVoxelEnergy
  and CalcAvgVoxelEnergy_PME functions
* fix a bug when parsing rigidAtomIndices
* implement a nocom option to recover the old GIST behavior, and a
  calcMolCenter method that computes the position of a molecule in the
  appropriate manner (COM or first "rigid" atom)
* make individual pointers for Esw_, Eww_ etc. datasets, instead of the
  std::map.
* Refactor some of the dataset math in the Print method
* remove solute positions from Esw in PME-GIST.
* divide Eww by two when printing the total Eww on the grid.
* Add -a flag to DoTest because energy output is off in the last printed
  digit
* Fix GPU code after the last refactoring
rigidAtomIndices_ specifies which atoms of the solvent define a rigid
subunit. This is needed for the entropy calc, and also (if "nocom" is
specified) to define the molecule's position. Each number is an offset
from the molecule's start index (i.e., 0 means the first atom of the
moleucle). The first number is the central atom, and the second and
third number are other atoms that are bound to it. It can be set in 2
ways:
* The user specifies rigidatoms. In that case, the atom names are
  matched to set the indices (offset from molecule start)
* If the user doesn't specify, the solvent atom with the most bonds is
  searched. If it has less than 2 bonds to heavy atoms, hydrogens are
  also included. The central atom is the one with the most bonds. The
  two others are chosen from the atoms that are bonded to it.

Molecular densities are tracked in separate datasets. They are defined
relative to the solvent bulk density, and are computed by binning the
solvent centers of each specified solvent species (either the COM or the
first of "rigidAtomIndices_"). The user has to choose which molecules
are tracked by specifying "solventmols", followed by a comma-separated
list of molecule names. No selection masks are allowed, firstly because
this would mess with the comma-separated list, and also because we
anyway assume that we select whole molecules. For each specified
molecule, a dataset is generated and written out as dx file, e.g.,
gist-g-mol-WAT.dx
* add entropy tests for Gist4 test and Gist2 test
* run PME tests on GPU builds as well.
* change test save files to output v4, but don't change any other
  numbers.
* Use DoTest ... -a to allow for minor differences.
* Parallel entropy calculations (dTSorient and dTSsix+dTStrans) for
  GIST, using OpenMP. The parallelization is very easy because most of
  the calculation can be done independently for each voxel, with only
  read access the voxel_xyz_ and voxel_Q_ arrays. The parallelization is
  implemented using the loop over all voxels, and there is a critical
  section when writing the data to arrays and incrementing the count of
  finished voxels and the count of water molecules. In a test system of
  10000 frames of benzene in water, the results are identical to the
  previous version.
* If skipS_ is specified, don't fill the voxel_xyz_ and voxel_Q_ arrays,
  since they are only needed for the entropy. Those are arrays of
  arrays. The outer array is still allocated, but each voxel array now
  stays empty with skipS_. For large GIST calculations (say, 200x200x200
  voxels and >10000 frames), those arrays need large amounts of memory
  (~8GB in this example, assuming mostly water in the grid)
  This allows for an efficient strategy for very large GIST calculations:
  do the energy once in total (for GPU or PME implementations, this is
  more efficient than splitting the grid) and do the entropy calculation
  for small sub-grids, or on a cluster with high RAM (now also in
  parallel).
@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

@lgtm-com
Copy link

lgtm-com bot commented Apr 4, 2022

This pull request introduces 3 alerts when merging 298f245 into f1783be - view on LGTM.com

new alerts:

  • 3 for Wrong type of arguments to formatting function

* Tests for GIST entropy were added previously, but the save files were
  not yet committed, leading to failing tests.
@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

1 similar comment
@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

@fwaibl
Copy link
Contributor Author

fwaibl commented Apr 5, 2022

Should we update the tests to use "DoTest ... -a $TEST_TOLERANCE" also for the full output file? Currently, there is a 0.0001 difference (the last printed digit) in one line of Gist4-Eww-dens.dx and in one line of Gist3-output.dat. Otherwise, I can also update the .save files.

@drroe
Copy link
Contributor

drroe commented Apr 5, 2022

Do you have suggestions regarding the changed user interface options, or should I revert some of the changes?

Why remove the excludeions keyword? You can certainly remove it prior to gist with strip, but then that removes the ion for every subsequent action, not just gist. Why not leave excludeions for maximum flexibility?

Should we update the tests to use "DoTest ... -a $TEST_TOLERANCE" also for the full output file?

It depends on whether you think the error is due something innocuous like floating-point round-off error or not. If the difference is due to the fact that the underlying algorithms have changed (i.e., it will always have the new value), then it's best to update the test values (assuming they have been properly validated). If the difference is because at some point the calculation accumulates round-off error (because numbers become denormalized or something; this happens a lot in curve-fitting for example) then it's appropriate to add a tolerance. Let me know if that makes sense.

@fwaibl
Copy link
Contributor Author

fwaibl commented Apr 5, 2022

Why remove the excludeions keyword? You can certainly remove it prior to gist with strip, but then that removes the ion for every subsequent action, not just gist. Why not leave excludeions for maximum flexibility?

The reason I removed it was that I changed a lot of stuff regarding the assignment of atoms for the energy / entropy calculations, and it would have made everything more difficult. However, now that everything else is working, I assume that we could add it again without a large amount of work. Basically, ions are already omitted in the entropy calculation, but we would have to also omit them in the energy calculation.
However, I think that it is not physically reasonable to omit some atoms in the energy calculation. In my opinion, it would be better to give the user more control over the energy output.

It depends on whether you think the error is due something innocuous like floating-point round-off error or not. If the difference is due to the fact that the underlying algorithms have changed (i.e., it will always have the new value), then it's best to update the test values (assuming they have been properly validated). If the difference is because at some point the calculation accumulates round-off error (because numbers become denormalized or something; this happens a lot in curve-fitting for example) then it's appropriate to add a tolerance. Let me know if that makes sense.

I think that the difference is due to some floating point error, because it is very small and only occurs in one voxel of the test calculations. However, it will probably stay the same as long as we don't change the implementation again, so we could change the .save files. Adding a tolerance would make the tests more robust to floating point errors in the future, but could also mask small systematic errors.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 2, 2022

I added some comments and updated the documentation. I also added another regression test for benzene as solvent. Furthermore, I added our papers regarding the extension to multiple solvents and to salt-water mixtures to the GIST citation list. Is this ok with you?
From my side, I think we should be ready to merge this pull request. Is there anything else I should change/fix?

@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

1 similar comment
@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

@lgtm-com
Copy link

lgtm-com bot commented Jun 2, 2022

This pull request introduces 1 alert when merging 37cbf98 into b65fc44 - view on LGTM.com

new alerts:

  • 1 for Too few arguments to formatting function

infofile_->Printf("Ensemble total solute energy on the grid: %9.5f Kcal/mol \n", SumDataSet(*U_PME_) / NFRAME_);
}
if (n_linear_solvents_ > 0) {
mprintf("GIST warning: %d almost-linear solvent molecules occurred. Maybe choose other \"rigidatoms\".\n");
Copy link
Contributor

Choose a reason for hiding this comment

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

This printf is missing an argument for the %d .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I meant to print n_linear_solvents.

@drroe
Copy link
Contributor

drroe commented Jun 2, 2022

There is a test failure in the PGI build (this is non-fatal by default since PGI compilers are a bit out of the mainstream these days):

[2022-06-02T15:47:16.169Z] TEST: /home/jenkins/jenkins-cpu/workspace/amber-github_cpptraj_PR-961@3/test/Test_GIST
[2022-06-02T15:47:16.169Z]   Gist10-Info.dat.save Gist10-Info.dat are different.
[2022-06-02T15:47:16.169Z] 6c6
[2022-06-02T15:47:16.169Z] < Total 6d if all one vox: -11.14038 kcal/mol
[2022-06-02T15:47:16.169Z] > Total 6d if all one vox: -11.14022 kcal/mol
[2022-06-02T15:47:16.169Z] ### Maximum absolute error in matching lines = 1.60e-04 at line 6 field 7
[2022-06-02T15:47:16.169Z] ### Maximum relative error in matching lines = 1.44e-05 at line 6 field 7
[2022-06-02T15:47:16.169Z]   Gist10-output.dat.save Gist10-output.dat are different.
[2022-06-02T15:47:16.169Z] 96c96
[2022-06-02T15:47:16.169Z] < 93 -2.5 2.5 2.5 3 0 0 -0.0101261 -4.21922 -0.0165125 -6.88021 -0.0275413 -11.4755 -0.0417808 -17.4087 0 0 0.000457794 -0.000303779 -0.000202246 0.000585458 0 0 0 
[2022-06-02T15:47:16.169Z] > 93 -2.5 2.5 2.5 3 0 0 -0.0101261 -4.21922 -0.0165128 -6.88034 -0.0275406 -11.4753 -0.0417808 -17.4087 0 0 0.000457794 -0.000303779 -0.000202246 0.000585458 0 0 0 
[2022-06-02T15:47:16.169Z] 132c132
[2022-06-02T15:47:16.169Z] < 129 2.5 2.5 2.5 1 0.000767677 0.000767677 -0.00384745 -4.80932 0 0 -0.00917607 -11.4701 -0.0140286 -17.5357 0 0 -3.41573e-05 -4.47789e-05 -4.79207e-05 7.39477e-05 0 0 0 
[2022-06-02T15:47:16.169Z] > 129 2.5 2.5 2.5 1 0.000767677 0.000767677 -0.00384745 -4.80932 0 0 -0.00917542 -11.4693 -0.0140286 -17.5357 0 0 -3.41573e-05 -4.47789e-05 -4.79207e-05 7.39477e-05 0 0 0 
[2022-06-02T15:47:16.169Z] ### Maximum absolute error in matching lines = 8.00e-04 at line 132 field 13
[2022-06-02T15:47:16.169Z] ### Maximum relative error in matching lines = 6.98e-05 at line 132 field 13
[2022-06-02T15:47:16.169Z]   2 out of 61 comparisons failed.

Seems like it might be round-off to me, but it's worth double-checking. Do the diffs look reasonable to you? If so, you may want to add/increase the test tolerance. If you're not comfortable with that we could potentially ignore it for now in favor of getting the code merged.

@AmberJenkins
Copy link
Collaborator

The PGI build in Jenkins failed.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

The error in the PGI build seems a bit too much for round-off, since we are not adding a lot of numbers here (it is 10 frames of one molecule). I'll try to get a PGI build locally to debug it.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

I did some testing and it seems that the failing PGI test is actually a round-off problem.
Since I defined the (restrained) benzene molecule as a "solvent" in this test, there are very similar quaternions. It seems that there is one pair with a distance of 0.015202 rad (0.8 deg) (calculated in Python using https://github.com/moble/quaternion.git). A Python version of the algorithm used in GIST returned 0.01519989
I used the following code snippet for testing:

#include <iostream>
#include <cmath>

using TYPE = float;

int main()
{
    TYPE w1 = 0.5080507994;
    TYPE x1 = 0.4968648851;
    TYPE y1 = 0.7026906610;
    TYPE z1 = -0.0351498947;

    TYPE w2 = 0.5141307712;
    TYPE x2 = 0.4948045015;
    TYPE y2 = 0.6995792985;
    TYPE z2 = -0.0377739333;

    double norm = w1*w2 + x1*x2 + y1*y2 + z1*z2;
    double ang = 2*acos(fabs(w1*w2 + x1*x2 + y1*y2 + z1*z2));
    std::cout << norm << " " << ang << std::endl;

    return 0;
}

With TYPE=double, it get 0.0151999 from GCC and PGI regardless of optimization level, with TYPE=float, it get 0.0152075 from PGI with -O0, and 0.0151918 from GCC as well as GPI with optimizations on.

It is certainly rare to find low angles in a GIST calculation (because they are sampled from a 3D space), but on the other hand, 0.8 degree does not seem that small. How should we proceed?

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

A more accurate version could be implemented easily by re-normalizing the quaternions at double precision before calculating the angle. However, I would not like to store the quaternions at double precision because the RAM requirement of GIST can sometimes be problematic, and the quaternion array is already one of the main offenders.

@drroe
Copy link
Contributor

drroe commented Jun 3, 2022

A more accurate version could be implemented easily by re-normalizing the quaternions at double precision before calculating the angle. However, I would not like to store the quaternions at double precision because the RAM requirement of GIST can sometimes be problematic, and the quaternion array is already one of the main offenders.

I unfortunately don't have ready access to a modern PGI to try to reproduce the problem; this issue doesnt appear with v17 PGI. Is this the code (in Action_GIST::Print()) that leads to the round-off error? :

double rR = fabs(  voxel_Q_[gr_pt][q1  ] * voxel_Q_[gr_pt][q0  ]
                               + voxel_Q_[gr_pt][q1+1] * voxel_Q_[gr_pt][q0+1]
                               + voxel_Q_[gr_pt][q1+2] * voxel_Q_[gr_pt][q0+2]
                               + voxel_Q_[gr_pt][q1+3] * voxel_Q_[gr_pt][q0+3] ); 

If so, can't we just cast all the voxel values to double during the calculation?

double rR = fabs(  (double)voxel_Q_[gr_pt][q1  ] *(double) voxel_Q_[gr_pt][q0  ]
                               + (double)voxel_Q_[gr_pt][q1+1] * (double)voxel_Q_[gr_pt][q0+1]

etc. This can be done anywhere there's potential for round-off error.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

If so, can't we just cast all the voxel values to double during the calculation?

I think this should give us consistent results between compilers. But the angle would not be much more accurate, because the quaternions are not normalized in double precision. A small error in the absolute value of the quaternion leads to a bigger error in the angle because arccos is very steep close to 1.

But, as I said, I expect that this is only relevant very rarely.

@drroe
Copy link
Contributor

drroe commented Jun 3, 2022

But the angle would not be much more accurate, because the quaternions are not normalized in double precision.

I think I see now. It's the values being put into the voxel_Q_ array itself that are the problem; this could only be solved by making voxel_Q_ double precision, which is a dramatic increase in memory as you have said.

But, as I said, I expect that this is only relevant very rarely.

If your feeling is that the code is good as-is that's fine. I think we do either (or both) of the following:

  1. Increase the test tolerance.
  2. Add a different test where the values are not so sensitive.

Doing both might provide the best coverage, but if you just want to do 1 I'm not opposed to that.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

I quickly checked, and both compilers give the different result when casting to double. I.e., it seems that GCC is calculating the dot product in float, while PGI is casting to double by default.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

Using the following snippet, both compilers print out 0.0152023, which is consistent with the quaternion library, even though the input variables are still in float.

#include <iostream>
#include <cmath>

using TYPE = float;

inline double cos_qdist(const double w1, const double x1, const double y1, const double z1, const double w2, const double x2, const double y2, const double z2)
{
    double square_mag1 = w1*w1 + x1*x1 + y1*y1 + z1*z1;
    double square_mag2 = w2*w2 + x2*x2 + y2*y2 + z2*z2;
    double dotprod = w1*w2 + x1*x2 + y1*y2 + z1*z2;
    return dotprod / sqrt(square_mag1*square_mag2);
}

int main()
{
    TYPE w1 = 0.5080507994;
    TYPE x1 = 0.4968648851;
    TYPE y1 = 0.7026906610;
    TYPE z1 = -0.0351498947;

    TYPE w2 = 0.5141307712;
    TYPE x2 = 0.4948045015;
    TYPE y2 = 0.6995792985;
    TYPE z2 = -0.0377739333;

    double norm = w1*w2 + x1*x2 + y1*y2 + z1*z2;
    /* double ang = 2*acos(fabs(w1*w2 + x1*x2 + y1*y2 + z1*z2)); */
    double ang = 2*acos(fabs(cos_qdist(w1, x1, y1, z1, w2, x2, y2, z2)));
    std::cout << norm << " " << ang << std::endl;

    return 0;
}

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

We could also avoid the sqrt by assuming that the quaternions are close to normalized. There is an approximate equation as follows:

inline double cos_qdist(const double w1, const double x1, const double y1, const double z1, const double w2, const double x2, const double y2, const double z2)
{
    double square_mag1 = w1*w1 + x1*x1 + y1*y1 + z1*z1;
    double square_mag2 = w2*w2 + x2*x2 + y2*y2 + z2*z2;
    double dotprod = w1*w2 + x1*x2 + y1*y2 + z1*z2;
    double both = square_mag1 * square_mag2;
    double inv_sqrt = 2.0 / (1.0 + both);
    return dotprod * inv_sqrt;
}

This still gives the correct result on both compilers. I would have to test how much slowdown this would be compared to the existing implementation.

Edit:
the equation is taken from https://stackoverflow.com/questions/11667783/quaternion-and-normalization, and is a Pade approximation.

@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 3, 2022

I am getting a slowdown of 22% (Action_Post) with the more exact quaternion distance. The six-dimensional distance is different by 0.16 kcal/mol over the entire grid. This is surprisingly high (and I will double-check this next week). However, I am not sure whether it is worth the slowdown, because we also have other error sources of similar magnitude (even the output file precision might be in that range).

@tkurtzman
Copy link

tkurtzman commented Jun 3, 2022 via email

@drroe
Copy link
Contributor

drroe commented Jun 3, 2022

Yeah. I'd rather be slower and keep the accuracy. Those errors will be magnified with bigger systems.

Agreed. @fwaibl let me know when you think the code is final and I can merge it.

* add functions to compute the distance between almost normalized
  quaternions, such as those cast from float to double.
* update unitTests with new functions
@fwaibl
Copy link
Contributor Author

fwaibl commented Jun 7, 2022

Now everything seems consistent. If you agree with the new code and there is nothing else to correct, I think we could merge.

@drroe drroe merged commit c7526d7 into Amber-MD:master Jun 7, 2022
@tkurtzman
Copy link

tkurtzman commented Jun 7, 2022 via email

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.

4 participants