Task: Fixes #6241 (rdkit/rdkit#6257)

org: rdkit
repo: rdkit
pr_number: 6257
issue_title: Fixes #6241
issue_body: Adds a new function to `MolFileStereochem.h` and the python wrapper to apply the "standard" semantics of the chiral flag
resolved_issues: number: 6241
title: Add a function to translate the MDL chiral flag into enhanced stereo groups
body: The "chiral flag" is a feature that was used in MDL v2000 mol files, but was superseded by Enhanced Stereo Groups, which are much more flexible, in the v3000 format.

It would be useful to have some infrastructure that allows us to "translate" the stereochemistry read from a v2000 mol block into Enhanced Stereo Groups. If my understanding of the related documentation is correct, this code should do the following:

For all atoms which are not already included in an Enhanced Stereo Group (these might have been defined at some point after reading the original v2000 input), 
- if the `RDKit::common_properties::_MolFileChiralFlag` property is set to "1", find or create the "ABS" Enhanced Stereo Group on the mol (which must be unique), and add the atoms to it.
- if the `RDKit::common_properties::_MolFileChiralFlag` property is set to "0", create a new "AND" Enhanced Stereo Group, and add all atoms to it.


Reference solution:
fix_patch: diff --git a/Code/GraphMol/FileParsers/MolFileParser.cpp b/Code/GraphMol/FileParsers/MolFileParser.cpp
index cb3c9e5cf6d..df8a93560e0 100644
--- a/Code/GraphMol/FileParsers/MolFileParser.cpp
+++ b/Code/GraphMol/FileParsers/MolFileParser.cpp
@@ -1388,7 +1388,8 @@ void setRGPProps(const std::string_view symb, Atom *res) {
   res->setProp(common_properties::dummyLabel, symbc);
 }
 
-void lookupAtomicNumber(Atom *res, const std::string &symb, bool strictParsing) {
+void lookupAtomicNumber(Atom *res, const std::string &symb,
+                        bool strictParsing) {
   try {
     res->setAtomicNum(PeriodicTable::getTable()->getAtomicNumber(symb));
   } catch (const Invar::Invariant &e) {
@@ -3057,9 +3058,7 @@ bool ParseV3000CTAB(std::istream *inStream, unsigned int &line, RWMol *mol,
     chiralFlag = FileParserUtils::toUnsigned(splitLine[4]);
   }
 
-  if (chiralFlag) {
-    mol->setProp(common_properties::_MolFileChiralFlag, chiralFlag);
-  }
+  mol->setProp(common_properties::_MolFileChiralFlag, chiralFlag);
 
   if (nAtoms) {
     ParseV3000AtomBlock(inStream, line, nAtoms, mol, conf, strictParsing);
@@ -3440,9 +3439,7 @@ RWMol *MolDataStreamToMol(std::istream *inStream, unsigned int &line,
     }
   }
 
-  if (chiralFlag) {
-    res->setProp(common_properties::_MolFileChiralFlag, chiralFlag);
-  }
+  res->setProp(common_properties::_MolFileChiralFlag, chiralFlag);
 
   Conformer *conf = nullptr;
   try {
diff --git a/Code/GraphMol/FileParsers/MolFileStereochem.cpp b/Code/GraphMol/FileParsers/MolFileStereochem.cpp
index b179e80ca6c..49ba6e92c28 100644
--- a/Code/GraphMol/FileParsers/MolFileStereochem.cpp
+++ b/Code/GraphMol/FileParsers/MolFileStereochem.cpp
@@ -321,4 +321,59 @@ void markUnspecifiedStereoAsUnknown(ROMol &mol, int confId) {
   }
 }
 
+void translateChiralFlagToStereoGroups(ROMol &mol,
+                                       StereoGroupType zeroFlagGroupType) {
+  if (!mol.hasProp(common_properties::_MolFileChiralFlag)) {
+    return;
+  }
+  int flagVal = 0;
+  mol.getProp(common_properties::_MolFileChiralFlag, flagVal);
+  mol.clearProp(common_properties::_MolFileChiralFlag);
+
+  StereoGroupType sgType =
+      flagVal ? StereoGroupType::STEREO_ABSOLUTE : zeroFlagGroupType;
+
+  auto sgs = mol.getStereoGroups();
+
+  boost::dynamic_bitset<> sgAtoms(mol.getNumAtoms());
+  const StereoGroup *absGroup = nullptr;
+  for (const auto &sg : sgs) {
+    for (const auto aptr : sg.getAtoms()) {
+      sgAtoms.set(aptr->getIdx());
+    }
+    // if we already have an ABS group, we'll add to it
+    if (sgType == StereoGroupType::STEREO_ABSOLUTE && !absGroup &&
+        sg.getGroupType() == StereoGroupType::STEREO_ABSOLUTE) {
+      absGroup = &sg;
+    }
+  }
+  ROMol::ATOM_PTR_VECT stereoAts;
+  for (const auto atom : mol.atoms()) {
+    if (!sgAtoms[atom->getIdx()] &&
+        (atom->getChiralTag() == Atom::ChiralType::CHI_TETRAHEDRAL_CCW ||
+         atom->getChiralTag() == Atom::ChiralType::CHI_TETRAHEDRAL_CW)) {
+      stereoAts.push_back(atom);
+    }
+  }
+  if (!stereoAts.empty()) {
+    if (!absGroup) {
+      sgs.emplace_back(sgType, stereoAts);
+      mol.setStereoGroups(sgs);
+    } else {
+      std::vector<StereoGroup> newSgs;
+      for (const auto &sg : sgs) {
+        if (&sg != absGroup) {
+          newSgs.push_back(sg);
+        } else {
+          ROMol::ATOM_PTR_VECT newStereoAtoms = sg.getAtoms();
+          newStereoAtoms.insert(newStereoAtoms.end(), stereoAts.begin(),
+                                stereoAts.end());
+          newSgs.emplace_back(StereoGroupType::STEREO_ABSOLUTE, newStereoAtoms);
+        }
+      }
+      mol.setStereoGroups(newSgs);
+    }
+  }
+}
+
 }  // namespace RDKit
diff --git a/Code/GraphMol/FileParsers/MolFileStereochem.h b/Code/GraphMol/FileParsers/MolFileStereochem.h
index 405d68fe094..47a691ba912 100644
--- a/Code/GraphMol/FileParsers/MolFileStereochem.h
+++ b/Code/GraphMol/FileParsers/MolFileStereochem.h
@@ -1,5 +1,5 @@
 //
-//  Copyright (C) 2004-2021 Greg Landrum and other RDKit contributors
+//  Copyright (C) 2004-2023 Greg Landrum and other RDKit contributors
 //
 //   @@ All Rights Reserved @@
 //  This file is part of the RDKit.
@@ -82,5 +82,27 @@ RDKIT_FILEPARSERS_EXPORT void invertMolBlockWedgingInfo(ROMol &mol);
 RDKIT_FILEPARSERS_EXPORT void markUnspecifiedStereoAsUnknown(ROMol &mol,
                                                              int confId = -1);
 
+//! generate enhanced stereo groups based on the status of the chiral flag
+/// property
+/*
+ \param mol: molecule to be modified
+ \param zeroFlagGroupType: how to handle non-grouped stereo centers when the
+        chiral flag is set to zero
+
+  If the chiral flag is set to a value of 1 then all specified tetrahedral
+  chiral centers which are not already in StereoGroups will be added to an
+  ABS StereoGroup.
+
+  If the chiral flag is set to a value of 0 then all specified tetrahedral
+  chiral centers will be added to a StereoGroup of the type zeroFlagGroupType
+
+  If there is no chiral flag set (i.e. the property is not present), the
+  molecule will not be modified.
+
+*/
+RDKIT_FILEPARSERS_EXPORT void translateChiralFlagToStereoGroups(
+    ROMol &mol,
+    StereoGroupType zeroFlagGroupType = StereoGroupType::STEREO_AND);
+
 }  // namespace RDKit
 #endif
diff --git a/Code/GraphMol/FileParsers/molfile_stereo_catch.cpp b/Code/GraphMol/FileParsers/molfile_stereo_catch.cpp
index 477b21d993e..4b295a3cee6 100644
--- a/Code/GraphMol/FileParsers/molfile_stereo_catch.cpp
+++ b/Code/GraphMol/FileParsers/molfile_stereo_catch.cpp
@@ -32,8 +32,150 @@ TEST_CASE("Github #5863: failure in WedgeMolBonds") {
     std::unique_ptr<ROMol> frag(Subgraphs::pathToSubmol(*mol, env));
     REQUIRE(frag);
     WedgeMolBonds(*frag, &frag->getConformer());
+    INFO(MolToV3KMolBlock(*frag));
     CHECK(frag->getBondBetweenAtoms(9, 10)->getBondDir() !=
           Bond::BondDir::NONE);
-    std::cerr << MolToV3KMolBlock(*frag) << std::endl;
   }
-}
\ No newline at end of file
+}
+
+TEST_CASE("translating the chiral flag to stereo groups") {
+  SECTION("basics") {
+    auto withFlag = R"CTAB(
+  Mrv2211 03302308372D          
+
+  5  4  0  0  1  0            999 V2000
+   -6.5625    3.9286    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
+   -5.8480    4.3411    0.0000 C   0  0  1  0  0  0  0  0  0  0  0  0
+   -5.1336    3.9286    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+   -6.1775    5.0826    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
+   -5.4384    5.0893    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  2  3  1  0  0  0  0
+  2  5  1  1  0  0  0
+  2  4  1  6  0  0  0
+M  END
+)CTAB"_ctab;
+    REQUIRE(withFlag);
+    int flag;
+    CHECK(withFlag->getPropIfPresent(common_properties::_MolFileChiralFlag,
+                                     flag));
+    CHECK(flag == 1);
+    RWMol zeroFlag(*withFlag);
+    zeroFlag.setProp(common_properties::_MolFileChiralFlag, 0);
+    RWMol noFlag(*withFlag);
+    noFlag.clearProp(common_properties::_MolFileChiralFlag);
+    CHECK(withFlag->getStereoGroups().empty());
+
+    translateChiralFlagToStereoGroups(*withFlag);
+    CHECK(!withFlag->hasProp(common_properties::_MolFileChiralFlag));
+    auto sgs = withFlag->getStereoGroups();
+    REQUIRE(sgs.size() == 1);
+    CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_ABSOLUTE);
+    CHECK(sgs[0].getAtoms().size() == 1);
+    CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+
+    {
+      RWMol cp(zeroFlag);
+      translateChiralFlagToStereoGroups(cp);
+      CHECK(!cp.hasProp(common_properties::_MolFileChiralFlag));
+      auto sgs = cp.getStereoGroups();
+      REQUIRE(sgs.size() == 1);
+      CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_AND);
+      CHECK(sgs[0].getAtoms().size() == 1);
+      CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+    }
+    {
+      RWMol cp(zeroFlag);
+      translateChiralFlagToStereoGroups(cp, StereoGroupType::STEREO_OR);
+      CHECK(!cp.hasProp(common_properties::_MolFileChiralFlag));
+      auto sgs = cp.getStereoGroups();
+      REQUIRE(sgs.size() == 1);
+      CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_OR);
+      CHECK(sgs[0].getAtoms().size() == 1);
+      CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+    }
+
+    translateChiralFlagToStereoGroups(noFlag);
+    CHECK(!noFlag.hasProp(common_properties::_MolFileChiralFlag));
+    CHECK(noFlag.getStereoGroups().empty());
+  }
+
+  SECTION("explicit zero chiral flag") {
+    auto zeroFlag = R"CTAB(
+  Mrv2211 03302308372D          
+
+  5  4  0  0  o  0            999 V2000
+   -6.5625    3.9286    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
+   -5.8480    4.3411    0.0000 C   0  0  1  0  0  0  0  0  0  0  0  0
+   -5.1336    3.9286    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+   -6.1775    5.0826    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
+   -5.4384    5.0893    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  2  3  1  0  0  0  0
+  2  5  1  1  0  0  0
+  2  4  1  6  0  0  0
+M  END
+)CTAB"_ctab;
+    REQUIRE(zeroFlag);
+    int flag;
+    CHECK(zeroFlag->getPropIfPresent(common_properties::_MolFileChiralFlag,
+                                     flag));
+    CHECK(flag == 0);
+    {
+      RWMol cp(*zeroFlag);
+      translateChiralFlagToStereoGroups(cp);
+      CHECK(!cp.hasProp(common_properties::_MolFileChiralFlag));
+      auto sgs = cp.getStereoGroups();
+      REQUIRE(sgs.size() == 1);
+      CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_AND);
+      CHECK(sgs[0].getAtoms().size() == 1);
+      CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+    }
+  }
+
+  SECTION("multiple stereocenters") {
+    auto noFlag = "C[C@@](N)(F)C[C@](C)(O)F"_smiles;
+    REQUIRE(noFlag);
+    RWMol withFlag(*noFlag);
+    withFlag.setProp(common_properties::_MolFileChiralFlag, 1);
+
+    CHECK(withFlag.getStereoGroups().empty());
+    translateChiralFlagToStereoGroups(withFlag);
+    CHECK(!withFlag.hasProp(common_properties::_MolFileChiralFlag));
+    auto sgs = withFlag.getStereoGroups();
+    REQUIRE(sgs.size() == 1);
+    CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_ABSOLUTE);
+    CHECK(sgs[0].getAtoms().size() == 2);
+    CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+    CHECK(sgs[0].getAtoms()[1]->getIdx() == 5);
+
+    {
+      RWMol zeroFlag(*noFlag);
+      zeroFlag.setProp(common_properties::_MolFileChiralFlag, 0);
+      translateChiralFlagToStereoGroups(zeroFlag);
+      CHECK(!zeroFlag.hasProp(common_properties::_MolFileChiralFlag));
+      auto sgs = zeroFlag.getStereoGroups();
+      REQUIRE(sgs.size() == 1);
+      CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_AND);
+      CHECK(sgs[0].getAtoms().size() == 2);
+      CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+      CHECK(sgs[0].getAtoms()[1]->getIdx() == 5);
+    }
+  }
+
+  SECTION("pre-existing ABS stereogroup") {
+    auto withFlag = "C[C@@](N)(F)C[C@](C)(O)F |a:1|"_smiles;
+    REQUIRE(withFlag);
+    withFlag->setProp(common_properties::_MolFileChiralFlag, 1);
+
+    CHECK(withFlag->getStereoGroups().size() == 1);
+    translateChiralFlagToStereoGroups(*withFlag);
+    CHECK(!withFlag->hasProp(common_properties::_MolFileChiralFlag));
+    auto sgs = withFlag->getStereoGroups();
+    REQUIRE(sgs.size() == 1);
+    CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_ABSOLUTE);
+    CHECK(sgs[0].getAtoms().size() == 2);
+    CHECK(sgs[0].getAtoms()[0]->getIdx() == 1);
+    CHECK(sgs[0].getAtoms()[1]->getIdx() == 5);
+  }
+}
diff --git a/Code/GraphMol/Wrap/MolOps.cpp b/Code/GraphMol/Wrap/MolOps.cpp
index d199197ef9e..f1c97c09d74 100644
--- a/Code/GraphMol/Wrap/MolOps.cpp
+++ b/Code/GraphMol/Wrap/MolOps.cpp
@@ -2878,6 +2878,27 @@ A note on the flags controlling which atoms/bonds are modified:
                 Chirality::getUseLegacyStereoPerception,
                 "returns whether or not the legacy stereo perception code is "
                 "being used");
+
+    python::def(
+        "TranslateChiralFlagToStereoGroups", translateChiralFlagToStereoGroups,
+        (python::arg("mol"),
+         python::arg("zeroFlagGroupType") = StereoGroupType::STEREO_AND),
+        R"DOC(Generate enhanced stereo groups based on the status of the chiral flag property.
+
+  Arguments:
+   - mol: molecule to be modified
+   - zeroFlagGroupType: how to handle non-grouped stereo centers when the
+          chiral flag is set to zero
+
+  If the chiral flag is set to a value of 1 then all specified tetrahedral
+  chiral centers which are not already in StereoGroups will be added to an
+  ABS StereoGroup.
+
+  If the chiral flag is set to a value of 0 then all specified tetrahedral
+  chiral centers will be added to a StereoGroup of the type zeroFlagGroupType
+
+  If there is no chiral flag set (i.e. the property is not present), the
+  molecule will not be modified.)DOC");
   }
 };
 }  // namespace RDKit
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index ed8dcd520ab..fd56319c6e9 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -12,6 +12,7 @@
 - The doIsomericSmiles parameter in Java and C# ROMol.MolToSmiles() now defaults to true (previously it was false), thus aligning to the C++ and Python behavior.
 - Double bonds which are marked as crossed (i.e. `bond.GetBondDir() == Bond.BondDir.EITHERDOUBLE`) now have their BondStereo set to `Bond.BondStereo.STEREOANY` and the BondDir information removed by default when molecules are parsed or `AssignStereochemistry()` is called with the `cleanIt` argument set to True.
 - The conformers generated for molecules with three-coordinate chiral centers will be somewhat different due to the fix for #5883.
+- Molecules which come from Mol or SDF files will now always have the "_MolFileChiralFlag" property set to the value of the chiral flag in the CTAB. In previous versions the property was not set if the chiral flag was 0.
 
 
 ## Bug Fixes: