Files
rdkit/Code/GraphMol/MolInterchange/molinterchange_catch.cpp
Greg Landrum 45681a1c04 Switch from using RapidJSON to Boost::JSON for MolInterchange (#8859)
* First pass at port

Mostly auto-converted using claude sonnet 4

Things are a bit slower in this initial port. Here's some timing data for molecules from SMILES (no coords) and from SDF (with coords)

# MASTER
## smiles
read: 50000 mols.
 9.260000s wall, 8.650000s user + 0.600000s system = 9.250000s CPU (99.9%)
serialize
 3.060000s wall, 2.400000s user + 0.660000s system = 3.060000s CPU (100.0%)
deserialize
 1.350000s wall, 1.250000s user + 0.090000s system = 1.340000s CPU (99.3%)

## SDF
read: 50000 mols.
 9.340000s wall, 8.930000s user + 0.400000s system = 9.330000s CPU (99.9%)
serialize
 6.630000s wall, 5.960000s user + 0.680000s system = 6.640000s CPU (100.2%)
deserialize
 1.450000s wall, 1.450000s user + 0.000000s system = 1.450000s CPU (100.0%)

# Boost::JSON
## smiles
read: 50000 mols.
 9.250000s wall, 8.830000s user + 0.420000s system = 9.250000s CPU (100.0%)
serialize
 4.770000s wall, 4.410000s user + 0.350000s system = 4.760000s CPU (99.8%)
deserialize
 2.320000s wall, 2.100000s user + 0.230000s system = 2.330000s CPU (100.4%)

## SDF
read: 50000 mols.
 9.500000s wall, 9.100000s user + 0.400000s system = 9.500000s CPU (100.0%)
serialize
 8.760000s wall, 8.330000s user + 0.420000s system = 8.750000s CPU (99.9%)
deserialize
 2.540000s wall, 2.330000s user + 0.210000s system = 2.540000s CPU (100.0%)

* some json parser optimization

* around the edges

* optimizations for the writer

* hopefully get things compiling

* convert the MinimalLib stuff to use boost::json

Again, a lot of the lifting here was done using Claude Sonnet 4 in VS Code Copilot

* fix Windows DLL build

* response to review

Co-authored-by: Paolo Tosco <paolo.tosco.mail@gmail.com>

* better not to blindly accept suggestions

* fix the problems in MinimalLib

---------

Co-authored-by: Paolo Tosco <paolo.tosco.mail@gmail.com>
Co-authored-by: = <=>
2025-11-11 11:54:44 +01:00

512 lines
18 KiB
C++

//
// Copyright (C) 2021-2025 Greg Landrum and other RDKit contributors
// @@ All Rights Reserved @@
// This file is part of the RDKit.
// The contents are covered by the terms of the BSD license
// which is included in the file license.txt, found at the root
// of the RDKit source tree.
//
#include <RDGeneral/test.h>
#include <catch2/catch_all.hpp>
#include <GraphMol/RDKitBase.h>
#include <GraphMol/MolPickler.h>
#include <GraphMol/FileParsers/FileParsers.h>
#include <GraphMol/SmilesParse/SmilesWrite.h>
#include <GraphMol/SmilesParse/SmilesParse.h>
#include <GraphMol/SmilesParse/SmartsWrite.h>
#include "MolInterchange.h"
#include <RDGeneral/FileParseException.h>
using namespace RDKit;
TEST_CASE("queries to JSON", "[query]") {
SECTION("SMARTS 1") {
auto mol = "C[cH]:[#7+]"_smarts;
REQUIRE(mol);
auto json = MolInterchange::MolToJSONData(*mol);
// no implicit Hs added with queries
CHECK(json.find("{\"impHs\":3}") == std::string::npos);
CHECK(json.find("\"descr\":\"AtomHCount\"") != std::string::npos);
auto nmols = MolInterchange::JSONDataToMols(json);
CHECK(nmols.size() == 1);
CHECK(MolToSmarts(*nmols[0]) == MolToSmarts(*mol));
}
SECTION("Recursive SMARTS 1") {
auto mol = "[C;R2]=,#!@[$(CO),r6]"_smarts;
REQUIRE(mol);
auto json = MolInterchange::MolToJSONData(*mol);
CHECK(json.find("\"subquery\":") != std::string::npos);
auto nmols = MolInterchange::JSONDataToMols(json);
CHECK(nmols.size() == 1);
CHECK(MolToSmarts(*nmols[0]) == MolToSmarts(*mol));
}
SECTION("mol blocks") {
auto mol = R"CTAB(
Mrv2102 04092105442D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 5 4 0 0 0
M V30 BEGIN ATOM
M V30 1 Q -1.4583 0.4583 0 0
M V30 2 C -0.1247 1.2283 0 0
M V30 3 C 1.209 0.4583 0 0
M V30 4 C 2.5427 1.2283 0 0
M V30 5 C 1.209 -1.0817 0 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 1 1 1 2
M V30 2 1 3 4
M V30 3 5 2 3
M V30 4 1 3 5 TOPO=2
M V30 END BOND
M V30 END CTAB
M END
)CTAB"_ctab;
REQUIRE(mol);
auto json = MolInterchange::MolToJSONData(*mol);
CHECK(json.find("\"type\":\"Q\"") != std::string::npos);
auto ps = MolInterchange::JSONParseParameters();
ps.useHCounts = false;
auto nmols = MolInterchange::JSONDataToMols(json, ps);
CHECK(nmols.size() == 1);
auto mb = MolToV3KMolBlock(*nmols[0]);
CHECK(mb.find(" Q ") != std::string::npos);
CHECK(mb.find("TOPO=2") != std::string::npos);
}
}
TEST_CASE("StereoGroups") {
auto ormol = "C[C@H](O)C[C@@H](C)F |o1:1,4|"_smiles;
REQUIRE(ormol);
auto andmol = "C[C@H](O)CC[C@@H](C)F |&1:1,5|"_smiles;
REQUIRE(andmol);
auto absmol = "CC(O)C[C@@H](C)F |a:4|"_smiles;
REQUIRE(absmol);
MolInterchange::JSONWriteParameters commonChemPs;
commonChemPs.useRDKitExtensions = false;
SECTION("writing") {
{
auto json = MolInterchange::MolToJSONData(*ormol);
CHECK(json.find("stereoGroups") != std::string::npos);
CHECK(json.find("\"or\"") != std::string::npos);
CHECK(json.find("[1,4]") != std::string::npos);
json = MolInterchange::MolToJSONData(*ormol, commonChemPs);
CHECK(json.find("stereoGroups") == std::string::npos);
CHECK(json.find("\"or\"") == std::string::npos);
CHECK(json.find("[1,4]") == std::string::npos);
}
{
auto json = MolInterchange::MolToJSONData(*andmol);
CHECK(json.find("stereoGroups") != std::string::npos);
CHECK(json.find("\"and\"") != std::string::npos);
CHECK(json.find("[1,5]") != std::string::npos);
json = MolInterchange::MolToJSONData(*ormol, commonChemPs);
CHECK(json.find("stereoGroups") == std::string::npos);
CHECK(json.find("\"and\"") == std::string::npos);
CHECK(json.find("[1,5]") == std::string::npos);
}
{
auto json = MolInterchange::MolToJSONData(*absmol);
CHECK(json.find("stereoGroups") != std::string::npos);
CHECK(json.find("\"abs\"") != std::string::npos);
CHECK(json.find("[4]") != std::string::npos);
json = MolInterchange::MolToJSONData(*ormol, commonChemPs);
CHECK(json.find("stereoGroups") == std::string::npos);
CHECK(json.find("\"abs\"") == std::string::npos);
CHECK(json.find("[4]") == std::string::npos);
}
}
SECTION("reading") {
{
auto json = MolInterchange::MolToJSONData(*ormol);
auto mols = MolInterchange::JSONDataToMols(json);
REQUIRE(mols.size() == 1);
auto sgs = mols[0]->getStereoGroups();
REQUIRE(sgs.size() == 1);
CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_OR);
auto ats = sgs[0].getAtoms();
REQUIRE(ats.size() == 2);
CHECK(ats[0]->getIdx() == 1);
CHECK(ats[1]->getIdx() == 4);
}
{
auto json = MolInterchange::MolToJSONData(*andmol);
auto mols = MolInterchange::JSONDataToMols(json);
REQUIRE(mols.size() == 1);
auto sgs = mols[0]->getStereoGroups();
REQUIRE(sgs.size() == 1);
CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_AND);
auto ats = sgs[0].getAtoms();
REQUIRE(ats.size() == 2);
CHECK(ats[0]->getIdx() == 1);
CHECK(ats[1]->getIdx() == 5);
}
{
auto json = MolInterchange::MolToJSONData(*absmol);
auto mols = MolInterchange::JSONDataToMols(json);
REQUIRE(mols.size() == 1);
auto sgs = mols[0]->getStereoGroups();
REQUIRE(sgs.size() == 1);
CHECK(sgs[0].getGroupType() == StereoGroupType::STEREO_ABSOLUTE);
auto ats = sgs[0].getAtoms();
REQUIRE(ats.size() == 1);
CHECK(ats[0]->getIdx() == 4);
}
}
SECTION("multiple groups") {
auto mol =
"C[C@H]1OC([C@H](O)F)[C@@H](C)[C@@H](C)C1[C@@H](O)F |a:1,o1:4,12,&1:7,&2:9|"_smiles;
REQUIRE(mol);
auto json = MolInterchange::MolToJSONData(*mol);
CHECK(json.find("stereoGroups") != std::string::npos);
CHECK(json.find("\"abs\"") != std::string::npos);
CHECK(json.find("\"or\"") != std::string::npos);
CHECK(json.find("\"and\"") != std::string::npos);
auto mols = MolInterchange::JSONDataToMols(json);
REQUIRE(mols.size() == 1);
auto sgs = mols[0]->getStereoGroups();
REQUIRE(sgs.size() == 4);
// rather than worry about group order here, just check the CXSMILES:
auto smi = MolToCXSmiles(*mols[0]);
CHECK(
smi ==
"C[C@H]1OC([C@H](O)F)[C@H](C)[C@H](C)C1[C@@H](O)F |a:1,o1:4,12,&1:7,&2:9|");
}
SECTION("writing multiple mols") {
std::vector<ROMol *> mols{ormol.get(), andmol.get()};
auto json = MolInterchange::MolsToJSONData(mols);
CHECK(json.find("stereoGroups") != std::string::npos);
CHECK(json.find("\"or\"") != std::string::npos);
CHECK(json.find("[1,4]") != std::string::npos);
CHECK(json.find("\"and\"") != std::string::npos);
CHECK(json.find("[1,5]") != std::string::npos);
json = MolInterchange::MolsToJSONData(mols, commonChemPs);
CHECK(json.find("stereoGroups") == std::string::npos);
}
}
TEST_CASE("SubstanceGroups") {
auto polymol = R"CTAB(
Mrv2219 12292206542D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 5 4 1 0 0
M V30 BEGIN ATOM
M V30 1 * -6.7083 3.2083 0 0
M V30 2 C -5.3747 3.9783 0 0
M V30 3 O -4.041 3.2083 0 0
M V30 4 * -2.7073 3.9783 0 0
M V30 5 C -5.3747 5.5183 0 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 1 1 1 2
M V30 2 1 2 3
M V30 3 1 3 4
M V30 4 1 2 5
M V30 END BOND
M V30 BEGIN SGROUP
M V30 1 SRU 0 ATOMS=(3 3 2 5) XBONDS=(2 3 1) BRKXYZ=(9 -3.9538 4.3256 0 -
M V30 -3.0298 2.7252 0 0 0 0) BRKXYZ=(9 -5.4618 2.8611 0 -6.3858 4.4615 0 0 -
M V30 0 0) CONNECT=HT LABEL=n
M V30 END SGROUP
M V30 END CTAB
M END
)CTAB"_ctab;
REQUIRE(polymol);
auto supmol = R"CTAB(example
-ISIS- 10171405052D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 14 15 1 0 0
M V30 BEGIN ATOM
M V30 1 C 6.4292 -1.1916 0 0 CFG=3
M V30 2 C 7.0125 -0.6042 0 0
M V30 3 N 6.4292 -0.0250999 0 0
M V30 4 C 5.8416 -0.6042 0 0
M V30 5 C 5.8416 -1.7708 0 0
M V30 6 N 6.4292 -2.3584 0 0 CFG=3
M V30 7 C 7.0125 -1.7708 0 0
M V30 8 O 5.7166 -3.5875 0 0
M V30 9 C 5.7166 -4.4125 0 0 CFG=3
M V30 10 C 4.8875 -4.4125 0 0
M V30 11 C 6.5376 -4.4166 0 0
M V30 12 C 5.7166 -5.2376 0 0
M V30 13 C 6.4292 -3.175 0 0
M V30 14 O 7.1375 -3.5875 0 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 1 1 1 2
M V30 2 1 2 3
M V30 3 1 3 4
M V30 4 1 4 1
M V30 5 1 1 5
M V30 6 1 5 6
M V30 7 1 6 7
M V30 8 1 7 1
M V30 9 1 6 13
M V30 10 1 8 9
M V30 11 1 9 10
M V30 12 1 9 11
M V30 13 1 9 12
M V30 14 2 13 14
M V30 15 1 8 13
M V30 END BOND
M V30 BEGIN SGROUP
M V30 1 SUP 0 ATOMS=(7 8 9 10 11 12 13 14) XBONDS=(1 9) BRKXYZ=(9 6.24 -2.9 0 -
M V30 6.24 -2.9 0 0 0 0) CSTATE=(4 9 0 0.82 0) LABEL=Boc SAP=(3 13 6 1)
M V30 END SGROUP
M V30 END CTAB
M END)CTAB"_ctab;
REQUIRE(supmol);
MolInterchange::JSONWriteParameters commonChemPs;
commonChemPs.useRDKitExtensions = false;
SECTION("writing") {
{
auto json = MolInterchange::MolToJSONData(*polymol);
CHECK(json.find("substanceGroups") != std::string::npos);
CHECK(json.find("\"TYPE\":\"SRU\"") != std::string::npos);
CHECK(json.find("\"atoms\":[2,1,4]") != std::string::npos);
CHECK(json.find("\"bonds\":[2,0]") != std::string::npos);
CHECK(json.find("\"brackets\"") != std::string::npos);
CHECK(json.find("\"cstates\"") == std::string::npos);
CHECK(json.find("\"attachPoints\"") == std::string::npos);
}
{
auto json = MolInterchange::MolToJSONData(*polymol, commonChemPs);
CHECK(json.find("substanceGroups") == std::string::npos);
}
{
auto json = MolInterchange::MolToJSONData(*supmol);
CHECK(json.find("substanceGroups") != std::string::npos);
CHECK(json.find("\"TYPE\":\"SUP\"") != std::string::npos);
CHECK(json.find("\"atoms\":[7,8,9,10,11,12,13]") != std::string::npos);
CHECK(json.find("\"bonds\":[8]") != std::string::npos);
CHECK(json.find("\"brackets\"") != std::string::npos);
CHECK(json.find("\"cstates\"") != std::string::npos);
CHECK(json.find("\"bond\":8") != std::string::npos);
CHECK(json.find("\"vector\"") != std::string::npos);
CHECK(json.find("\"attachPoints\"") != std::string::npos);
CHECK(json.find("{\"aIdx\":12,\"lvIdx\":5,\"id\":\"1\"}") !=
std::string::npos);
}
}
SECTION("parsing") {
auto json = MolInterchange::MolToJSONData(*supmol);
auto mols = MolInterchange::JSONDataToMols(json);
REQUIRE(mols.size() == 1);
auto sgs = getSubstanceGroups(*mols[0]);
REQUIRE(sgs.size() == 1);
CHECK(sgs[0].getAtoms() == std::vector<unsigned>{7, 8, 9, 10, 11, 12, 13});
CHECK(sgs[0].getBonds() == std::vector<unsigned>{8});
REQUIRE(sgs[0].getBrackets().size() == 1);
REQUIRE(sgs[0].getBrackets()[0].size() == 3);
CHECK(sgs[0].getBrackets()[0][0].x == Catch::Approx(6.24).margin(0.01));
REQUIRE(sgs[0].getCStates().size() == 1);
CHECK(sgs[0].getCStates()[0].bondIdx == 8);
CHECK(sgs[0].getCStates()[0].vector.y == Catch::Approx(0.82).margin(0.01));
REQUIRE(sgs[0].getAttachPoints().size() == 1);
CHECK(sgs[0].getAttachPoints()[0].aIdx == 12);
CHECK(sgs[0].getAttachPoints()[0].lvIdx == 5);
CHECK(sgs[0].getAttachPoints()[0].id == "1");
std::string pval;
CHECK(sgs[0].getPropIfPresent("LABEL", pval));
CHECK(pval == "Boc");
}
}
TEST_CASE("do not crash with null molecules") {
SECTION("only null") {
std::vector<ROMol *> mols{nullptr};
CHECK_THROWS_AS(MolInterchange::MolsToJSONData(mols), ValueErrorException);
}
SECTION("not just null") {
auto tmol = "CCC"_smiles;
std::vector<ROMol *> mols{tmol.get(), nullptr};
CHECK_THROWS_AS(MolInterchange::MolsToJSONData(mols), ValueErrorException);
}
}
TEST_CASE("Test segv reported in #6890") {
{
std::string mol_json_with_bad_format{R"({
"commonchem": 10,
"molecules": [
{
"name": "ethane",
"atoms": [
{"z": 6, "impHs": 3},
{"z": 6, "impHs": 3}
],
"bonds": [
{"type": 1, "atoms": [0, 1]}
]
}
]
})"};
CHECK_THROWS_AS(MolInterchange::JSONDataToMols(mol_json_with_bad_format),
FileParseException);
}
{
std::string mol_json_with_bad_format{R"({
"rdkitjson": 10,
"molecules": [
{
"name": "ethane",
"atoms": [
{"z": 6, "impHs": 3},
{"z": 6, "impHs": 3}
],
"bonds": [
{"type": 1, "atoms": [0, 1]}
]
}
]
})"};
CHECK_THROWS_AS(MolInterchange::JSONDataToMols(mol_json_with_bad_format),
FileParseException);
}
}
TEST_CASE("github #5923: add more error checking to substance groups") {
SECTION("basics") {
std::string moljson =
R"JSON({"rdkitjson":{"version":11},"defaults":{"atom":{"z":6,"impHs":0,"chg":0,"nRad":0,"isotope":0,"stereo":"unspecified"},"bond":{"bo":1,"stereo":"unspecified"}},
"molecules":[{"name":"","atoms":[{"z":0},{"impHs":1},{"z":8},{"z":0},{"impHs":3}],
"bonds":[{"atoms":[0,1]},{"atoms":[1,2]},{"atoms":[2,3]},{"atoms":[1,4]}],
"substanceGroups":[{"properties":{"TYPE":"SRU","index":1,"CONNECT":"HT","LABEL":"n","DATAFIELDS":"[]"},
"atoms":[2,1,4],"bonds":[2,0],"parentAtoms":[0,3],
"brackets":[[[-3.9538,4.3256,0.0],[-3.0298,2.7252,0.0],[0.0,0.0,0.0]],[[-5.4618,2.8611,0.0],[-6.3858,4.4615,0.0],[0.0,0.0,0.0]]]}],
"conformers":[{"dim":2,"coords":[[-6.7083,3.2083],[-5.3747,3.9783],[-4.041,3.2083],[-2.7073,3.9783],[-5.3747,5.5183]]}],"extensions":[{"name":"rdkitRepresentation","formatVersion":2,"toolkitVersion":"2023.09.4","cipRanks":[0,3,4,1,2]},{"name":"rdkitQueries","formatVersion":10,"toolkitVersion":"2023.09.4","atomQueries":[{"descr":"AtomNull","tag":40},{},{},{"descr":"AtomNull","tag":40},{}]}]}]})JSON";
// everything ok
{
auto mols = MolInterchange::JSONDataToMols(moljson);
REQUIRE(mols.size() == 1);
CHECK(getSubstanceGroups(*mols[0]).size() == 1);
}
// bad atom index
{
std::string badjson = moljson;
std::string lookFor = R"JSON("atoms":[2,1,4])JSON";
badjson.replace(badjson.find(lookFor), lookFor.size(),
R"JSON("atoms":[2,1,9])JSON");
CHECK_THROWS_AS(MolInterchange::JSONDataToMols(badjson),
ValueErrorException);
}
// bad parentAtom index
{
std::string badjson = moljson;
std::string lookFor = R"JSON("parentAtoms":[0,3])JSON";
badjson.replace(badjson.find(lookFor), lookFor.size(),
R"JSON("parentAtoms":[8,3])JSON");
CHECK_THROWS_AS(MolInterchange::JSONDataToMols(badjson),
ValueErrorException);
}
// bad bond index
{
std::string badjson = moljson;
std::string lookFor = R"JSON("bonds":[2,0])JSON";
badjson.replace(badjson.find(lookFor), lookFor.size(),
R"JSON("bonds":[2,7])JSON");
CHECK_THROWS_AS(MolInterchange::JSONDataToMols(badjson),
ValueErrorException);
}
}
}
TEST_CASE(
"github #8031: make sure floating point coords are not incorrectly truncated") {
auto mb = R"CTAB(
MJ240300
8 8 0 0 0 0 0 0 0 0999 V2000
-1.4955 1.1152 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.2099 0.7027 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.2099 -0.1223 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.4955 -0.5348 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.7810 -0.1223 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.7810 0.7027 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.0666 1.1152 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.9244 -0.5348 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 2 0 0 0 0
2 3 1 0 0 0 0
3 4 2 0 0 0 0
4 5 1 0 0 0 0
5 6 2 0 0 0 0
6 1 1 0 0 0 0
6 7 1 0 0 0 0
3 8 1 0 0 0 0
M ISO 1 7 2
M END
)CTAB";
std::unique_ptr<RWMol> mol(MolBlockToMol(mb, true, false));
REQUIRE(mol);
std::string pickle;
MolPickler::pickleMol(*mol, pickle);
ROMol molFromPickle(pickle);
auto json = MolInterchange::MolToJSONData(molFromPickle);
INFO(json);
CHECK(json.find("-5.3479999") != std::string::npos);
}
TEST_CASE("github #8460: MolToJSON does not handle atropisomers") {
SECTION("as reported") {
auto m = "C1CC(C)C=1c1c(CC)cccc1C |wU:4.3|"_smiles;
REQUIRE(m);
CHECK(m->getBondWithIdx(4)->getStereo() == Bond::BondStereo::STEREOATROPCW);
auto refSmiles = MolToCXSmiles(*m);
auto json = MolInterchange::MolToJSONData(*m);
CHECK(json.find("\"stereo\":\"atrop_cw\"") != std::string::npos);
auto nmols = MolInterchange::JSONDataToMols(json);
CHECK(nmols.size() == 1);
CHECK(nmols[0]->getBondWithIdx(4)->getStereo() ==
Bond::BondStereo::STEREOATROPCW);
CHECK(MolToCXSmiles(*nmols[0]) == refSmiles);
}
SECTION("enhanced stereo") {
auto m = "C1CC(C)C=1c1c(CC)cccc1C |wU:4.3,&1:4|"_smiles;
REQUIRE(m);
CHECK(m->getBondWithIdx(4)->getStereo() == Bond::BondStereo::STEREOATROPCW);
CHECK(m->getStereoGroups().size() == 1);
auto refSmiles = MolToCXSmiles(*m);
auto json = MolInterchange::MolToJSONData(*m);
CHECK(json.find("\"stereo\":\"atrop_cw\"") != std::string::npos);
auto nmols = MolInterchange::JSONDataToMols(json);
CHECK(nmols.size() == 1);
CHECK(nmols[0]->getBondWithIdx(4)->getStereo() ==
Bond::BondStereo::STEREOATROPCW);
CHECK(nmols[0]->getStereoGroups().size() == 1);
CHECK(MolToCXSmiles(*nmols[0]) == refSmiles);
}
SECTION("round trips") {
std::vector<std::string> smileses = {
"C1CC(C)C=1c1c(CC)cccc1C |wD:4.3|",
"C1CC(C)C=1c1c(CC)cccc1C |wD:4.3,&1:4|"
"C1CC(C)C=1c1c(CC)cccc1C |wU:4.3,o1:4|",
"C1CC(C)C=1c1c(CC)cccc1[C@H](F)Cl |wU:4.3,&1:4,13|"};
for (const auto &smi : smileses) {
auto m = v2::SmilesParse::MolFromSmiles(smi);
REQUIRE(m);
auto refSmiles = MolToCXSmiles(*m);
auto json = MolInterchange::MolToJSONData(*m);
auto nmols = MolInterchange::JSONDataToMols(json);
CHECK(nmols.size() == 1);
CHECK(MolToCXSmiles(*nmols[0]) == refSmiles);
}
}
}