/// <summary> /// Generates and assigns abbreviations to a molecule. Abbreviations are first /// generated with <see cref="Generate(IAtomContainer)"/> and the filtered based on /// the coverage. Currently only abbreviations that cover 100%, or < 40% of the /// atoms are assigned. /// </summary> /// <param name="mol">molecule</param> /// <returns>number of new abbreviations</returns> /// <seealso cref="Generate(IAtomContainer)"/> public int Apply(IAtomContainer mol) { var newSgroups = Generate(mol); var sgroups = mol.GetCtabSgroups(); if (sgroups == null) { sgroups = new List <Sgroup>(); } else { sgroups = new List <Sgroup>(sgroups); } var prev = sgroups.Count; foreach (var sgroup in newSgroups) { var coverage = sgroup.Atoms.Count / (double)mol.Atoms.Count; // update xml comment if changed! if (!sgroup.Bonds.Any() || coverage < 0.4d) { sgroups.Add(sgroup); } } mol.SetCtabSgroups(sgroups); return(sgroups.Count - prev); }
private bool WriteV3000(IAtomContainer container) { if (paramWriteV3000.IsSet) { return(true); } if (container.Atoms.Count > 999) { return(true); } if (container.Bonds.Count > 999) { return(true); } // check for positional variation, this can be output in base V3000 and not V2000 var sgroups = container.GetCtabSgroups(); if (sgroups != null) { foreach (Sgroup sgroup in sgroups) { if (sgroup.Type == SgroupType.ExtMulticenter) { return(true); } } } return(false); }
/// <summary> /// Generate the Sgroup elements for the provided atom contains. /// </summary> /// <param name="container">molecule</param> /// <returns>Sgroup rendering elements</returns> IRenderingElement GenerateSgroups(IAtomContainer container, AtomSymbol[] symbols) { var result = new ElementGroup(); var sgroups = container.GetCtabSgroups(); if (sgroups == null || !sgroups.Any()) { return(result); } var symbolMap = new Dictionary <IAtom, AtomSymbol>(); for (int i = 0; i < symbols.Length; i++) { if (symbols[i] != null) { symbolMap[container.Atoms[i]] = symbols[i]; } } foreach (var sgroup in sgroups) { switch (sgroup.Type) { case SgroupType.CtabAbbreviation: result.Add(GenerateAbbreviationSgroup(container, sgroup)); break; case SgroupType.CtabMultipleGroup: result.Add(GenerateMultipleSgroup(sgroup)); break; case SgroupType.CtabAnyPolymer: case SgroupType.CtabMonomer: case SgroupType.CtabCrossLink: case SgroupType.CtabCopolymer: case SgroupType.CtabStructureRepeatUnit: case SgroupType.CtabMer: case SgroupType.CtabGraft: case SgroupType.CtabModified: result.Add(GeneratePolymerSgroup(sgroup, symbolMap)); break; case SgroupType.CtabComponent: case SgroupType.CtabMixture: case SgroupType.CtabFormulation: result.Add(GenerateMixtureSgroup(sgroup)); break; case SgroupType.CtabGeneric: // not strictly a polymer but okay to draw as one result.Add(GeneratePolymerSgroup(sgroup, null)); break; } } return(result); }
/// <summary> /// If the molecule has display shortcuts (abbreviations or multiple group sgroups) certain parts /// of the structure are hidden from display. This method marks the parts to hide and in the case /// of abbreviations, remaps atom symbols. Apart from additional property flags, the molecule /// is unchanged by this method. /// </summary> /// <param name="container">molecule input</param> /// <param name="symbolRemap">a map that will hold symbol remapping</param> public static void PrepareDisplayShortcuts(IAtomContainer container, IDictionary <IAtom, string> symbolRemap) { var sgroups = container.GetCtabSgroups(); if (sgroups == null || !sgroups.Any()) { return; } // select abbreviations that should be contracted foreach (var sgroup in sgroups) { if (sgroup.Type == SgroupType.CtabAbbreviation) { bool?expansion = (bool?)sgroup.GetValue(SgroupKey.CtabExpansion); // abbreviation is displayed as expanded if (expansion ?? false) { continue; } // no or empty label, skip it if (string.IsNullOrEmpty(sgroup.Subscript)) { continue; } // only contract if the atoms are either partially or fully highlighted if (CheckAbbreviationHighlight(container, sgroup)) { ContractAbbreviation(container, symbolRemap, sgroup); } } else if (sgroup.Type == SgroupType.CtabMultipleGroup) { HideMultipleParts(container, sgroup); } else if (sgroup.Type == SgroupType.ExtMulticenter) { var atoms = sgroup.Atoms; // should only be one bond foreach (var bond in sgroup.Bonds) { var beg = bond.Begin; var end = bond.End; if (atoms.Contains(beg)) { StandardGenerator.HideFully(beg); } else { StandardGenerator.HideFully(end); } } } } }
/// <summary> /// Safely access the Sgroups of a molecule retuning an empty list /// if none are defined.. /// </summary> /// <param name="mol">molecule</param> /// <returns>the sgroups</returns> private static IList <Sgroup> GetSgroups(IAtomContainer mol) { var sgroups = mol.GetCtabSgroups(); if (sgroups == null) { sgroups = Array.Empty <Sgroup>(); } return(sgroups); }
public void PositionalVariation() { using (MDLV3000Reader reader = new MDLV3000Reader(GetType().Assembly.GetManifestResourceStream(GetType(), "multicenterBond.mol"))) { IAtomContainer container = reader.Read(builder.NewAtomContainer()); Assert.AreEqual(8, container.Bonds.Count); var sgroups = container.GetCtabSgroups(); Assert.IsNotNull(sgroups); Assert.AreEqual(1, sgroups.Count); Assert.AreEqual(SgroupType.ExtMulticenter, sgroups[0].Type); } }
// utility method that safely collects the Sgroup from a molecule private static void SafeAddSgroups(List <Sgroup> sgroups, IAtomContainer mol) { var molSgroups = mol.GetCtabSgroups(); if (molSgroups != null) { foreach (var g in molSgroups) { sgroups.Add(g); } } }
/// <summary> /// Write the bonds of a molecule. /// </summary> /// <param name="mol">molecule</param> /// <param name="idxs">index lookup</param> /// <exception cref="IOException">low-level IO error</exception> /// <exception cref="CDKException">inconsistent state etc</exception> private void WriteBondBlock(IAtomContainer mol, IReadOnlyDictionary <IChemObject, int> idxs) { if (mol.Bonds.Count == 0) { return; } // collect multicenter Sgroups before output var sgroups = mol.GetCtabSgroups(); var multicenterSgroups = new Dictionary <IBond, Sgroup>(); if (sgroups != null) { foreach (var sgroup in sgroups) { if (sgroup.Type != SgroupType.ExtMulticenter) { continue; } foreach (var bond in sgroup.Bonds) { multicenterSgroups[bond] = sgroup; } } } writer.Write("BEGIN BOND\n"); int bondIdx = 0; foreach (var bond in mol.Bonds) { var beg = bond.Begin; var end = bond.End; if (beg == null || end == null) { throw new InvalidOperationException($"Bond {bondIdx} had one or more atoms."); } int begIdx = FindIdx(idxs, beg); int endIdx = FindIdx(idxs, end); if (begIdx < 0 || endIdx < 0) { throw new InvalidOperationException($"Bond {bondIdx} had atoms not present in the molecule."); } var stereo = bond.Stereo; // swap beg/end if needed if (stereo == BondStereo.UpInverted || stereo == BondStereo.DownInverted || stereo == BondStereo.UpOrDownInverted) { int tmp = begIdx; begIdx = endIdx; endIdx = tmp; } var order = bond.Order.Numeric(); if (order < 1 || order > 3) { throw new CDKException($"Bond order {bond.Order} cannot be written to V3000"); } writer.Write(++bondIdx) .Write(' ') .Write(order) .Write(' ') .Write(begIdx) .Write(' ') .Write(endIdx); switch (stereo) { case BondStereo.Up: case BondStereo.UpInverted: writer.Write(" CFG=1"); break; case BondStereo.UpOrDown: case BondStereo.UpOrDownInverted: writer.Write(" CFG=2"); break; case BondStereo.Down: case BondStereo.DownInverted: writer.Write(" CFG=3"); break; case BondStereo.None: break; default: // warn? break; } if (multicenterSgroups.TryGetValue(bond, out Sgroup sgroup)) { var atoms = new List <IAtom>(sgroup.Atoms); atoms.Remove(bond.Begin); atoms.Remove(bond.End); writer.Write(" ATTACH=Any ENDPTS=(").Write(atoms, idxs).Write(')'); } writer.Write('\n'); } writer.Write("END BOND\n"); }
/// <summary> /// Reads the bond atoms, order and stereo configuration. /// </summary> public void ReadBondBlock(IAtomContainer readData) { Trace.TraceInformation("Reading BOND block"); bool foundEND = false; while (!foundEND) { string command = ReadCommand(ReadLine()); if (string.Equals("END BOND", command, StringComparison.Ordinal)) { foundEND = true; } else { Debug.WriteLine($"Parsing bond from: {command}"); var tokenizer = Strings.Tokenize(command).GetEnumerator(); IBond bond = readData.Builder.NewBond(); // parse the index try { tokenizer.MoveNext(); string indexString = tokenizer.Current; bond.Id = indexString; } catch (Exception exception) { string error = "Error while parsing bond index"; Trace.TraceError(error); Debug.WriteLine(exception); throw new CDKException(error, exception); } // parse the order try { tokenizer.MoveNext(); string orderString = tokenizer.Current; int order = int.Parse(orderString, NumberFormatInfo.InvariantInfo); if (order >= 4) { Trace.TraceWarning("Query order types are not supported (yet). File a bug if you need it"); } else { bond.Order = BondManipulator.CreateBondOrder((double)order); } } catch (Exception exception) { string error = "Error while parsing bond index"; Trace.TraceError(error); Debug.WriteLine(exception); throw new CDKException(error, exception); } // parse index atom 1 try { tokenizer.MoveNext(); string indexAtom1String = tokenizer.Current; int indexAtom1 = int.Parse(indexAtom1String, NumberFormatInfo.InvariantInfo); IAtom atom1 = readData.Atoms[indexAtom1 - 1]; bond.Atoms.Add(atom1); // bond.Atoms[0] } catch (Exception exception) { string error = "Error while parsing index atom 1 in bond"; Trace.TraceError(error); Debug.WriteLine(exception); throw new CDKException(error, exception); } // parse index atom 2 try { tokenizer.MoveNext(); string indexAtom2String = tokenizer.Current; int indexAtom2 = int.Parse(indexAtom2String, NumberFormatInfo.InvariantInfo); IAtom atom2 = readData.Atoms[indexAtom2 - 1]; bond.Atoms.Add(atom2); // bond.Atoms[1] } catch (Exception exception) { string error = "Error while parsing index atom 2 in bond"; Trace.TraceError(error); Debug.WriteLine(exception); throw new CDKException(error, exception); } var endpts = new List <IAtom>(); string attach = null; // the rest are key=value fields if (command.IndexOf('=') != -1) { var options = ParseOptions(ExhaustStringTokenizer(tokenizer)); foreach (var key in options.Keys) { string value = options[key]; try { switch (key) { case "CFG": int configuration = int.Parse(value, NumberFormatInfo.InvariantInfo); if (configuration == 0) { bond.Stereo = BondStereo.None; } else if (configuration == 1) { bond.Stereo = BondStereo.Up; } else if (configuration == 2) { bond.Stereo = BondStereo.None; } else if (configuration == 3) { bond.Stereo = BondStereo.Down; } break; case "ENDPTS": string[] endptStr = value.Split(' '); // skip first value that is count for (int i = 1; i < endptStr.Length; i++) { endpts.Add(readData.Atoms[int.Parse(endptStr[i], NumberFormatInfo.InvariantInfo) - 1]); } break; case "ATTACH": attach = value; break; default: Trace.TraceWarning("Not parsing key: " + key); break; } } catch (Exception exception) { string error = "Error while parsing key/value " + key + "=" + value + ": " + exception.Message; Trace.TraceError(error); Debug.WriteLine(exception); throw new CDKException(error, exception); } } } // storing bond readData.Bonds.Add(bond); // storing positional variation if (string.Equals("ANY", attach, StringComparison.Ordinal)) { Sgroup sgroup = new Sgroup { Type = SgroupType.ExtMulticenter }; sgroup.Atoms.Add(bond.Begin); // could be other end? sgroup.Bonds.Add(bond); foreach (var endpt in endpts) { sgroup.Atoms.Add(endpt); } var sgroups = readData.GetCtabSgroups(); if (sgroups == null) { readData.SetCtabSgroups(sgroups = new List <Sgroup>(4)); } sgroups.Add(sgroup); } Debug.WriteLine($"Added bond: {bond}"); } } }
/// <summary> /// Find all enabled abbreviations in the provided molecule. They are not /// added to the existing Sgroups and may need filtering. /// </summary> /// <param name="mol">molecule</param> /// <returns>list of new abbreviation Sgroups</returns> public IList <Sgroup> Generate(IAtomContainer mol) { // mark which atoms have already been abbreviated or are // part of an existing Sgroup var usedAtoms = new HashSet <IAtom>(); var sgroups = mol.GetCtabSgroups(); if (sgroups != null) { foreach (var sgroup in sgroups) { foreach (var atom in sgroup.Atoms) { usedAtoms.Add(atom); } } } var newSgroups = new List <Sgroup>(); // disconnected abbreviations, salts, common reagents, large compounds if (!usedAtoms.Any()) { try { var copy = AtomContainerManipulator.CopyAndSuppressedHydrogens(mol); string cansmi = usmigen.Create(copy); if (disconnectedAbbreviations.TryGetValue(cansmi, out string label) && !disabled.Contains(label) && ContractToSingleLabel) { var sgroup = new Sgroup { Type = SgroupType.CtabAbbreviation, Subscript = label }; foreach (var atom in mol.Atoms) { sgroup.Atoms.Add(atom); } return(new[] { sgroup }); } else if (cansmi.Contains(".")) { var parts = ConnectivityChecker.PartitionIntoMolecules(mol); // leave one out Sgroup best = null; for (int i = 0; i < parts.Count; i++) { var a = parts[i]; var b = a.Builder.NewAtomContainer(); for (int j = 0; j < parts.Count; j++) { if (j != i) { b.Add(parts[j]); } } var sgroup1 = GetAbbr(a); var sgroup2 = GetAbbr(b); if (sgroup1 != null && sgroup2 != null && ContractToSingleLabel) { var combined = new Sgroup(); label = null; foreach (var atom in sgroup1.Atoms) { combined.Atoms.Add(atom); } foreach (var atom in sgroup2.Atoms) { combined.Atoms.Add(atom); } if (sgroup1.Subscript.Length > sgroup2.Subscript.Length) { combined.Subscript = sgroup1.Subscript + String_Interpunct + sgroup2.Subscript; } else { combined.Subscript = sgroup2.Subscript + String_Interpunct + sgroup1.Subscript; } combined.Type = SgroupType.CtabAbbreviation; return(new[] { combined }); } if (sgroup1 != null && (best == null || sgroup1.Atoms.Count > best.Atoms.Count)) { best = sgroup1; } if (sgroup2 != null && (best == null || sgroup2.Atoms.Count < best.Atoms.Count)) { best = sgroup2; } } if (best != null) { newSgroups.Add(best); foreach (var atom in best.Atoms) { usedAtoms.Add(atom); } } } } catch (CDKException) { } } var fragments = GenerateFragments(mol); var sgroupAdjs = new MultiDictionary <IAtom, Sgroup>(); foreach (var frag in fragments) { try { var smi = usmigen.Create(AtomContainerManipulator.CopyAndSuppressedHydrogens(frag)); if (!connectedAbbreviations.TryGetValue(smi, out string label) || disabled.Contains(label)) { continue; } bool overlap = false; // note: first atom is '*' var numAtoms = frag.Atoms.Count; var numBonds = frag.Bonds.Count; for (int i = 1; i < numAtoms; i++) { if (usedAtoms.Contains(frag.Atoms[i])) { overlap = true; break; } } // overlaps with previous assignment if (overlap) { continue; } // create new abbreviation Sgroup var sgroup = new Sgroup { Type = SgroupType.CtabAbbreviation, Subscript = label }; var attachBond = frag.Bonds[0].GetProperty <IBond>(PropertyName_CutBond); IAtom attachAtom = null; sgroup.Bonds.Add(attachBond); for (int i = 1; i < numAtoms; i++) { var atom = frag.Atoms[i]; usedAtoms.Add(atom); sgroup.Atoms.Add(atom); if (attachBond.Begin.Equals(atom)) { attachAtom = attachBond.End; } else if (attachBond.End.Equals(atom)) { attachAtom = attachBond.Begin; } } if (attachAtom != null) { sgroupAdjs.Add(attachAtom, sgroup); } newSgroups.Add(sgroup); } catch (CDKException) { // ignore } } if (!ContractOnHetero) { return(newSgroups); } // now collapse foreach (var attach in mol.Atoms) { if (usedAtoms.Contains(attach)) { continue; } // skip charged or isotopic labelled, C or R/*, H, He if ((attach.FormalCharge != null && attach.FormalCharge != 0) || attach.MassNumber != null || attach.AtomicNumber == 6 || attach.AtomicNumber < 2) { continue; } var hcount = attach.ImplicitHydrogenCount.Value; var xatoms = new HashSet <IAtom>(); var xbonds = new HashSet <IBond>(); var newbonds = new HashSet <IBond>(); xatoms.Add(attach); var nbrSymbols = new List <string>(); var todelete = new HashSet <Sgroup>(); foreach (var sgroup in sgroupAdjs[attach]) { if (ContainsChargeChar(sgroup.Subscript)) { continue; } if (sgroup.Bonds.Count != 1) { continue; } var xbond = sgroup.Bonds.First(); xbonds.Add(xbond); foreach (var a in sgroup.Atoms) { xatoms.Add(a); } if (attach.Symbol.Length == 1 && char.IsLower(sgroup.Subscript[0])) { if (ChemicalElement.OfSymbol(attach.Symbol + sgroup.Subscript[0]) != ChemicalElement.R) { goto continue_collapse; } } nbrSymbols.Add(sgroup.Subscript); todelete.Add(sgroup); } int numSGrpNbrs = nbrSymbols.Count; foreach (var bond in mol.GetConnectedBonds(attach)) { if (!xbonds.Contains(bond)) { var nbr = bond.GetOther(attach); // contract terminal bonds if (mol.GetConnectedBonds(nbr).Count() == 1) { if (nbr.MassNumber != null || (nbr.FormalCharge != null && nbr.FormalCharge != 0)) { newbonds.Add(bond); } else if (nbr.AtomicNumber == 1) { hcount++; xatoms.Add(nbr); } else if (nbr.AtomicNumber > 0) { nbrSymbols.Add(NewSymbol(nbr.AtomicNumber, nbr.ImplicitHydrogenCount.Value, false)); xatoms.Add(nbr); } } else { newbonds.Add(bond); } } } // reject if no symbols // reject if no bonds (<1), except if all symbols are identical... (HashSet.size==1) // reject if more that 2 bonds if (!nbrSymbols.Any() || newbonds.Count < 1 && (new HashSet <string>(nbrSymbols).Count != 1) || newbonds.Count > 2) { continue; } // create the symbol var sb = new StringBuilder(); sb.Append(NewSymbol(attach.AtomicNumber, hcount, newbonds.Count == 0)); string prev = null; int count = 0; nbrSymbols.Sort((o1, o2) => { int cmp = o1.Length.CompareTo(o2.Length); if (cmp != 0) { return(cmp); } return(o1.CompareTo(o2)); }); foreach (string nbrSymbol in nbrSymbols) { if (nbrSymbol.Equals(prev)) { count++; } else { bool useParen = count == 0 || CountUpper(prev) > 1 || (prev != null && nbrSymbol.StartsWith(prev)); AppendGroup(sb, prev, count, useParen); prev = nbrSymbol; count = 1; } } AppendGroup(sb, prev, count, false); // remove existing foreach (var e in todelete) { newSgroups.Remove(e); } // create new var newSgroup = new Sgroup { Type = SgroupType.CtabAbbreviation, Subscript = sb.ToString() }; foreach (var bond in newbonds) { newSgroup.Bonds.Add(bond); } foreach (var atom in xatoms) { newSgroup.Atoms.Add(atom); } newSgroups.Add(newSgroup); foreach (var a in xatoms) { usedAtoms.Add(a); } continue_collapse: ; } return(newSgroups); }
// Creates a CxSmilesState from a molecule with atom labels, repeat units, multi-center bonds etc private static CxSmilesState GetCxSmilesState(SmiFlavors flavour, IAtomContainer mol) { CxSmilesState state = new CxSmilesState { atomCoords = new List <double[]>(), coordFlag = false }; // set the atom labels, values, and coordinates, // and build the atom->idx map required by other parts var atomidx = new Dictionary <IAtom, int>(); for (int idx = 0; idx < mol.Atoms.Count; idx++) { IAtom atom = mol.Atoms[idx]; if (atom is IPseudoAtom) { if (state.atomLabels == null) { state.atomLabels = new SortedDictionary <int, string>(); } IPseudoAtom pseudo = (IPseudoAtom)atom; if (pseudo.AttachPointNum > 0) { state.atomLabels[idx] = "_AP" + pseudo.AttachPointNum; } else { if (!"*".Equals(pseudo.Label, StringComparison.Ordinal)) { state.atomLabels[idx] = pseudo.Label; } } } object comment = atom.GetProperty <object>(CDKPropertyName.Comment); if (comment != null) { if (state.atomValues == null) { state.atomValues = new SortedDictionary <int, string>(); } state.atomValues[idx] = comment.ToString(); } atomidx[atom] = idx; var p2 = atom.Point2D; var p3 = atom.Point3D; if (SmiFlavorTool.IsSet(flavour, SmiFlavors.Cx2dCoordinates) && p2 != null) { state.atomCoords.Add(new double[] { p2.Value.X, p2.Value.Y, 0 }); state.coordFlag = true; } else if (SmiFlavorTool.IsSet(flavour, SmiFlavors.Cx3dCoordinates) && p3 != null) { state.atomCoords.Add(new double[] { p3.Value.X, p3.Value.Y, p3.Value.Z }); state.coordFlag = true; } else if (SmiFlavorTool.IsSet(flavour, SmiFlavors.CxCoordinates)) { state.atomCoords.Add(new double[3]); } } if (!state.coordFlag) { state.atomCoords = null; } // radicals if (mol.SingleElectrons.Count > 0) { state.atomRads = new SortedDictionary <int, CxSmilesState.Radical>(); foreach (ISingleElectron radical in mol.SingleElectrons) { // 0->1, 1->2, 2->3 if (!state.atomRads.TryGetValue(EnsureNotNull(atomidx[radical.Atom]), out CxSmilesState.Radical val)) { val = CxSmilesState.Radical.Monovalent; } else if (val == CxSmilesState.Radical.Monovalent) { val = CxSmilesState.Radical.Divalent; } else if (val == CxSmilesState.Radical.Divalent) { val = CxSmilesState.Radical.Trivalent; } else if (val == CxSmilesState.Radical.Trivalent) { throw new ArgumentException("Invalid radical state, can not be more than trivalent"); } state.atomRads[atomidx[radical.Atom]] = val; } } var sgroups = mol.GetCtabSgroups(); if (sgroups != null) { state.sgroups = new List <CxSmilesState.PolymerSgroup>(); state.positionVar = new SortedDictionary <int, IList <int> >(); foreach (Sgroup sgroup in sgroups) { switch (sgroup.Type) { // polymer SRU case SgroupType.CtabStructureRepeatUnit: case SgroupType.CtabMonomer: case SgroupType.CtabMer: case SgroupType.CtabCopolymer: case SgroupType.CtabCrossLink: case SgroupType.CtabModified: case SgroupType.CtabMixture: case SgroupType.CtabFormulation: case SgroupType.CtabAnyPolymer: case SgroupType.CtabGeneric: case SgroupType.CtabComponent: case SgroupType.CtabGraft: string supscript = (string)sgroup.GetValue(SgroupKey.CtabConnectivity); state.sgroups.Add(new CxSmilesState.PolymerSgroup(GetSgroupPolymerKey(sgroup), ToAtomIdxs(sgroup.Atoms, atomidx), sgroup.Subscript, supscript)); break; case SgroupType.ExtMulticenter: IAtom beg = null; List <IAtom> ends = new List <IAtom>(); ISet <IBond> bonds = sgroup.Bonds; if (bonds.Count != 1) { throw new ArgumentException("Multicenter Sgroup in inconsistent state!"); } IBond bond = bonds.First(); foreach (IAtom atom in sgroup.Atoms) { if (bond.Contains(atom)) { if (beg != null) { throw new ArgumentException("Multicenter Sgroup in inconsistent state!"); } beg = atom; } else { ends.Add(atom); } } state.positionVar[EnsureNotNull(atomidx[beg])] = ToAtomIdxs(ends, atomidx); break; case SgroupType.CtabAbbreviation: case SgroupType.CtabMultipleGroup: // display shortcuts are not output break; case SgroupType.CtabData: // can be generated but currently ignored break; default: throw new NotSupportedException("Unsupported Sgroup Polymer"); } } } return(state); }