From 877f7538e4194ef54c0f512b23ebc33291bccea1 Mon Sep 17 00:00:00 2001 From: Nauman Ullah Khan <43783619+nnuk@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:01:42 +0000 Subject: [PATCH 1/4] Fix Polymers with explicit H atoms as leaving group --- .../INCHI_API/libinchi/src/inchi_dll_a2.c | 38 ++-- INCHI-1-SRC/INCHI_BASE/src/ichimake.c | 3 +- INCHI-1-SRC/INCHI_BASE/src/ichinorm.c | 6 +- INCHI-1-SRC/INCHI_BASE/src/ichinorm.h | 3 +- INCHI-1-SRC/INCHI_BASE/src/ichirvr1.c | 1 + INCHI-1-SRC/INCHI_BASE/src/inpdef.h | 2 + INCHI-1-SRC/INCHI_BASE/src/mol2atom.c | 14 +- INCHI-1-SRC/INCHI_BASE/src/runichi.c | 20 +- INCHI-1-SRC/INCHI_BASE/src/runichi2.c | 192 +++++++++++++----- INCHI-1-SRC/INCHI_BASE/src/runichi3.c | 10 +- INCHI-1-SRC/INCHI_BASE/src/strutil.c | 13 +- INCHI-1-SRC/INCHI_BASE/src/strutil.h | 2 + .../tests/test_unit/test_polymers.cpp | 60 ++++++ 13 files changed, 268 insertions(+), 96 deletions(-) create mode 100644 INCHI-1-TEST/tests/test_unit/test_polymers.cpp diff --git a/INCHI-1-SRC/INCHI_API/libinchi/src/inchi_dll_a2.c b/INCHI-1-SRC/INCHI_API/libinchi/src/inchi_dll_a2.c index 52355a15..64104bad 100644 --- a/INCHI-1-SRC/INCHI_API/libinchi/src/inchi_dll_a2.c +++ b/INCHI-1-SRC/INCHI_API/libinchi/src/inchi_dll_a2.c @@ -167,6 +167,7 @@ int Normalization_step( CANON_GLOBALS *pCG, inp_ATOM *inp_at, INP_ATOM_DATA *out_norm_data[2], int num_inp_at, + const unsigned char *keep_explicit_HDT, struct tagInchiTime *ulMaxTime, INCHI_MODE *pbTautFlags, INCHI_MODE *pbTautFlagsDone, @@ -340,19 +341,19 @@ int NormOneStructureINChI( CANON_GLOBALS *pCG, /* assign values to sd->num_components[] */ /* djb-rwth: MYREALLOC2( PINChI2, PINChI_Aux2, pINChI2[iINChI], pINChI_Aux2[iINChI], sd->num_components[iINChI], cur_prep_inp_data->num_components, k ) has been replaced and the whole block rewritten to address memory leaks and reading from freed memory locations */ - do - { - if( (sd->num_components[iINChI]) <= (cur_prep_inp_data->num_components) ) + do + { + if( (sd->num_components[iINChI]) <= (cur_prep_inp_data->num_components) ) { PINChI2* newPTR1 = (PINChI2 *)inchi_calloc( (long long)cur_prep_inp_data->num_components + 1, sizeof(PINChI2) ); PINChI_Aux2* newPTR2 = (PINChI_Aux2*)inchi_calloc( (long long)cur_prep_inp_data->num_components + 1, sizeof(PINChI_Aux2) ); if ( newPTR1 && newPTR2 ) - { + { if (pINChI2[iINChI] && sd->num_components[iINChI] > 0) memcpy( newPTR1, pINChI2[iINChI], (sd->num_components[iINChI]) * sizeof(PINChI2) ); if (pINChI_Aux2[iINChI] && sd->num_components[iINChI] > 0) memcpy( newPTR2, pINChI_Aux2[iINChI], (sd->num_components[iINChI]) * sizeof(PINChI_Aux2) ); - if (pINChI2[iINChI]) + if (pINChI2[iINChI]) inchi_free(pINChI2[iINChI]); if (pINChI_Aux2[iINChI]) inchi_free(pINChI_Aux2[iINChI]); @@ -360,17 +361,17 @@ int NormOneStructureINChI( CANON_GLOBALS *pCG, pINChI_Aux2[iINChI] = newPTR2; sd->num_components[iINChI] = cur_prep_inp_data->num_components; k = 0; - } - else - { - inchi_free(newPTR1); - inchi_free(newPTR2); + } + else + { + inchi_free(newPTR1); + inchi_free(newPTR2); k = 1; - } - } - else - { - k = 0; + } + } + else + { + k = 0; } } while (0); @@ -790,7 +791,7 @@ int NormOneComponentINChI( CANON_GLOBALS *pCG, { num_at = Normalization_step(pCG, ic, cur_INChI, cur_INChI_Aux, - inp_cur_data->at, inp_norm_data, inp_cur_data->num_at, + inp_cur_data->at, inp_norm_data, inp_cur_data->num_at, inp_cur_data->keep_explicit_HDT, pulTEnd, &bTautFlags, &bTautFlagsDone, cti); SetConnectedComponentNumber(inp_cur_data->at, inp_cur_data->num_at, i + 1); /* normalization alters structure component number */ @@ -1142,6 +1143,7 @@ int Normalization_step( CANON_GLOBALS *pCG, inp_ATOM *inp_at, INP_ATOM_DATA *out_norm_data[2], int num_inp_at, + const unsigned char *keep_explicit_HDT, struct tagInchiTime *ulMaxTime, INCHI_MODE *pbTautFlags, INCHI_MODE *pbTautFlagsDone, @@ -1182,7 +1184,7 @@ int Normalization_step( CANON_GLOBALS *pCG, memset( z->s, 0, sizeof( z->s ) ); /* djb-rwth: memset_s C11/Annex K variant? */ - if (pBCN) + if (pBCN) memset( pBCN, 0, sizeof( pBCN[0] ) ); /* djb-rwth: memset_s C11/Annex K variant? */ memset( t_group_info, 0, sizeof( *t_group_info ) ); /* djb-rwth: memset_s C11/Annex K variant? */ @@ -1251,7 +1253,7 @@ int Normalization_step( CANON_GLOBALS *pCG, } else { - z->num_at_tg = z->num_atoms = remove_terminal_HDT( num_inp_at, z->out_at, z->fix_termhchrg ); + z->num_at_tg = z->num_atoms = remove_terminal_HDT( num_inp_at, z->out_at, z->fix_termhchrg, keep_explicit_HDT); z->num_deleted_H = num_inp_at - z->num_atoms; t_group_info->tni.nNumRemovedExplicitH = z->num_deleted_H; diff --git a/INCHI-1-SRC/INCHI_BASE/src/ichimake.c b/INCHI-1-SRC/INCHI_BASE/src/ichimake.c index 0826571d..b175d329 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/ichimake.c +++ b/INCHI-1-SRC/INCHI_BASE/src/ichimake.c @@ -3712,6 +3712,7 @@ int Create_INChI(CANON_GLOBALS* pCG, inp_ATOM* inp_at, INP_ATOM_DATA* out_norm_data[2], int num_inp_at, + const unsigned char* keep_explicit_HDT, INCHI_MODE nUserMode, INCHI_MODE* pbTautFlags, INCHI_MODE* pbTautFlagsDone, @@ -3893,7 +3894,7 @@ int Create_INChI(CANON_GLOBALS* pCG, } else { - num_at_tg = num_atoms = remove_terminal_HDT(num_inp_at, out_at, bFixTermHChrg); + num_at_tg = num_atoms = remove_terminal_HDT(num_inp_at, out_at, bFixTermHChrg, keep_explicit_HDT); num_removed_H = num_inp_at - num_atoms; t_group_info->tni.nNumRemovedExplicitH = num_removed_H; add_DT_to_num_H(num_atoms, out_at); diff --git a/INCHI-1-SRC/INCHI_BASE/src/ichinorm.c b/INCHI-1-SRC/INCHI_BASE/src/ichinorm.c index d7557e44..7f00c38f 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/ichinorm.c +++ b/INCHI-1-SRC/INCHI_BASE/src/ichinorm.c @@ -5805,7 +5805,7 @@ int OAD_Edit_Underivatize( struct tagINCHI_CLOCK *ic, /*set_R2C_el_numbers( );*/ #ifndef UNDERIV_ADD_EXPLICIT_H - num_atoms = remove_terminal_HDT( orig_inp_data->num_inp_atoms, at, 1 ); + num_atoms = remove_terminal_HDT( orig_inp_data->num_inp_atoms, at, 1, NULL ); /*^^^^^ always accomodate accomodate FIX_TERM_H_CHRG_BUG - IPl, July 2008*/ num_removed_H = orig_inp_data->num_inp_atoms - num_atoms; orig_inp_data->num_inp_atoms = num_atoms; @@ -5833,7 +5833,7 @@ int OAD_Edit_Underivatize( struct tagINCHI_CLOCK *ic, } #ifdef UNDERIV_ADD_EXPLICIT_H - num_atoms = remove_terminal_HDT(inp_cur_data[i_component].num_at, inp_cur_data[i_component].at, 1); + num_atoms = remove_terminal_HDT(inp_cur_data[i_component].num_at, inp_cur_data[i_component].at, 1, NULL); inp_cur_data[i_component].num_removed_H = inp_cur_data[i_component].num_at - num_atoms; inp_cur_data[i_component].num_at = num_atoms; #endif @@ -7184,7 +7184,7 @@ int Ring2Chain( struct tagINCHI_CLOCK *ic, /*set_R2C_el_numbers( );*/ - num_atoms = remove_terminal_HDT( orig_inp_data->num_inp_atoms, at, 1 ); + num_atoms = remove_terminal_HDT( orig_inp_data->num_inp_atoms, at, 1, NULL ); /*^^^^^ always accomodate accomodate FIX_TERM_H_CHRG_BUG - IPl, July 2008*/ orig_inp_data->num_inp_atoms = num_atoms; diff --git a/INCHI-1-SRC/INCHI_BASE/src/ichinorm.h b/INCHI-1-SRC/INCHI_BASE/src/ichinorm.h index 848e5bf0..9932ae4f 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/ichinorm.h +++ b/INCHI-1-SRC/INCHI_BASE/src/ichinorm.h @@ -110,7 +110,8 @@ int MakeIsotopicHGroup( inp_ATOM *at, int remove_terminal_HDT( int num_atoms, inp_ATOM *at, - int bFixTermHChrg ); + int bFixTermHChrg, + const unsigned char* keep_explicit_HDT); int RemoveExcessiveImplicitH( int num_atoms, int num_removed_H, inp_ATOM *at ); diff --git a/INCHI-1-SRC/INCHI_BASE/src/ichirvr1.c b/INCHI-1-SRC/INCHI_BASE/src/ichirvr1.c index c3acf90a..7e8251d6 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/ichirvr1.c +++ b/INCHI-1-SRC/INCHI_BASE/src/ichirvr1.c @@ -5332,6 +5332,7 @@ int MakeOneInChIOutOfStrFromINChI( struct tagCANON_GLOBALS *pCG, inp_cur_data->at, inp_norm_data, inp_cur_data->num_at + inp_cur_data->num_removed_H, + NULL, ip->nMode, &bTautFlags, &bTautFlagsDone, diff --git a/INCHI-1-SRC/INCHI_BASE/src/inpdef.h b/INCHI-1-SRC/INCHI_BASE/src/inpdef.h index 2981528e..70775e02 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/inpdef.h +++ b/INCHI-1-SRC/INCHI_BASE/src/inpdef.h @@ -568,6 +568,7 @@ typedef struct tagInfoAtomData * * @param at Array of input atoms * @param at_fixed_bonds Array of input atoms with fixed bonds (tautomeric case, added or removed H) + * @param keep_explicit_HDT Component-local polymer crossing-bond endpoint mask used to prevent explicit H/D/T removal * @param num_at Number of atoms * @param num_removed_H Number of removed hydrogens * @param num_bonds Number of bonds @@ -588,6 +589,7 @@ typedef struct tagInputAtomData { inp_ATOM *at; inp_ATOM *at_fixed_bonds; /* tautomeric case, added or removed H */ + unsigned char *keep_explicit_HDT; int num_at; int num_removed_H; int num_bonds; diff --git a/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c b/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c index 487685fd..4bc71d43 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c +++ b/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c @@ -1183,6 +1183,14 @@ void FreeInpAtomData(INP_ATOM_DATA *inp_at_data) { FreeInpAtom(&inp_at_data->at); FreeInpAtom(&inp_at_data->at_fixed_bonds); + + /*@nnuk*/ + if (inp_at_data->keep_explicit_HDT) + { + inchi_free(inp_at_data->keep_explicit_HDT); + inp_at_data->keep_explicit_HDT = NULL; + } + memset(inp_at_data, 0, sizeof(*inp_at_data)); /* djb-rwth: memset_s C11/Annex K variant? */ } @@ -1557,12 +1565,16 @@ int SetExtOrigAtDataByMolfileExtInput(MOL_FMT_DATA *mfdata, ia2 = mfdata->ctab.bonds[ib - 1].atnum2; unitk->blist[2 * m] = ia1; unitk->blist[2 * m + 1] = ia2; + + /** + *@nnuk: GHI#252 Adressed + * if (!strcmp(mfdata->ctab.atoms[ia1 - 1].symbol, "H") || !strcmp(mfdata->ctab.atoms[ia2 - 1].symbol, "H")) { TREAT_ERR(err, 9002, "Hydrogen as polymer end group is not supported"); goto exit_function; - } + }*/ } } else diff --git a/INCHI-1-SRC/INCHI_BASE/src/runichi.c b/INCHI-1-SRC/INCHI_BASE/src/runichi.c index ea1c12f7..e61c9a7c 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/runichi.c +++ b/INCHI-1-SRC/INCHI_BASE/src/runichi.c @@ -327,18 +327,6 @@ int ProcessOneStructure(INCHI_CLOCK* ic, goto exit_function; } - /*// Debugging: Print atom structure after disconnections - for (int i = 0; i < orig_inp_data->num_inp_atoms; i++) - { - printf("Atom %d: Element %s, Valence: %d, Charge: %d, Num of Implicit H: %d, Neighbors: ", - i + 1, orig_inp_data->at[i].elname, orig_inp_data->at[i].valence, orig_inp_data->at[i].charge, orig_inp_data->at[i].num_H); - for (int j = 0; j < orig_inp_data->at[i].valence; j++) - { - printf("%d ", orig_inp_data->at[i].neighbor[j] + 1); - } - printf("\n"); - }*/ - /* Preserve SDF output in Molecular Inorganics mode if requested */ ret1 = OrigAtData_SaveMolfile(orig_inp_data, sd, ip, num_inp, out_file); if (ret1) @@ -346,7 +334,6 @@ int ProcessOneStructure(INCHI_CLOCK* ic, goto exit_function; } - /*printf("Molecular inorganics preprocessing completed successfully.\n");*/ nRet1 = CreateOneStructureINChI(pCG, ic, sd, ip, szTitle, pINChI, pINChI_Aux, INCHI_BAS, inp_file, log_file, out_file, prb_file, @@ -361,7 +348,6 @@ int ProcessOneStructure(INCHI_CLOCK* ic, (sd->bTautFlagsDone[INCHI_BAS] & TG_FLAG_DISCONNECT_COORD_DONE) && (ip->bTautFlags & TG_FLAG_RECONNECT_COORD) ) { - /*printf("Generating reconnected InChI due to retained bonds.\n");*/ nRet1 = CreateOneStructureINChI(pCG, ic, sd, ip, szTitle, pINChI, pINChI_Aux, INCHI_REC, @@ -391,10 +377,9 @@ int ProcessOneStructure(INCHI_CLOCK* ic, /* * InChI is already generated via the Molecular Inorganics-specific path. * Skip the standard structure-generation path to prevent duplicate - * CreateOneStructureINChI() calls, which previously caused redundant - * allocations and AddressSanitizer-reported memory leaks. + * CreateOneStructureINChI() calls and redundant allocations. * - * pOrigStruct was populated above from the native input, so the shared + * pOrigStruct and OrigStruct were populated above from the native input, so the shared * cleanup path (SortAndPrintINChI + OrigStruct_Free) emits the AuxInfo * reversibility layers (/rA, /rB, /rC), including atom coordinates, and * releases it. @@ -1978,6 +1963,7 @@ int CreateOneComponentINChI(CANON_GLOBALS* pCG, cur_INChI, cur_INChI_Aux, orig_inp_data/* not used */, inp_cur_data->at, inp_norm_data, inp_cur_data->num_at, + inp_cur_data->keep_explicit_HDT, ip->nMode, &bTautFlags, &bTautFlagsDone, pulTEnd, NULL, sd->pStrErrStruct); diff --git a/INCHI-1-SRC/INCHI_BASE/src/runichi2.c b/INCHI-1-SRC/INCHI_BASE/src/runichi2.c index a3649a4d..068c03f2 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/runichi2.c +++ b/INCHI-1-SRC/INCHI_BASE/src/runichi2.c @@ -166,7 +166,7 @@ int GetOneStructure( INCHI_CLOCK *ic, if (!struct_fptrs->fptr || struct_fptrs->len_fptr <= struct_fptrs->cur_fptr + 1) { - INCHI_FPTR *new_fptr = (INCHI_FPTR *) + INCHI_FPTR *new_fptr = (INCHI_FPTR *) inchi_calloc( (long long)struct_fptrs->len_fptr + ADD_LEN_STRUCT_FPTRS, sizeof( new_fptr[0] ) ); /* djb-rwth: cast operator added */ if (new_fptr) @@ -338,6 +338,90 @@ int GetOneStructure( INCHI_CLOCK *ic, return nRet; } +/** @nnuk +**************************************************************************** + Build a component-local mask of polymer crossing-bond endpoints. + + The polymer blist uses whole-structure, 1-based atom numbering, whereas + inp_cur_data contains only atoms from the selected connected component. + ExtractConnectedComponent() preserves the original atom order within the + component, allowing the corresponding local atom index to be reconstructed. + + The mask is later consulted by remove_terminal_HDT() to preserve explicit + H/D/T atoms that participate in polymer crossing bonds. + + @param INP_ATOM_DATA chemical structure information from INP_ATOM_DATA struct + @param ORIG_ATOM_DATA chemical structure information from ORIG_ATOM_DATA struct + @param INPUT_PARMS input parameters + @param component_number number of component in the chemical structure + + @return Return 0 on success or when no mask is required; return -1 on allocation + failure. +****************************************************************************/ +static int BuildPolymerCrossingBondEndpointMask(INP_ATOM_DATA *inp_cur_data, + const ORIG_ATOM_DATA *orig_inp_data, + const INPUT_PARMS *ip, + int component_number) +{ + int orig_idx; + int local_idx = 0; + int unit_idx; + int endpoint_idx; + + if (inp_cur_data->num_at <= 0 || ip->bPolymers == POLYMERS_NO || + !orig_inp_data->valid_polymer || !orig_inp_data->polymer || + orig_inp_data->polymer->n <= 0) + { + return 0; + } + + inp_cur_data->keep_explicit_HDT = (unsigned char *)inchi_calloc((long long)inp_cur_data->num_at, sizeof(inp_cur_data->keep_explicit_HDT[0])); + + if (!inp_cur_data->keep_explicit_HDT) + { + return -1; + } + + for (orig_idx = 0; orig_idx < orig_inp_data->num_inp_atoms; orig_idx++) + { + if (orig_inp_data->at[orig_idx].component != component_number) + { + continue; + } + + for (unit_idx = 0; unit_idx < orig_inp_data->polymer->n; unit_idx++) + { + const OAD_PolymerUnit *u = orig_inp_data->polymer->units[unit_idx]; + + if (!u || !u->blist) + { + continue; + } + + /* + * Each crossing bond contributes two atom numbers to blist. + * blist uses whole-structure, 1-based atom numbering. + */ + for (endpoint_idx = 0; endpoint_idx < 2 * u->nb; endpoint_idx++) + { + if (u->blist[endpoint_idx] == orig_idx + 1) + { + inp_cur_data->keep_explicit_HDT[local_idx] = 1; + break; + } + } + + if (inp_cur_data->keep_explicit_HDT[local_idx]) + { + break; + } + } + + local_idx++; + } + + return 0; +} /**************************************************************************** Extract one connected component from the input structure @@ -360,6 +444,18 @@ int GetOneComponent( INCHI_CLOCK *ic, inp_cur_data->num_at = ExtractConnectedComponent( orig_inp_data->at, orig_inp_data->num_inp_atoms, i + 1, inp_cur_data->at ); + if (BuildPolymerCrossingBondEndpointMask(inp_cur_data, orig_inp_data, ip, i + 1) < 0) + { + AddErrorMessage(sd->pStrErrStruct, "Out of memory while processing polymer data"); + + sd->nErrorCode = CT_OUT_OF_RAM; + sd->nErrorType = _IS_FATAL; + + sd->ulStructTime += InchiTimeElapsed(ic, &ulTStart); + + return sd->nErrorType; + } + sd->ulStructTime += InchiTimeElapsed( ic, &ulTStart ); @@ -1563,7 +1659,7 @@ int POSEContext_Init(POSEContext *context, ret = _IS_ERROR; goto exit_function; } - context->pINChI_Aux2[0] = context->pINChI_Aux2[1] = NULL; + context->pINChI_Aux2[0] = context->pINChI_Aux2[1] = NULL; if (pINChI_Aux2 && (pINChI_Aux2[0] || pINChI_Aux2[1])) /* djb-rwth: condition corrected */ { ret = _IS_ERROR; @@ -1806,8 +1902,8 @@ void OAD_StructureEdits_Clear(OAD_StructureEdits *ed) /****************************************************************************/ void OAD_StructureEdits_DebugPrint(OAD_StructureEdits *ed) { - ITRACE_("\n*****************************\nOAD_StructureEdits @ %-p\n*****************************", ed); - ITRACE_("\nDel_side_chains :\t%-d\n", ed->del_side_chains); + ITRACE_("\n*****************************\nOAD_StructureEdits @ %-p\n*****************************", ed); + ITRACE_("\nDel_side_chains :\t%-d\n", ed->del_side_chains); ITRACE_("Del_atom:\t%-s", ed->del_atom->used ? "" : "(empty)\n"); IntArray_DebugPrint(ed->del_atom); ITRACE_("Del_bond:\t%-s", ed->del_bond->used ? "" : "(empty)\n"); @@ -1818,7 +1914,7 @@ void OAD_StructureEdits_DebugPrint(OAD_StructureEdits *ed) IntArray_DebugPrint(ed->mod_bond); ITRACE_("Mod_coord:\t%-s", ed->mod_coord->used ? "" : "(empty)\n"); IntArray_DebugPrint(ed->mod_coord); - + } @@ -1836,7 +1932,7 @@ xc_opp[MAX_ATOMS]; /* Extended (stereo-aware) atom classes. (k + neclasses) for '-' parity (k + 2*neclasses) for '+' parity */ int OAD_Polymer_PrepareFoldCRUEdits( ORIG_ATOM_DATA *orig_at_data, - char *sinchi_noedits, + char *sinchi_noedits, char *saux_noedits, char *sinchi, char *saux, @@ -1850,7 +1946,7 @@ int OAD_Polymer_PrepareFoldCRUEdits( ORIG_ATOM_DATA *orig_at_data, int nat = orig_at_data->num_inp_atoms; int neclasses = 0; /* No of constitutional equivalence classses for the atoms */ int nxclasses = 0; /* No of extended (stereo-aware) atom classses == 3*neclasses */ - + int *all_bkb_orig = NULL, n_all_bkb_orig = 0; OAD_Polymer *p = orig_at_data->polymer; int nu = orig_at_data->polymer->n; @@ -2017,7 +2113,7 @@ DiylFrag* DiylFrag_New(int na, int end1, int end2, char *s) goto exit_function; } - pfrag->na = na; + pfrag->na = na; pfrag->end1 = end1; pfrag->end2 = end2; pfrag->alist = NULL; @@ -2066,18 +2162,18 @@ void DiylFrag_Free(DiylFrag *pfrag) return; } /***************************************************************************/ -void DiylFrag_MakeSignature(DiylFrag *pfrag, +void DiylFrag_MakeSignature(DiylFrag *pfrag, int nxc, /* n xclasses (molecule-wide) */ int *xc, /* xclasses (molecule-wide) */ int *cnt ) /* temp storage: counts of xclasses */ { int i, k, nxc_frag; /* djb-rwth: ignoring LLVM warning: variable used to store function return value */ - + inchi_strbuf_printf(&pfrag->sig, "%-d,%-d,%-d{", pfrag->na, xc[pfrag->end1], xc[pfrag->end2]); for (i = 0; i < pfrag->na; i++) { pfrag->xclist[i] = xc[pfrag->alist[i]]; - } + } nxc_frag = count_colors_in_sequence(pfrag->xclist, pfrag->na, nxc+1, cnt); /* djb-rwth: ignoring LLVM warning: variable used to store function return value */ for (k = 0; k < nxc; k++) { @@ -2124,7 +2220,7 @@ void DiylFrag_DebugTrace(DiylFrag *pfrag) { return; } - + ITRACE_("DiylFrag @ %-p ", pfrag); na = pfrag->na; ITRACE_("\n\t%-d atoms. List of atoms and their xclasses : { ", na); @@ -2135,7 +2231,7 @@ void DiylFrag_DebugTrace(DiylFrag *pfrag) ITRACE_(" %-d(%-d) }\n", pfrag->alist[na - 1], pfrag->xclist[na - 1]); ITRACE_("\tend1 = %-d, end2 = %-d, nb = %-d\n", pfrag->end1, pfrag->end2, pfrag->nb); - + ITRACE_("\tSignature = '%-s'\n", pfrag->sig.pStr); return; @@ -2147,20 +2243,20 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, int iunit, int n_all_bkb, int *all_bkb, - int nxclasses, + int nxclasses, int *xc, OAD_StructureEdits *ed) { int ret = _IS_OKAY; int err, i, j, k, m, fail, a1, a2; - int n_cuts = 0, n_frags = 0; + int n_cuts = 0, n_frags = 0; int n_frags_in_repeating_subunit = 0; int n_fold, n_frag_classes = 0; int subunit_last_atom, next_subunit_first_atom = 0; - int *cut = NULL; /* [ bkbond1at1, bkbond1at2, bkbond2at1,bkbond2at2, ... ] + int *cut = NULL; /* [ bkbond1at1, bkbond1at2, bkbond2at1,bkbond2at2, ... ] these are (atoms of) backbone bonds which are non-cyclic and non-multiple ('breakable') */ DiylFrag **frag=NULL; /* frag is divalent fragment surrounded by 'cut' bonds, so it may be a repeating CRU sub-unit */ - int *frag_class=NULL; /* fragments are classified, by their signatures, to produce unique labelling; + int *frag_class=NULL; /* fragments are classified, by their signatures, to produce unique labelling; if the two fragments have the same class, they have the same signature and whence are equivalent */ int *frag_xc_counts = NULL; /* counts of xclass atoms in CRU, order of class numbers */ char pStrErr[STR_ERR_LEN]; @@ -2205,8 +2301,8 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, } /* Make 'cut' list from the bonds which are both in all_bkb and u->bkb - (all_bkb eliminates bonds with order >1 and cyclic ones, - but may contain artificial cyclizing bond) + (all_bkb eliminates bonds with order >1 and cyclic ones, + but may contain artificial cyclizing bond) */ for (i = 0; i nbkbonds; i++) { @@ -2248,10 +2344,10 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, { /* Create fragment */ int forbidden[4], novel=1; - DiylFrag *pfrag = NULL; + DiylFrag *pfrag = NULL; /* Calculate and store signature of the fragment */ - /* + /* end_atom1...cut[i-1])---frag[i]---cut[i]---...end_atom2 */ if (i == 0) @@ -2292,7 +2388,7 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, goto exit_function; } - DiylFrag_MakeSignature(pfrag, nxclasses, xc, frag_xc_counts); + DiylFrag_MakeSignature(pfrag, nxclasses, xc, frag_xc_counts); novel = 1; for (j = 0; j < i; j++) @@ -2318,7 +2414,7 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, /* All classes are distinct ==> no repeats, folding is impossible, skip the CRU */ goto exit_function; } - + n_frags_in_repeating_subunit = len_repeating_subsequence(frag_class, NULL, n_frags); if (0 == n_frags_in_repeating_subunit) { @@ -2326,7 +2422,7 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, goto exit_function; } n_fold = n_frags / n_frags_in_repeating_subunit; - if (1==n_fold || (0!=n_frags%n_frags_in_repeating_subunit) ) + if (1==n_fold || (0!=n_frags%n_frags_in_repeating_subunit) ) { /* valid repeating pattern not found */ goto exit_function; @@ -2337,7 +2433,7 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, ITRACE_("\n* Found %-d times foldable unit of %-d fragments\n* First repeating sub-unit formed by %-d-fragment backbone : ", n_fold, n_frags, n_frags_in_repeating_subunit); - + for (k = 0; k < n_frags_in_repeating_subunit && n_frags_in_repeating_subunit < n_frags && frag[k]; k++) /* djb-rwth: fixing a NULL pointer dereference and buffer overflow */ { if (frag[k]->end1 == frag[k]->end2) @@ -2364,18 +2460,18 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, } } ITRACE_("\n"); - - /* Folding is possible, prepare the edits - Keep the least in-CRU repeating subunit + + /* Folding is possible, prepare the edits + Keep the least in-CRU repeating subunit { frag[0] ... frag[n_frags_in_repeating_subunit-1] } - and remove - { frag[n_frags_in_repeating_subunit]...frag[n_frags-1] } and all side chain attached to that - - NB: which bond is modified and which is broke is important for applying these edits further! + and remove + { frag[n_frags_in_repeating_subunit]...frag[n_frags-1] } and all side chain attached to that + + NB: which bond is modified and which is broke is important for applying these edits further! */ - /* Break bond from the subunit to the next fragment and replace an original - bond to "right" cap with bond from the subunit "right" atom + /* Break bond from the subunit to the next fragment and replace an original + bond to "right" cap with bond from the subunit "right" atom */ /*djb-rwth: the whole block had to be rewritten to fix NULL pointer dereference */ @@ -2399,7 +2495,7 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, goto exit_function; } } - + /* Now collect all backbone atoms to be deleted (we will then delete the associated side chains also, but no need to reveal them at the moment) */ @@ -2418,8 +2514,8 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, } } } - /* Care on atom coordinates: as bond to cap2 changes, - we use coordinates of next_subunit_first_atom for cap2 + /* Care on atom coordinates: as bond to cap2 changes, + we use coordinates of next_subunit_first_atom for cap2 */ fail = 0; fail += IntArray_Append(ed->mod_coord, next_subunit_first_atom); @@ -2457,14 +2553,14 @@ int analyze_CRU_folding(ORIG_ATOM_DATA *orig_at_data, } /*************************************************************************** - Return number of colors ncol<=maxcol in the sequence of n colored entries + Return number of colors ncol<=maxcol in the sequence of n colored entries and counts of individiual colors ***************************************************************************/ int count_colors_in_sequence( int *color, int n, int maxcol, int *counts) { int i, ncol=0; memset(counts, 0, maxcol * sizeof(int)); /* djb-rwth: memset_s C11/Annex K variant? */ - for (i = 0; ipolymer; int nu = orig_at_data->polymer->n; int nat = orig_at_data->num_inp_atoms; - + /* Extract cano_nums-->orig_nums mapping for InChI AuxInfo Main Layer */ orig = (int *)inchi_calloc((long long)nat + 1, sizeof(int)); /* djb-rwth: cast operator added */ if (!orig) @@ -2554,7 +2650,7 @@ int OAD_Polymer_PrepareFrameShiftEdits( ORIG_ATOM_DATA *orig_at_data, ret = _IS_ERROR; goto exit_function; } - + /* Parse InChI and extract, for each 'bistar' CRU, the senior bkbond (to frame-shift brackets to its ends) */ frame_shift_info = (int *)inchi_calloc(3 * ((long long)nu + 1), sizeof(int)); /* djb-rwth: cast operator added */ @@ -2580,12 +2676,12 @@ int OAD_Polymer_PrepareFrameShiftEdits( ORIG_ATOM_DATA *orig_at_data, end1 = frame_shift_info[3 * j + 1]; end2 = frame_shift_info[3 * j + 2]; - + /* Find the unit to edit (== that unit whose alist contains the new end atoms) */ for (k = 0; k < p->n; k++) { int ak, present=0; - + if (NULL == p->units[k]->blist || p->units[k]->nb < 2 ) { /* No crossing bonds in the unit */ @@ -2653,7 +2749,7 @@ int OAD_Polymer_PrepareFrameShiftEdits( ORIG_ATOM_DATA *orig_at_data, } /* If applicable, collect bonds to modify */ - + /* Check if atoms involved in modifications are stereocenters (needs additional care) */ ModSCenter_Init(&scinfo[0], orig_at_data->at, old_end1 - 1); ModSCenter_Init(&scinfo[1], orig_at_data->at, old_end2 - 1); diff --git a/INCHI-1-SRC/INCHI_BASE/src/runichi3.c b/INCHI-1-SRC/INCHI_BASE/src/runichi3.c index fec395e5..b44aece0 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/runichi3.c +++ b/INCHI-1-SRC/INCHI_BASE/src/runichi3.c @@ -1571,7 +1571,7 @@ int OAD_ValidatePolymerAndPseudoElementData(ORIG_ATOM_DATA* orig_at_data, { /* Check if unit data makes sense */ u = pd->units[i]; - if ( u->nb != 0 && u->nb != 2 ) + if ( u->nb != 0 && u->nb != 2) { TREAT_ERR(err, 9003, "Number of crossing bonds in polymer unit is not 0 or 2"); goto exit_function; @@ -1835,6 +1835,9 @@ int OAD_ValidatePolymerAndPseudoElementData(ORIG_ATOM_DATA* orig_at_data, { /* Check that there are no H end groups */ a1 = u->blist[2 * k]; a2 = u->blist[2 * k + 1]; + + /** + *@nnuk : GHI # 252 Addressed if ( !strcmp(orig_at_data->at[a1 - 1].elname, "H") || !strcmp(orig_at_data->at[a1 - 1].elname, "D") || !strcmp(orig_at_data->at[a1 - 1].elname, "T") ) @@ -1848,7 +1851,8 @@ int OAD_ValidatePolymerAndPseudoElementData(ORIG_ATOM_DATA* orig_at_data, { TREAT_ERR(err, 9031, "H as polymer end group is not supported"); goto exit_function; - } + }*/ + /* Ensure that caps of polymer unit lie outside it */ a1_is_not_in_alist = a1_is_star_atom = 0; a2_is_not_in_alist = a2_is_star_atom = 0; @@ -4527,4 +4531,4 @@ int Inp_Atom_GetBondType(inp_ATOM* at, int iatom1, int iatom2) } return -1; -} \ No newline at end of file +} diff --git a/INCHI-1-SRC/INCHI_BASE/src/strutil.c b/INCHI-1-SRC/INCHI_BASE/src/strutil.c index 0bbc91a9..a0d1f55b 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/strutil.c +++ b/INCHI-1-SRC/INCHI_BASE/src/strutil.c @@ -3742,7 +3742,7 @@ static int is_only_HDT_neighbors(const inp_ATOM* at, int num_atoms, int metal_id /**************************************************************************** Return value: new number of atoms > 0 or -1=out of RAM ****************************************************************************/ -int remove_terminal_HDT(int num_atoms, inp_ATOM *at, int bFixTermHChrg) +int remove_terminal_HDT(int num_atoms, inp_ATOM *at, int bFixTermHChrg, const unsigned char* keep_explicit_HDT) { AT_NUMB *new_ord; inp_ATOM *new_at; @@ -3792,7 +3792,11 @@ int remove_terminal_HDT(int num_atoms, inp_ATOM *at, int bFixTermHChrg) * Note: This must be consistent with MOL_FMT_to_atom() * treatment of isotopic Hn aliases. */ - if (2 == num_H && 2 == num_atoms && !NUMH(at, 0) && !NUMH(at, 1)) + if (2 == num_H && 2 == num_atoms && + !NUMH(at, 0) && !NUMH(at, 1) && + (!keep_explicit_HDT || + (!keep_explicit_HDT[0] && + !keep_explicit_HDT[1]))) { if (at[0].iso_atw_diff >= at[1].iso_atw_diff) @@ -3823,10 +3827,11 @@ int remove_terminal_HDT(int num_atoms, inp_ATOM *at, int bFixTermHChrg) : kMax; n = (int)at[i].neighbor[0]; if (k < kMax && at[i].valence == 1 && at[i].chem_bonds_valence == 1 && + (!keep_explicit_HDT || !keep_explicit_HDT[i]) && /* the order of comparison is important */ ((n > i) /* at[n] has not been encountered yet*/ || - (int)new_ord[n] < num_atoms - num_hydrogens) /* at[n] might have been encountered; it has not been moved */ && - (!is_el_a_metal(at[n].el_number) || is_only_HDT_neighbors(at, num_atoms, n))/*@nnuk*/ ) + (int)new_ord[n] < num_atoms - num_hydrogens) /* at[n] might have been encountered; it has not been moved */ && + (!is_el_a_metal(at[n].el_number) || is_only_HDT_neighbors(at, num_atoms, n))/*@nnuk*/) { /* found an explicit terminal hydrogen */ num_hydrogens++; diff --git a/INCHI-1-SRC/INCHI_BASE/src/strutil.h b/INCHI-1-SRC/INCHI_BASE/src/strutil.h index a08d434c..1f561240 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/strutil.h +++ b/INCHI-1-SRC/INCHI_BASE/src/strutil.h @@ -254,6 +254,7 @@ extern "C" * @param inp_at Pointer to atom array * @param inp_norm_data Pointer to normalized atom data * @param num_inp_at Number of atoms + * @param keep_explicit_HDT keep HDT atoms in case of polymers with leaving group * @param nUserMode User mode * @param pbTautFlags Tautomer flags * @param pbTautFlagsDone Tautomer flags completed @@ -271,6 +272,7 @@ extern "C" inp_ATOM *inp_at, INP_ATOM_DATA *inp_norm_data[2], int num_inp_at, + const unsigned char* keep_explicit_HDT, INCHI_MODE nUserMode, INCHI_MODE *pbTautFlags, INCHI_MODE *pbTautFlagsDone, diff --git a/INCHI-1-TEST/tests/test_unit/test_polymers.cpp b/INCHI-1-TEST/tests/test_unit/test_polymers.cpp new file mode 100644 index 00000000..79862060 --- /dev/null +++ b/INCHI-1-TEST/tests/test_unit/test_polymers.cpp @@ -0,0 +1,60 @@ +#include + +extern "C" +{ +#include "../../../INCHI-1-SRC/INCHI_BASE/src/inchi_api.h" +#include "../../../INCHI-1-SRC/INCHI_BASE/src/mode.h" +} + +/* +* (NaumanUllahKhan :: @nnuk) +* helper function to run the molfiles below as unit tests. +*/ +static void ExpectedPolymersInChI(const char* molblock, + const char* expected_inchi, + int expected_retcode) +{ + char options[] = "-Polymers -NPZz"; + + inchi_Output output; + inchi_Output* poutput = &output; + memset(poutput, 0, sizeof(*poutput)); + + int ret = MakeINCHIFromMolfileText(molblock, options, poutput); + + EXPECT_EQ(ret, expected_retcode); + ASSERT_NE(poutput->szInChI, nullptr); + EXPECT_STREQ(poutput->szInChI, expected_inchi); + + FreeINCHI(poutput); +} + +/* GitHub issue # 252 */ +TEST(test_polymers, test_GHI_252) +{ + const char* molblock = R"( + ACCLDraw08102613492D + + 5 4 0 0 0 0 0 0 0 0999 V2000 + 5.9688 -4.0938 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 6.9916 -3.5032 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.0148 -4.0939 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 9.0380 -3.5032 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 10.0611 -4.0939 0.0000 H 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 + 3 4 1 0 0 0 0 + 4 5 1 0 0 0 0 +M STY 1 1 SRU +M SLB 1 1 1 +M SCN 1 1 HT +M SAL 1 3 3 2 4 +M SBL 1 2 1 4 +M SDI 1 4 6.4802 -4.3892 6.4802 -3.2078 +M SDI 1 4 9.5495 -3.2079 9.5495 -4.3892 +M SMT 1 n +M END +)"; + + ExpectedPolymersInChI(molblock, "InChI=1B/C2H6O2/c3-1-2-4-5/h3H,1-2H2/z101-1-2,4(5-4,3-1)", 0); +} From af7517692518ce1de88862a97c0c8f0c93acd9c4 Mon Sep 17 00:00:00 2001 From: Nauman Ullah Khan <43783619+nnuk@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:25:03 +0000 Subject: [PATCH 2/4] Update changelog --- INCHI-1-DOC/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/INCHI-1-DOC/CHANGELOG.md b/INCHI-1-DOC/CHANGELOG.md index 9bae7260..c45974a0 100644 --- a/INCHI-1-DOC/CHANGELOG.md +++ b/INCHI-1-DOC/CHANGELOG.md @@ -7,6 +7,10 @@ - Molecular Inorganics preprocessing now follows the revised bond-disconnection decision tree, preserving stereo-indicated metal bonds, linked-metal structures, and qualifying metal-containing chelate ring systems while applying the Group 1/2 and terminal-metal exceptions. - The generated InChI for qualifying chelate complexes may intentionally change from a disconnected representation to a connected representation. In particular, the Ti bis-flavonoid regression case is now represented as connected `C30H18O14Ti` instead of disconnected `2C15H10O7.Ti` components. +### Fixed + +- GHI #252: Explicit H/D/T atoms used as polymer crossing-bond end groups are now preserved during terminal-H processing when the `Polymers` option is enabled. This allows valid SRU/CRU structures with explicit hydrogen leaving groups to be processed instead of being rejected as unsupported. + ## v1.07.5 2026-02-17 ### Changed From 69b19812e20441d7bad9f28cb04dfa62d3bf8f77 Mon Sep 17 00:00:00 2001 From: Nauman Ullah Khan <43783619+nnuk@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:48:37 +0000 Subject: [PATCH 3/4] Update minor logic and documentation --- INCHI-1-DOC/CHANGELOG.md | 2 +- INCHI-1-SRC/INCHI_BASE/src/mol2atom.c | 10 +---- INCHI-1-SRC/INCHI_BASE/src/runichi2.c | 38 +++++++++++++++++-- INCHI-1-SRC/INCHI_BASE/src/runichi3.c | 25 ++++-------- .../tests/test_unit/test_polymers.cpp | 8 +++- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/INCHI-1-DOC/CHANGELOG.md b/INCHI-1-DOC/CHANGELOG.md index c45974a0..bb05cd19 100644 --- a/INCHI-1-DOC/CHANGELOG.md +++ b/INCHI-1-DOC/CHANGELOG.md @@ -9,7 +9,7 @@ ### Fixed -- GHI #252: Explicit H/D/T atoms used as polymer crossing-bond end groups are now preserved during terminal-H processing when the `Polymers` option is enabled. This allows valid SRU/CRU structures with explicit hydrogen leaving groups to be processed instead of being rejected as unsupported. +- GHI #252: Explicit H/D/T atoms used as polymer crossing-bond end groups are now preserved during terminal-H processing when the `Polymers` option is enabled. This allows valid SRU/CRU structures with explicit hydrogen leaving groups to be processed instead of being rejected as unsupported. The retained H/D/T atom is represented explicitly as a numbered atom in the `/c` layer rather than being converted to an implicit hydrogen. ## v1.07.5 2026-02-17 diff --git a/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c b/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c index 4bc71d43..025c2165 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c +++ b/INCHI-1-SRC/INCHI_BASE/src/mol2atom.c @@ -1567,14 +1567,8 @@ int SetExtOrigAtDataByMolfileExtInput(MOL_FMT_DATA *mfdata, unitk->blist[2 * m + 1] = ia2; /** - *@nnuk: GHI#252 Adressed - * - if (!strcmp(mfdata->ctab.atoms[ia1 - 1].symbol, "H") || - !strcmp(mfdata->ctab.atoms[ia2 - 1].symbol, "H")) - { - TREAT_ERR(err, 9002, "Hydrogen as polymer end group is not supported"); - goto exit_function; - }*/ + *@nnuk: GHI#252 addressed and redundant logic removed + */ } } else diff --git a/INCHI-1-SRC/INCHI_BASE/src/runichi2.c b/INCHI-1-SRC/INCHI_BASE/src/runichi2.c index 068c03f2..f783790d 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/runichi2.c +++ b/INCHI-1-SRC/INCHI_BASE/src/runichi2.c @@ -350,9 +350,9 @@ int GetOneStructure( INCHI_CLOCK *ic, The mask is later consulted by remove_terminal_HDT() to preserve explicit H/D/T atoms that participate in polymer crossing bonds. - @param INP_ATOM_DATA chemical structure information from INP_ATOM_DATA struct - @param ORIG_ATOM_DATA chemical structure information from ORIG_ATOM_DATA struct - @param INPUT_PARMS input parameters + @param inp_cur_data chemical structure information for the current component + @param orig_inp_data chemical structure information for the whole structure + @param ip input parameters @param component_number number of component in the chemical structure @return Return 0 on success or when no mask is required; return -1 on allocation @@ -375,6 +375,24 @@ static int BuildPolymerCrossingBondEndpointMask(INP_ATOM_DATA *inp_cur_data, return 0; } + /* No mask is needed if this component contains no explicit H/D/T atoms. */ + for (local_idx = 0; local_idx < inp_cur_data->num_at; local_idx++) + { + const char *elname = inp_cur_data->at[local_idx].elname; + + if (elname[1] == '\0' && (elname[0] == 'H' || elname[0] == 'D' || elname[0] == 'T')) + { + break; + } + } + + if (local_idx == inp_cur_data->num_at) + { + return 0; + } + + local_idx = 0; + inp_cur_data->keep_explicit_HDT = (unsigned char *)inchi_calloc((long long)inp_cur_data->num_at, sizeof(inp_cur_data->keep_explicit_HDT[0])); if (!inp_cur_data->keep_explicit_HDT) @@ -389,6 +407,20 @@ static int BuildPolymerCrossingBondEndpointMask(INP_ATOM_DATA *inp_cur_data, continue; } + /* + * The mask is consulted only for explicit H/D/T atoms by + * remove_terminal_HDT(), so heavy-atom and pseudoatom endpoints + * do not need to be marked. + */ + if (!(orig_inp_data->at[orig_idx].elname[1] == '\0' && + (orig_inp_data->at[orig_idx].elname[0] == 'H' || + orig_inp_data->at[orig_idx].elname[0] == 'D' || + orig_inp_data->at[orig_idx].elname[0] == 'T'))) + { + local_idx++; + continue; + } + for (unit_idx = 0; unit_idx < orig_inp_data->polymer->n; unit_idx++) { const OAD_PolymerUnit *u = orig_inp_data->polymer->units[unit_idx]; diff --git a/INCHI-1-SRC/INCHI_BASE/src/runichi3.c b/INCHI-1-SRC/INCHI_BASE/src/runichi3.c index b44aece0..a1514186 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/runichi3.c +++ b/INCHI-1-SRC/INCHI_BASE/src/runichi3.c @@ -1538,7 +1538,7 @@ int OAD_ValidatePolymerAndPseudoElementData(ORIG_ATOM_DATA* orig_at_data, /* Assign polymer type and subunits type and check polymer data for consistency */ /* djb-rwth: addressing coverity ID #499497 -- TREAT_ERR properly used in all cases */ - + orig_at_data->valid_polymer = 0; if ( treat_polymers && pd ) { @@ -1561,6 +1561,10 @@ int OAD_ValidatePolymerAndPseudoElementData(ORIG_ATOM_DATA* orig_at_data, subtype = pd->units[0]->subtype; if ( subtype == POLYMER_SST_RAN || subtype == POLYMER_SST_ALT || subtype == POLYMER_SST_BLK ) { + /** @nnuk: + * 9002 remains assigned to this validation; its former use for + *unsupported polymer H end groups was removed with GHI #252. + */ TREAT_ERR(err, 9002, "Single polymer unit may not be RAN/ALT/BLO"); goto exit_function; } @@ -1837,21 +1841,8 @@ int OAD_ValidatePolymerAndPseudoElementData(ORIG_ATOM_DATA* orig_at_data, a1 = u->blist[2 * k]; a2 = u->blist[2 * k + 1]; /** - *@nnuk : GHI # 252 Addressed - if ( !strcmp(orig_at_data->at[a1 - 1].elname, "H") || - !strcmp(orig_at_data->at[a1 - 1].elname, "D") || - !strcmp(orig_at_data->at[a1 - 1].elname, "T") ) - { - TREAT_ERR(err, 9030, "H as polymer end group is not supported"); - goto exit_function; - } - if ( !strcmp(orig_at_data->at[a2 - 1].elname, "H") || - !strcmp(orig_at_data->at[a2 - 1].elname, "D") || - !strcmp(orig_at_data->at[a2 - 1].elname, "T") ) - { - TREAT_ERR(err, 9031, "H as polymer end group is not supported"); - goto exit_function; - }*/ + *@nnuk: GHI#252 addressed and redundant logic removed + */ /* Ensure that caps of polymer unit lie outside it */ a1_is_not_in_alist = a1_is_star_atom = 0; @@ -4437,7 +4428,7 @@ void OAD_ValidateAndSortOutPseudoElementAtoms(ORIG_ATOM_DATA* orig_at_data, TREAT_ERR(*err, (70 + 5), "Invalid element(s):"); TREAT_ERR(*err, (70 + 5), orig_at_data->at[k].elname); continue; -#endif +#endif } is_star = !strcmp(orig_at_data->at[k].elname, "*"); if ( !is_star ) diff --git a/INCHI-1-TEST/tests/test_unit/test_polymers.cpp b/INCHI-1-TEST/tests/test_unit/test_polymers.cpp index 79862060..87801917 100644 --- a/INCHI-1-TEST/tests/test_unit/test_polymers.cpp +++ b/INCHI-1-TEST/tests/test_unit/test_polymers.cpp @@ -23,8 +23,12 @@ static void ExpectedPolymersInChI(const char* molblock, int ret = MakeINCHIFromMolfileText(molblock, options, poutput); EXPECT_EQ(ret, expected_retcode); - ASSERT_NE(poutput->szInChI, nullptr); - EXPECT_STREQ(poutput->szInChI, expected_inchi); + EXPECT_NE(poutput->szInChI, nullptr); + + if (poutput->szInChI) + { + EXPECT_STREQ(poutput->szInChI, expected_inchi); + } FreeINCHI(poutput); } From a249029412e8295f3875e4cb423bb4d7c6130a4e Mon Sep 17 00:00:00 2001 From: Nauman Ullah Khan <43783619+nnuk@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:18:42 +0000 Subject: [PATCH 4/4] Improve comment --- INCHI-1-SRC/INCHI_BASE/src/runichi2.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/INCHI-1-SRC/INCHI_BASE/src/runichi2.c b/INCHI-1-SRC/INCHI_BASE/src/runichi2.c index f783790d..5a702810 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/runichi2.c +++ b/INCHI-1-SRC/INCHI_BASE/src/runichi2.c @@ -342,10 +342,11 @@ int GetOneStructure( INCHI_CLOCK *ic, **************************************************************************** Build a component-local mask of polymer crossing-bond endpoints. - The polymer blist uses whole-structure, 1-based atom numbering, whereas - inp_cur_data contains only atoms from the selected connected component. - ExtractConnectedComponent() preserves the original atom order within the - component, allowing the corresponding local atom index to be reconstructed. + The polymer crossing-bond list (OAD_PolymerUnit.blist) uses whole-structure, + 1-based atom numbering, whereas inp_cur_data contains only atoms from the + selected connected component. ExtractConnectedComponent() preserves the original + atom order within the component, allowing the corresponding local atom index to + be reconstructed. The mask is later consulted by remove_terminal_HDT() to preserve explicit H/D/T atoms that participate in polymer crossing bonds.