diff --git a/.gitmodules b/.gitmodules index 06b4a88..7acf5eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/ViennaRNA"] - path = lib/viennarna/ViennaRNA - url = https://github.com/ViennaRNA/ViennaRNA.git [submodule "util/regression"] path = lib/mmseqs/util/regression url = https://github.com/soedinglab/MMseqs2-Regression.git diff --git a/CMakeLists.txt b/CMakeLists.txt index d5d259c..0939805 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,18 +31,14 @@ if (RIBOSEEK_FRAMEWORK_ONLY) set(FRAMEWORK_ONLY 1 CACHE INTERNAL "" FORCE) endif() -# Infernal bridge for cmbuild -set(ENABLE_INFERNAL_BRIDGE 1 CACHE BOOL "Enable embedded Infernal-backed CM build bridge") set(INFERNAL_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lib/infernal/vendor" CACHE PATH "Path to vendored Infernal source/build tree") -if (ENABLE_INFERNAL_BRIDGE) - if (NOT EXISTS ${INFERNAL_ROOT}/src/cmbuild.c) - message(FATAL_ERROR "ENABLE_INFERNAL_BRIDGE=ON requires INFERNAL_ROOT with src/cmbuild.c") - endif () - add_subdirectory(lib/infernal) +set(INFERNAL_CONFIG_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/infernal/config") +if (NOT EXISTS ${INFERNAL_ROOT}/src/infernal.h) + message(FATAL_ERROR "riboseek requires vendored infernal at INFERNAL_ROOT") endif () +add_subdirectory(lib/infernal) -# ViennaRNA minimal library (hand-rolled CMake wrapper, autotools-free) -add_subdirectory(lib/viennarna) +add_subdirectory(lib/tornadofold EXCLUDE_FROM_ALL) add_subdirectory(data) add_subdirectory(src) diff --git a/lib/infernal/CMakeLists.txt b/lib/infernal/CMakeLists.txt index ba68e60..ba290f4 100644 --- a/lib/infernal/CMakeLists.txt +++ b/lib/infernal/CMakeLists.txt @@ -3,111 +3,60 @@ set(INFERNAL_HMMER_DIR ${INFERNAL_ROOT}/hmmer) set(INFERNAL_HMMER_SRC_DIR ${INFERNAL_ROOT}/hmmer/src) set(INFERNAL_EASEL_DIR ${INFERNAL_ROOT}/easel) -# out-of-source autoconf configure for hmmer+easel -set(INFERNAL_CONFIGURE_DIR ${CMAKE_BINARY_DIR}/infernal-configure) - -if (NOT EXISTS ${INFERNAL_HMMER_DIR}/configure) - message(FATAL_ERROR "hmmer configure script not found at ${INFERNAL_HMMER_DIR}/configure") -endif () - -if (NOT EXISTS ${INFERNAL_CONFIGURE_DIR}/src/p7_config.h) - # hmmer's configure needs easel/Makefile.in reachable as ${srcdir}/easel/Makefile.in. - # The symlink is committed, but recreate it as a fallback for environments where - # git does not restore symlinks (e.g. Windows without core.symlinks). - if (NOT EXISTS ${INFERNAL_HMMER_DIR}/easel) - execute_process( - COMMAND ${CMAKE_COMMAND} -E create_symlink ../easel ${INFERNAL_HMMER_DIR}/easel - RESULT_VARIABLE _symlink_rc - ) - if (NOT _symlink_rc EQUAL 0) - message(FATAL_ERROR "Failed to create easel symlink in ${INFERNAL_HMMER_DIR}") - endif () - endif () - # Pass --host so configure uses the right target architecture and skips - # run-time probes that fail when cross-compiling - set(_hmmer_configure_args "CC=${CMAKE_C_COMPILER}" "CFLAGS=${CMAKE_C_FLAGS}") - if (CMAKE_OSX_ARCHITECTURES MATCHES "x86_64") - # macOS cross-arch build targeting x86_64 (e.g. on Apple Silicon) - list(APPEND _hmmer_configure_args --host=x86_64-apple-darwin) - elseif (CMAKE_OSX_ARCHITECTURES MATCHES "arm64") - list(APPEND _hmmer_configure_args --host=aarch64-apple-darwin) - elseif (NOT APPLE) - # Ask the compiler for its target triple - execute_process( - COMMAND ${CMAKE_C_COMPILER} -dumpmachine - OUTPUT_VARIABLE _compiler_target - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if (_compiler_target) - list(APPEND _hmmer_configure_args --host=${_compiler_target}) - endif () - endif () - file(MAKE_DIRECTORY ${INFERNAL_CONFIGURE_DIR}) - message(STATUS "Configuring hmmer out-of-source in ${INFERNAL_CONFIGURE_DIR} (args: ${_hmmer_configure_args})") - execute_process( - COMMAND ${INFERNAL_HMMER_DIR}/configure ${_hmmer_configure_args} - WORKING_DIRECTORY ${INFERNAL_CONFIGURE_DIR} - RESULT_VARIABLE _configure_rc - ) - if (NOT _configure_rc EQUAL 0) - message(FATAL_ERROR "./configure failed for hmmer (exit code ${_configure_rc})") - endif () -endif () - if (NOT EXISTS ${INFERNAL_SRC_DIR}/config.h) - message(FATAL_ERROR "Infernal bridge requires generated ${INFERNAL_SRC_DIR}/config.h in INFERNAL_ROOT") + message(FATAL_ERROR "vendored infernal requires ${INFERNAL_SRC_DIR}/config.h") endif () -if (NOT EXISTS ${INFERNAL_CONFIGURE_DIR}/src/p7_config.h) - message(FATAL_ERROR "hmmer configure did not produce ${INFERNAL_CONFIGURE_DIR}/src/p7_config.h") -endif () -if (NOT EXISTS ${INFERNAL_CONFIGURE_DIR}/easel/esl_config.h) - message(FATAL_ERROR "hmmer configure did not produce ${INFERNAL_CONFIGURE_DIR}/easel/esl_config.h") +if (NOT EXISTS ${INFERNAL_CONFIG_DIR}/p7_config.h OR NOT EXISTS ${INFERNAL_CONFIG_DIR}/esl_config.h) + message(FATAL_ERROR "missing config headers in ${INFERNAL_CONFIG_DIR}") endif () -find_package(Threads REQUIRED) +# Build Infernal/HMMER/Easel with the same SIMD/arch flags as riboseek (MMSEQS_ARCH). +if (MMSEQS_ARCH) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MMSEQS_ARCH}") +endif () set(EASEL_SRCS ${INFERNAL_EASEL_DIR}/easel.c - ${INFERNAL_EASEL_DIR}/esl_alloc.c + # ${INFERNAL_EASEL_DIR}/esl_alloc.c ${INFERNAL_EASEL_DIR}/esl_alphabet.c ${INFERNAL_EASEL_DIR}/esl_arr2.c ${INFERNAL_EASEL_DIR}/esl_arr3.c ${INFERNAL_EASEL_DIR}/esl_bitfield.c ${INFERNAL_EASEL_DIR}/esl_buffer.c ${INFERNAL_EASEL_DIR}/esl_cluster.c - ${INFERNAL_EASEL_DIR}/esl_composition.c - ${INFERNAL_EASEL_DIR}/esl_cpu.c + # ${INFERNAL_EASEL_DIR}/esl_composition.c + # ${INFERNAL_EASEL_DIR}/esl_cpu.c ${INFERNAL_EASEL_DIR}/esl_dirichlet.c ${INFERNAL_EASEL_DIR}/esl_distance.c ${INFERNAL_EASEL_DIR}/esl_dmatrix.c - ${INFERNAL_EASEL_DIR}/esl_dsqdata.c + # ${INFERNAL_EASEL_DIR}/esl_dsqdata.c ${INFERNAL_EASEL_DIR}/esl_exponential.c ${INFERNAL_EASEL_DIR}/esl_fileparser.c - ${INFERNAL_EASEL_DIR}/esl_gamma.c - ${INFERNAL_EASEL_DIR}/esl_gencode.c + # ${INFERNAL_EASEL_DIR}/esl_gamma.c + # ${INFERNAL_EASEL_DIR}/esl_gencode.c ${INFERNAL_EASEL_DIR}/esl_getopts.c - ${INFERNAL_EASEL_DIR}/esl_gev.c + # ${INFERNAL_EASEL_DIR}/esl_gev.c ${INFERNAL_EASEL_DIR}/esl_graph.c ${INFERNAL_EASEL_DIR}/esl_gumbel.c - ${INFERNAL_EASEL_DIR}/esl_heap.c - ${INFERNAL_EASEL_DIR}/esl_histogram.c + # ${INFERNAL_EASEL_DIR}/esl_heap.c + # ${INFERNAL_EASEL_DIR}/esl_histogram.c ${INFERNAL_EASEL_DIR}/esl_hmm.c - ${INFERNAL_EASEL_DIR}/esl_huffman.c - ${INFERNAL_EASEL_DIR}/esl_hyperexp.c - ${INFERNAL_EASEL_DIR}/esl_iset.c - ${INFERNAL_EASEL_DIR}/esl_json.c + # ${INFERNAL_EASEL_DIR}/esl_huffman.c + # ${INFERNAL_EASEL_DIR}/esl_hyperexp.c + # ${INFERNAL_EASEL_DIR}/esl_iset.c + # ${INFERNAL_EASEL_DIR}/esl_json.c ${INFERNAL_EASEL_DIR}/esl_keyhash.c - ${INFERNAL_EASEL_DIR}/esl_lognormal.c + # ${INFERNAL_EASEL_DIR}/esl_lognormal.c ${INFERNAL_EASEL_DIR}/esl_matrixops.c ${INFERNAL_EASEL_DIR}/esl_mem.c ${INFERNAL_EASEL_DIR}/esl_minimizer.c ${INFERNAL_EASEL_DIR}/esl_mixdchlet.c - ${INFERNAL_EASEL_DIR}/esl_mixgev.c - ${INFERNAL_EASEL_DIR}/esl_mpi.c + # ${INFERNAL_EASEL_DIR}/esl_mixgev.c + # ${INFERNAL_EASEL_DIR}/esl_mpi.c ${INFERNAL_EASEL_DIR}/esl_msa.c ${INFERNAL_EASEL_DIR}/esl_msacluster.c ${INFERNAL_EASEL_DIR}/esl_msafile.c - ${INFERNAL_EASEL_DIR}/esl_msafile2.c + # ${INFERNAL_EASEL_DIR}/esl_msafile2.c ${INFERNAL_EASEL_DIR}/esl_msafile_a2m.c ${INFERNAL_EASEL_DIR}/esl_msafile_afa.c ${INFERNAL_EASEL_DIR}/esl_msafile_clustal.c @@ -115,20 +64,20 @@ set(EASEL_SRCS ${INFERNAL_EASEL_DIR}/esl_msafile_psiblast.c ${INFERNAL_EASEL_DIR}/esl_msafile_selex.c ${INFERNAL_EASEL_DIR}/esl_msafile_stockholm.c - ${INFERNAL_EASEL_DIR}/esl_msashuffle.c + # ${INFERNAL_EASEL_DIR}/esl_msashuffle.c ${INFERNAL_EASEL_DIR}/esl_msaweight.c - ${INFERNAL_EASEL_DIR}/esl_normal.c - ${INFERNAL_EASEL_DIR}/esl_paml.c + # ${INFERNAL_EASEL_DIR}/esl_normal.c + # ${INFERNAL_EASEL_DIR}/esl_paml.c ${INFERNAL_EASEL_DIR}/esl_quicksort.c ${INFERNAL_EASEL_DIR}/esl_random.c ${INFERNAL_EASEL_DIR}/esl_rand64.c ${INFERNAL_EASEL_DIR}/esl_randomseq.c - ${INFERNAL_EASEL_DIR}/esl_ratematrix.c - ${INFERNAL_EASEL_DIR}/esl_recorder.c - ${INFERNAL_EASEL_DIR}/esl_red_black.c - ${INFERNAL_EASEL_DIR}/esl_regexp.c + # ${INFERNAL_EASEL_DIR}/esl_ratematrix.c + # ${INFERNAL_EASEL_DIR}/esl_recorder.c + # ${INFERNAL_EASEL_DIR}/esl_red_black.c + # ${INFERNAL_EASEL_DIR}/esl_regexp.c ${INFERNAL_EASEL_DIR}/esl_rootfinder.c - ${INFERNAL_EASEL_DIR}/esl_scorematrix.c + # ${INFERNAL_EASEL_DIR}/esl_scorematrix.c ${INFERNAL_EASEL_DIR}/esl_sq.c ${INFERNAL_EASEL_DIR}/esl_sqio.c ${INFERNAL_EASEL_DIR}/esl_sqio_ascii.c @@ -137,74 +86,74 @@ set(EASEL_SRCS ${INFERNAL_EASEL_DIR}/esl_stack.c ${INFERNAL_EASEL_DIR}/esl_stats.c ${INFERNAL_EASEL_DIR}/esl_stopwatch.c - ${INFERNAL_EASEL_DIR}/esl_stretchexp.c - ${INFERNAL_EASEL_DIR}/esl_subcmd.c - ${INFERNAL_EASEL_DIR}/esl_threads.c + # ${INFERNAL_EASEL_DIR}/esl_stretchexp.c + # ${INFERNAL_EASEL_DIR}/esl_subcmd.c + # ${INFERNAL_EASEL_DIR}/esl_threads.c ${INFERNAL_EASEL_DIR}/esl_tree.c - ${INFERNAL_EASEL_DIR}/esl_varint.c + # ${INFERNAL_EASEL_DIR}/esl_varint.c ${INFERNAL_EASEL_DIR}/esl_vectorops.c - ${INFERNAL_EASEL_DIR}/esl_weibull.c - ${INFERNAL_EASEL_DIR}/esl_workqueue.c + # ${INFERNAL_EASEL_DIR}/esl_weibull.c + # ${INFERNAL_EASEL_DIR}/esl_workqueue.c ${INFERNAL_EASEL_DIR}/esl_wuss.c ) set(HMMER_SRCS - ${INFERNAL_HMMER_SRC_DIR}/build.c - ${INFERNAL_HMMER_SRC_DIR}/cachedb.c - ${INFERNAL_HMMER_SRC_DIR}/cachedb_shard.c + # ${INFERNAL_HMMER_SRC_DIR}/build.c + # ${INFERNAL_HMMER_SRC_DIR}/cachedb.c + # ${INFERNAL_HMMER_SRC_DIR}/cachedb_shard.c ${INFERNAL_HMMER_SRC_DIR}/emit.c ${INFERNAL_HMMER_SRC_DIR}/errors.c ${INFERNAL_HMMER_SRC_DIR}/evalues.c ${INFERNAL_HMMER_SRC_DIR}/eweight.c ${INFERNAL_HMMER_SRC_DIR}/generic_decoding.c ${INFERNAL_HMMER_SRC_DIR}/generic_fwdback.c - ${INFERNAL_HMMER_SRC_DIR}/generic_fwdback_chk.c - ${INFERNAL_HMMER_SRC_DIR}/generic_fwdback_banded.c + # ${INFERNAL_HMMER_SRC_DIR}/generic_fwdback_chk.c + # ${INFERNAL_HMMER_SRC_DIR}/generic_fwdback_banded.c ${INFERNAL_HMMER_SRC_DIR}/generic_null2.c - ${INFERNAL_HMMER_SRC_DIR}/generic_msv.c + # ${INFERNAL_HMMER_SRC_DIR}/generic_msv.c ${INFERNAL_HMMER_SRC_DIR}/generic_optacc.c ${INFERNAL_HMMER_SRC_DIR}/generic_stotrace.c - ${INFERNAL_HMMER_SRC_DIR}/generic_viterbi.c - ${INFERNAL_HMMER_SRC_DIR}/generic_vtrace.c - ${INFERNAL_HMMER_SRC_DIR}/h2_io.c - ${INFERNAL_HMMER_SRC_DIR}/heatmap.c - ${INFERNAL_HMMER_SRC_DIR}/hmmlogo.c - ${INFERNAL_HMMER_SRC_DIR}/hmmdmstr.c - ${INFERNAL_HMMER_SRC_DIR}/hmmdmstr_shard.c - ${INFERNAL_HMMER_SRC_DIR}/hmmd_search_status.c - ${INFERNAL_HMMER_SRC_DIR}/hmmdwrkr.c - ${INFERNAL_HMMER_SRC_DIR}/hmmdwrkr_shard.c - ${INFERNAL_HMMER_SRC_DIR}/hmmdutils.c + # ${INFERNAL_HMMER_SRC_DIR}/generic_viterbi.c + # ${INFERNAL_HMMER_SRC_DIR}/generic_vtrace.c + # ${INFERNAL_HMMER_SRC_DIR}/h2_io.c + # ${INFERNAL_HMMER_SRC_DIR}/heatmap.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmlogo.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmdmstr.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmdmstr_shard.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmd_search_status.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmdwrkr.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmdwrkr_shard.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmdutils.c ${INFERNAL_HMMER_SRC_DIR}/hmmer.c ${INFERNAL_HMMER_SRC_DIR}/logsum.c ${INFERNAL_HMMER_SRC_DIR}/modelconfig.c ${INFERNAL_HMMER_SRC_DIR}/modelstats.c - ${INFERNAL_HMMER_SRC_DIR}/mpisupport.c - ${INFERNAL_HMMER_SRC_DIR}/seqmodel.c + # ${INFERNAL_HMMER_SRC_DIR}/mpisupport.c + # ${INFERNAL_HMMER_SRC_DIR}/seqmodel.c ${INFERNAL_HMMER_SRC_DIR}/tracealign.c ${INFERNAL_HMMER_SRC_DIR}/p7_alidisplay.c ${INFERNAL_HMMER_SRC_DIR}/p7_bg.c - ${INFERNAL_HMMER_SRC_DIR}/p7_builder.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_builder.c ${INFERNAL_HMMER_SRC_DIR}/p7_domain.c ${INFERNAL_HMMER_SRC_DIR}/p7_domaindef.c - ${INFERNAL_HMMER_SRC_DIR}/p7_gbands.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_gbands.c ${INFERNAL_HMMER_SRC_DIR}/p7_gmx.c - ${INFERNAL_HMMER_SRC_DIR}/p7_gmxb.c - ${INFERNAL_HMMER_SRC_DIR}/p7_gmxchk.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_gmxb.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_gmxchk.c ${INFERNAL_HMMER_SRC_DIR}/p7_hit.c ${INFERNAL_HMMER_SRC_DIR}/p7_hmm.c - ${INFERNAL_HMMER_SRC_DIR}/p7_hmmcache.c - ${INFERNAL_HMMER_SRC_DIR}/p7_hmmd_search_stats.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_hmmcache.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_hmmd_search_stats.c ${INFERNAL_HMMER_SRC_DIR}/p7_hmmfile.c ${INFERNAL_HMMER_SRC_DIR}/p7_hmmwindow.c ${INFERNAL_HMMER_SRC_DIR}/p7_pipeline.c - ${INFERNAL_HMMER_SRC_DIR}/p7_prior.c + # ${INFERNAL_HMMER_SRC_DIR}/p7_prior.c ${INFERNAL_HMMER_SRC_DIR}/p7_profile.c ${INFERNAL_HMMER_SRC_DIR}/p7_spensemble.c ${INFERNAL_HMMER_SRC_DIR}/p7_tophits.c ${INFERNAL_HMMER_SRC_DIR}/p7_trace.c ${INFERNAL_HMMER_SRC_DIR}/p7_scoredata.c - ${INFERNAL_HMMER_SRC_DIR}/hmmpgmd2msa.c + # ${INFERNAL_HMMER_SRC_DIR}/hmmpgmd2msa.c ${INFERNAL_HMMER_SRC_DIR}/fm_alphabet.c ${INFERNAL_HMMER_SRC_DIR}/fm_general.c ${INFERNAL_HMMER_SRC_DIR}/fm_sse.c @@ -231,7 +180,7 @@ set(HMMER_IMPL_SRCS ${HMMER_IMPL_DIR}/vitfilter.c ${HMMER_IMPL_DIR}/p7_omx.c ${HMMER_IMPL_DIR}/p7_oprofile.c - ${HMMER_IMPL_DIR}/mpi.c + # ${HMMER_IMPL_DIR}/mpi.c ) if (ARM) list(APPEND EASEL_SRCS ${INFERNAL_EASEL_DIR}/esl_neon.c) @@ -258,7 +207,7 @@ set(INFERNAL_CORE_SRCS ${INFERNAL_SRC_DIR}/cm_submodel.c ${INFERNAL_SRC_DIR}/cm_tophits.c ${INFERNAL_SRC_DIR}/cm_trunc.c - ${INFERNAL_SRC_DIR}/cm_p7_band.c + # ${INFERNAL_SRC_DIR}/cm_p7_band.c ${INFERNAL_SRC_DIR}/cm_p7_domaindef.c ${INFERNAL_SRC_DIR}/cm_p7_modelconfig_trunc.c ${INFERNAL_SRC_DIR}/cm_p7_modelmaker.c @@ -273,7 +222,7 @@ set(INFERNAL_CORE_SRCS ${INFERNAL_SRC_DIR}/eweight.c ${INFERNAL_SRC_DIR}/hmmband.c ${INFERNAL_SRC_DIR}/logsum.c - ${INFERNAL_SRC_DIR}/mpisupport.c + # ${INFERNAL_SRC_DIR}/mpisupport.c ${INFERNAL_SRC_DIR}/prior.c ${INFERNAL_SRC_DIR}/rnamat.c ${INFERNAL_SRC_DIR}/stats.c @@ -283,60 +232,27 @@ set(INFERNAL_CORE_SRCS add_library(infernal-vendor-easel STATIC ${EASEL_SRCS}) target_compile_definitions(infernal-vendor-easel PRIVATE HAVE_CONFIG_H=1) target_include_directories(infernal-vendor-easel PRIVATE - ${INFERNAL_CONFIGURE_DIR}/easel + ${INFERNAL_CONFIG_DIR} ${INFERNAL_EASEL_DIR} ) -target_link_libraries(infernal-vendor-easel PUBLIC Threads::Threads m) +target_link_libraries(infernal-vendor-easel PUBLIC m) add_library(infernal-vendor-hmmer STATIC ${HMMER_SRCS} ${HMMER_IMPL_SRCS}) target_compile_definitions(infernal-vendor-hmmer PRIVATE HAVE_CONFIG_H=1) target_include_directories(infernal-vendor-hmmer PRIVATE - ${INFERNAL_CONFIGURE_DIR}/src - ${INFERNAL_CONFIGURE_DIR}/easel + ${INFERNAL_CONFIG_DIR} ${INFERNAL_HMMER_SRC_DIR} ${HMMER_IMPL_DIR} ${INFERNAL_EASEL_DIR} ) -target_link_libraries(infernal-vendor-hmmer PUBLIC infernal-vendor-easel Threads::Threads m) +target_link_libraries(infernal-vendor-hmmer PUBLIC infernal-vendor-easel m) add_library(infernal-vendor-core STATIC ${INFERNAL_CORE_SRCS}) target_compile_definitions(infernal-vendor-core PRIVATE HAVE_CONFIG_H=1) target_include_directories(infernal-vendor-core PRIVATE - ${INFERNAL_CONFIGURE_DIR}/src - ${INFERNAL_CONFIGURE_DIR}/easel - ${INFERNAL_SRC_DIR} - ${INFERNAL_HMMER_SRC_DIR} - ${INFERNAL_EASEL_DIR} -) -target_link_libraries(infernal-vendor-core PUBLIC infernal-vendor-hmmer infernal-vendor-easel Threads::Threads m) - -add_library(infernal-bridge - InfernalBridge.cpp - CmCalibrateCapture.c - ${INFERNAL_SRC_DIR}/cmbuild.c - ${INFERNAL_SRC_DIR}/cmcalibrate.c -) - -set_source_files_properties(${INFERNAL_SRC_DIR}/cmbuild.c PROPERTIES - COMPILE_DEFINITIONS "main=infernal_cmbuild_main;exit=infernal_bridge_exit") -set_source_files_properties(${INFERNAL_SRC_DIR}/cmcalibrate.c PROPERTIES - COMPILE_DEFINITIONS "main=infernal_cmcalibrate_main_impl;exit=infernal_bridge_exit") - -target_compile_definitions(infernal-bridge PRIVATE HAVE_CONFIG_H=1) - -target_include_directories(infernal-bridge PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_include_directories(infernal-bridge PRIVATE - ${INFERNAL_CONFIGURE_DIR}/src - ${INFERNAL_CONFIGURE_DIR}/easel + ${INFERNAL_CONFIG_DIR} ${INFERNAL_SRC_DIR} ${INFERNAL_HMMER_SRC_DIR} ${INFERNAL_EASEL_DIR} ) - -target_link_libraries(infernal-bridge - infernal-vendor-core - infernal-vendor-hmmer - infernal-vendor-easel - Threads::Threads - m -) +target_link_libraries(infernal-vendor-core PUBLIC infernal-vendor-hmmer infernal-vendor-easel m) diff --git a/lib/infernal/CmCalibrateCapture.c b/lib/infernal/CmCalibrateCapture.c deleted file mode 100644 index 3360806..0000000 --- a/lib/infernal/CmCalibrateCapture.c +++ /dev/null @@ -1,58 +0,0 @@ -/* - * CmCalibrateCapture.c - * - * Provides the capture-to-memory shim for embedded infernal cmcalibrate. - * CMakeLists.txt renames cmcalibrate's main() to infernal_cmcalibrate_main_impl(). - * This file wraps that with infernal_cmcalibrate_main(), which after a successful - * run reads the rewritten CM file (always the last positional argv) into a heap - * buffer so that InfernalBridge can retrieve it without an extra disk read. - */ - -#include -#include -#include - -/* Defined by cmcalibrate.c after preprocessor rename */ -int infernal_cmcalibrate_main_impl(int argc, char **argv); - -static int g_capture_enabled = 0; -static char *g_capture_buf = NULL; -static size_t g_capture_len = 0; - -void infernal_cmcalibrate_set_capture_to_memory(int enabled) { - g_capture_enabled = enabled; - if (!enabled) { - free(g_capture_buf); - g_capture_buf = NULL; - g_capture_len = 0; - } -} - -const char *infernal_cmcalibrate_get_captured_cm_text(size_t *opt_len) { - if (opt_len) { *opt_len = g_capture_len; } - return g_capture_buf; -} - -int infernal_cmcalibrate_main(int argc, char **argv) { - int ret = infernal_cmcalibrate_main_impl(argc, argv); - if (g_capture_enabled && ret == 0 && argc > 0) { - /* cmcalibrate rewrites argv[argc-1] (the CM file path) in-place */ - const char *cmpath = argv[argc - 1]; - FILE *f = fopen(cmpath, "rb"); - if (f) { - fseek(f, 0, SEEK_END); - long sz = ftell(f); - rewind(f); - if (sz > 0) { - char *buf = (char *)realloc(g_capture_buf, (size_t)(sz) + 1); - if (buf) { - g_capture_buf = buf; - g_capture_len = (size_t)fread(g_capture_buf, 1, (size_t)sz, f); - g_capture_buf[g_capture_len] = '\0'; - } - } - fclose(f); - } - } - return ret; -} diff --git a/lib/infernal/InfernalBridge.cpp b/lib/infernal/InfernalBridge.cpp deleted file mode 100644 index 81b8a16..0000000 --- a/lib/infernal/InfernalBridge.cpp +++ /dev/null @@ -1,461 +0,0 @@ -#include "InfernalBridge.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" { -int infernal_cmbuild_main(int argc, char **argv); -int infernal_cmcalibrate_main(int argc, char **argv); -void infernal_cmcalibrate_set_capture_to_memory(int enabled); -const char *infernal_cmcalibrate_get_captured_cm_text(size_t *opt_len); -} - -namespace { - -static bool fileExists(const std::string &p) { - struct stat st; - return stat(p.c_str(), &st) == 0; -} - -static std::string makeShmName() { - std::ostringstream os; - os << "/mmseqs-infernal-" << static_cast(getpid()) << "-" << static_cast(std::rand()); - return os.str(); -} - -static bool createInMemoryFd(const std::string *content, int &fdOut, std::string &pathOut, std::string &error) { - fdOut = -1; - pathOut.clear(); - - int fd = -1; - std::string shmName; - for (int attempt = 0; attempt < 16; ++attempt) { - shmName = makeShmName(); - fd = shm_open(shmName.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600); - if (fd >= 0) { - break; - } - } - if (fd >= 0) { - // Unlink immediately; object lives as long as fd is open. - shm_unlink(shmName.c_str()); - } else { - // Fallback for environments where shm_open is unavailable/restricted. - char tmpTemplate[] = "/tmp/mmseqs-infernal-fd-XXXXXX"; - fd = mkstemp(tmpTemplate); - if (fd < 0) { - error = "failed to create anonymous in-memory fd"; - return false; - } - unlink(tmpTemplate); - } - - const size_t n = (content != NULL) ? content->size() : 0; - if (n > 0) { - if (ftruncate(fd, static_cast(n)) != 0) { - error = "ftruncate failed for in-memory file"; - close(fd); - return false; - } - size_t written = 0; - while (written < n) { - const ssize_t w = write(fd, content->data() + written, n - written); - if (w <= 0) { - error = "write failed for in-memory file"; - close(fd); - return false; - } - written += static_cast(w); - } - if (lseek(fd, 0, SEEK_SET) < 0) { - error = "lseek failed for in-memory file"; - close(fd); - return false; - } - } - - std::ostringstream fdPath; - fdPath << "/dev/fd/" << fd; - fdOut = fd; - pathOut = fdPath.str(); - return true; -} - -static thread_local bool gBridgeExitActive = false; -static thread_local int gBridgeExitCode = 1; -static thread_local std::jmp_buf gBridgeExitJmp; - -extern "C" void infernal_bridge_exit(int code) { - if (gBridgeExitActive) { - gBridgeExitCode = code; - std::longjmp(gBridgeExitJmp, 1); - } - std::exit(code); -} - -static int runInfernalMainInProcess(int (*fn)(int, char **), const std::vector &args) { - std::vector argv; - argv.reserve(args.size() + 1); - for (size_t i = 0; i < args.size(); ++i) { - argv.push_back(const_cast(args[i].c_str())); - } - argv.push_back(NULL); - optind = 1; - gBridgeExitCode = 1; - gBridgeExitActive = true; - if (setjmp(gBridgeExitJmp) == 0) { - const int rc = fn(static_cast(args.size()), argv.data()); - gBridgeExitActive = false; - return rc; - } - gBridgeExitActive = false; - return gBridgeExitCode; -} - -} // namespace - -namespace { - -static bool readWholeFile(const std::string &path, std::string &out, std::string &error) { - int fd = open(path.c_str(), O_RDONLY); - if (fd < 0) { - error = "failed to open CM file for readback"; - return false; - } - out.clear(); - char buf[65536]; - for (;;) { - ssize_t r = read(fd, buf, sizeof(buf)); - if (r < 0) { - if (errno == EINTR) continue; - error = "read failed on CM file"; - close(fd); - return false; - } - if (r == 0) break; - out.append(buf, static_cast(r)); - } - close(fd); - return true; -} - -static bool runCmBuildInProcess(const std::string &stockholmText, std::string &cmText, std::string &error, bool calibrate) { - char tmpTemplate[] = "/tmp/mmseqs-infernal-XXXXXX"; - char *tmpDir = mkdtemp(tmpTemplate); - if (tmpDir == NULL) { - error = "mkdtemp failed"; - return false; - } - const std::string dir(tmpDir); - int stoFd = -1; - std::string stoPath; - const std::string cmPath = dir + "/model.cm"; - - if (!createInMemoryFd(&stockholmText, stoFd, stoPath, error)) { - rmdir(dir.c_str()); - return false; - } - - std::vector cmbArgs; - cmbArgs.push_back("infernal-cmbuild"); - cmbArgs.push_back("-F"); - // --hand: respect #=GC RF in the input MSA. Caller emits an all-'x' RF - // line (length = qLen), so every column becomes a consensus match column - // and CM clen == qLen. This is what makes downstream cmsearch traces - // query-centric. - cmbArgs.push_back("--hand"); - cmbArgs.push_back(cmPath); - cmbArgs.push_back(stoPath); - bool ok = (runInfernalMainInProcess(&infernal_cmbuild_main, cmbArgs) == 0); - if (!ok) { - error = "embedded infernal cmbuild failed"; - close(stoFd); - unlink(cmPath.c_str()); - rmdir(dir.c_str()); - return false; - } - - if (calibrate) { - infernal_cmcalibrate_set_capture_to_memory(1); - std::vector calArgs; - calArgs.push_back("infernal-cmcalibrate"); - calArgs.push_back("-L"); - calArgs.push_back("0.01"); - calArgs.push_back("--cpu"); - calArgs.push_back("1"); - calArgs.push_back(cmPath); - if (runInfernalMainInProcess(&infernal_cmcalibrate_main, calArgs) != 0) { - infernal_cmcalibrate_set_capture_to_memory(0); - error = "embedded infernal cmcalibrate failed"; - close(stoFd); - unlink(cmPath.c_str()); - rmdir(dir.c_str()); - return false; - } - size_t cmLen = 0; - const char *calibratedCm = infernal_cmcalibrate_get_captured_cm_text(&cmLen); - if (calibratedCm == NULL || cmLen == 0) { - infernal_cmcalibrate_set_capture_to_memory(0); - error = "embedded infernal did not return calibrated CM text"; - close(stoFd); - unlink(cmPath.c_str()); - rmdir(dir.c_str()); - return false; - } - cmText.assign(calibratedCm, cmLen); - infernal_cmcalibrate_set_capture_to_memory(0); - } else { - ok = readWholeFile(cmPath, cmText, error); - } - - close(stoFd); - unlink(cmPath.c_str()); - rmdir(dir.c_str()); - return ok; -} - -static bool writeAllFd(int fd, const void *buf, size_t n) { - const char *p = static_cast(buf); - while (n > 0) { - ssize_t w = write(fd, p, n); - if (w < 0) { - if (errno == EINTR) continue; - return false; - } - if (w == 0) return false; - p += w; - n -= static_cast(w); - } - return true; -} - -static bool readAllFd(int fd, void *buf, size_t n) { - char *p = static_cast(buf); - while (n > 0) { - ssize_t r = read(fd, p, n); - if (r < 0) { - if (errno == EINTR) continue; - return false; - } - if (r == 0) return false; - p += r; - n -= static_cast(r); - } - return true; -} - -} // namespace - -namespace { - -struct Worker { - pid_t pid; - int toWorker; // parent writes, worker reads - int fromWorker; // worker writes, parent reads - std::mutex io; // serialize send/recv on this worker - bool alive = true; -}; - -static void workerMainLoop(int fdIn, int fdOut) { - for (;;) { - uint8_t flag = 0; - uint64_t len = 0; - if (!readAllFd(fdIn, &flag, 1)) break; - if (!readAllFd(fdIn, &len, sizeof(len))) break; - std::string sto; - if (len > 0) { - if (len > (1ULL << 30)) break; - sto.resize(static_cast(len)); - if (!readAllFd(fdIn, &sto[0], len)) break; - } - std::string cmText; - std::string err; - bool ok = runCmBuildInProcess(sto, cmText, err, flag != 0); - const std::string &payload = ok ? cmText : err; - uint8_t outFlag = ok ? 1 : 0; - uint64_t outLen = static_cast(payload.size()); - if (!writeAllFd(fdOut, &outFlag, 1)) break; - if (!writeAllFd(fdOut, &outLen, sizeof(outLen))) break; - if (outLen > 0 && !writeAllFd(fdOut, payload.data(), payload.size())) break; - } - close(fdIn); - close(fdOut); -} - -} // namespace - -namespace InfernalBridge { - -struct WorkerPool { - std::vector workers; - std::mutex mu; - std::condition_variable cv; - std::deque free; - int aliveCount = 0; -}; - -bool isConfigured() { - return true; -} - -WorkerPool *startWorkerPool(int nWorkers) { - if (nWorkers <= 0) return NULL; - WorkerPool *pool = new WorkerPool(); - pool->workers.reserve(nWorkers); - for (int i = 0; i < nWorkers; ++i) { - int p2w[2]; - int w2p[2]; - if (pipe(p2w) != 0) { - stopWorkerPool(pool); - return NULL; - } - if (pipe(w2p) != 0) { - close(p2w[0]); close(p2w[1]); - stopWorkerPool(pool); - return NULL; - } - pid_t pid = fork(); - if (pid < 0) { - close(p2w[0]); close(p2w[1]); - close(w2p[0]); close(w2p[1]); - stopWorkerPool(pool); - return NULL; - } - if (pid == 0) { - // Child: close parent-side fds of our pipes + inherited fds of - // any older siblings, then enter serial work loop. - close(p2w[1]); - close(w2p[0]); - for (size_t j = 0; j < pool->workers.size(); ++j) { - close(pool->workers[j]->toWorker); - close(pool->workers[j]->fromWorker); - } - workerMainLoop(p2w[0], w2p[1]); - _exit(0); - } - close(p2w[0]); - close(w2p[1]); - Worker *w = new Worker(); - w->pid = pid; - w->toWorker = p2w[1]; - w->fromWorker = w2p[0]; - pool->workers.push_back(w); - pool->free.push_back(static_cast(pool->workers.size()) - 1); - pool->aliveCount++; - } - return pool; -} - -void stopWorkerPool(WorkerPool *pool) { - if (pool == NULL) return; - for (size_t i = 0; i < pool->workers.size(); ++i) { - if (pool->workers[i]->alive) { - close(pool->workers[i]->toWorker); // EOF triggers worker exit - } - } - for (size_t i = 0; i < pool->workers.size(); ++i) { - if (pool->workers[i]->alive) { - int status = 0; - while (waitpid(pool->workers[i]->pid, &status, 0) < 0 && errno == EINTR) {} - close(pool->workers[i]->fromWorker); - } - delete pool->workers[i]; - } - delete pool; -} - -bool buildCmFromStockholmText(WorkerPool *pool, - const std::string &stockholmText, - std::string &cmText, - std::string &error, - bool calibrate) { - if (pool == NULL || pool->workers.empty()) { - error = "no infernal worker pool available"; - return false; - } - - int idx = -1; - { - std::unique_lock lk(pool->mu); - pool->cv.wait(lk, [&]{ return !pool->free.empty() || pool->aliveCount == 0; }); - if (pool->free.empty()) { - error = "all infernal workers have died"; - return false; - } - idx = pool->free.front(); - pool->free.pop_front(); - } - Worker *w = pool->workers[idx]; - - uint8_t flag = calibrate ? 1 : 0; - uint64_t len = static_cast(stockholmText.size()); - uint8_t rFlag = 0; - uint64_t rLen = 0; - std::string payload; - bool ok = false; - { - std::lock_guard iolk(w->io); - ok = writeAllFd(w->toWorker, &flag, 1) - && writeAllFd(w->toWorker, &len, sizeof(len)) - && (len == 0 || writeAllFd(w->toWorker, stockholmText.data(), stockholmText.size())); - if (ok) { - ok = readAllFd(w->fromWorker, &rFlag, 1) - && readAllFd(w->fromWorker, &rLen, sizeof(rLen)); - } - if (ok && rLen > 0) { - if (rLen > (1ULL << 30)) { - ok = false; - } else { - payload.resize(static_cast(rLen)); - ok = readAllFd(w->fromWorker, &payload[0], payload.size()); - } - } - } - - { - std::unique_lock lk(pool->mu); - if (ok) { - pool->free.push_back(idx); - } else { - // Worker IPC broke — assume the child died. Retire the slot - // rather than returning it to the free queue so a crashed worker - // isn't handed out again and again. - w->alive = false; - pool->aliveCount--; - close(w->toWorker); - close(w->fromWorker); - int status = 0; - while (waitpid(w->pid, &status, 0) < 0 && errno == EINTR) {} - } - } - pool->cv.notify_all(); - - if (!ok) { - error = "infernal worker IPC failure (worker retired)"; - return false; - } - if (rFlag == 1) { - cmText = std::move(payload); - return true; - } - error = std::move(payload); - return false; -} - -} // namespace InfernalBridge diff --git a/lib/infernal/InfernalBridge.h b/lib/infernal/InfernalBridge.h deleted file mode 100644 index 5f65f08..0000000 --- a/lib/infernal/InfernalBridge.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef MMSEQS_INFERNALBRIDGE_H -#define MMSEQS_INFERNALBRIDGE_H - -#include - -namespace InfernalBridge { - -struct WorkerPool; // opaque - -bool isConfigured(); - -// Create N pre-forked Infernal worker processes. Fork them BEFORE allocating -// large memory (DBReader mmap etc.) so each worker stays small — this avoids -// the kernel-mm_struct contention that serializes concurrent forks from a fat -// parent. Returns NULL on failure. Thread-safe to use from OMP regions. -WorkerPool *startWorkerPool(int nWorkers); -void stopWorkerPool(WorkerPool *pool); - -// If calibrate is true, worker runs infernal cmcalibrate after cmbuild -// (slow, ~1-2s/query) to fit exp-tail E-value parameters. If false, returns -// the uncalibrated CM and downstream consumers must supply their own E-value -// mapping. -bool buildCmFromStockholmText(WorkerPool *pool, - const std::string &stockholmText, - std::string &cmText, - std::string &error, - bool calibrate = false); - -} // namespace InfernalBridge - -#endif diff --git a/lib/infernal/config/esl_config.h b/lib/infernal/config/esl_config.h new file mode 100644 index 0000000..6fb672f --- /dev/null +++ b/lib/infernal/config/esl_config.h @@ -0,0 +1,68 @@ +#ifndef eslCONFIG_INCLUDED +#define eslCONFIG_INCLUDED + +/* Version info. */ +#define EASEL_VERSION "0.49" +#define EASEL_DATE "Aug 2023" +#define EASEL_COPYRIGHT "Copyright (C) 2023 Howard Hughes Medical Institute." +#define EASEL_LICENSE "Freely distributed under the BSD open source license." +#define EASEL_URL "http://bioeasel.org/" + +/* Control of debugging instrumentation */ +#define eslDEBUGLEVEL 0 + +#if defined(__aarch64__) || defined(__ARM_NEON) || defined(_M_ARM64) +# define eslENABLE_NEON 1 +# if defined(__aarch64__) || defined(_M_ARM64) +# define eslHAVE_NEON_AARCH64 1 +# endif +#elif defined(__SSE2__) || defined(__x86_64__) || defined(_M_X64) || defined(__i386__) +# define eslENABLE_SSE 1 +/* _MM_SET_FLUSH_ZERO_MODE, always avail w/ SSE */ +# define HAVE_FLUSH_ZERO_MODE 1 +#else +# error "unsupported target architecture for Easel (need NEON or SSE2)" +#endif + +/* HAVE_PTHREAD DISABLED */ +/* #undef HAVE_PTHREAD */ + +/* HAVE_GZIP DISABLED */ +/* #undef HAVE_GZIP */ + +/* Headers */ +#if defined(__has_include) +# if __has_include() +/* glibc; absent on macOS/BSD (they use machine/endian.h) */ +# define HAVE_ENDIAN_H 1 +# endif +#elif defined(__linux__) +# define HAVE_ENDIAN_H 1 +#endif +#define HAVE_INTTYPES_H 1 +#define HAVE_STDINT_H 1 +#define HAVE_UNISTD_H 1 +#define HAVE_SYS_TYPES_H 1 +#define HAVE_STRINGS_H 1 +#define HAVE_NETINET_IN_H 1 + +#define HAVE_SYS_PARAM_H 1 + +/* Compiler characteristics */ +#define HAVE_FUNC_ATTRIBUTE_NORETURN 1 +#define HAVE_FUNC_ATTRIBUTE_FORMAT 1 + +/* Functions (all present on Linux/macOS targets) */ +#define HAVE_GETCWD 1 +#define HAVE_GETPID 1 +#define HAVE_POPEN 1 +#define HAVE_STRCASECMP 1 +#define HAVE_STRSEP 1 +#define HAVE_SYSCONF 1 +#define HAVE_TIMES 1 +#define HAVE_FSEEKO 1 + +/* Function behavior */ +#define eslSTOPWATCH_HIGHRES + +#endif /*eslCONFIG_INCLUDED*/ diff --git a/lib/infernal/config/p7_config.h b/lib/infernal/config/p7_config.h new file mode 100644 index 0000000..f350419 --- /dev/null +++ b/lib/infernal/config/p7_config.h @@ -0,0 +1,51 @@ +#ifndef P7_CONFIGH_INCLUDED +#define P7_CONFIGH_INCLUDED + +/* 1. Compile-time constants controlling computational behavior / output. + * (p7_RAMLIMIT and p7_NCPU are omitted: the first is unreferenced by any source + * we compile, the second is only used by HMMER's own program mains, which we + * don't build.) */ +#ifndef p7_ALILENGTH +#define p7_ALILENGTH 50 +#endif + +/* 2. Empirically tuned default parameters. */ +#define p7_ETARGET_AMINO 0.59 /* bits, from the work of Steve Johnson. */ +#define p7_ETARGET_DNA 0.62 /* bits, from the work of Travis Wheeler and Robert Hubley. */ +#define p7_ETARGET_OTHER 1.0 /* bits */ + +#define p7_SEQDBENV "BLASTDB" +#define p7_HMMDBENV "PFAMDB" + +/* 3. Fundamental parameters. */ +#define p7_MAXABET 20 /* maximum size of alphabet (4 or 20) */ +#define p7_MAXCODE 29 /* maximum degenerate alphabet size (18 or 29) */ +#define p7_MAX_SC_TXTLEN 11 +#define p7_MAXDCHLET 20 /* maximum # Dirichlet components in mixture prior */ + +/* 4. Version info. */ +#define HMMER_VERSION "3.4" +#define HMMER_DATE "Aug 2023" +#define HMMER_COPYRIGHT "Copyright (C) 2023 Howard Hughes Medical Institute." +#define HMMER_LICENSE "Freely distributed under the BSD open source license." +#define HMMER_URL "http://hmmer.org/" + +/* Choice of optimized implementation. Must match Easel's esl_config.h and the + * impl_* sources CMake compiles. Selected from the compiler's target macros. */ +#if defined(__aarch64__) || defined(__ARM_NEON) || defined(_M_ARM64) +# define eslENABLE_NEON 1 +#elif defined(__SSE2__) || defined(__x86_64__) || defined(_M_X64) || defined(__i386__) +# define eslENABLE_SSE 1 +# define HAVE_FLUSH_ZERO_MODE 1 +#else +# error "riboseek: unsupported target architecture for vendored HMMER (need NEON or SSE2)" +#endif + +/* System headers (present on our Linux/macOS targets) */ +#define HAVE_NETINET_IN_H 1 +#define HAVE_SYS_PARAM_H 1 + +/* HMMER_THREADS DISABLED */ +/* #undef HMMER_THREADS */ + +#endif /*P7_CONFIGH_INCLUDED*/ diff --git a/lib/infernal/vendor/src/infernal.h b/lib/infernal/vendor/src/infernal.h index 479022b..7cd2779 100644 --- a/lib/infernal/vendor/src/infernal.h +++ b/lib/infernal/vendor/src/infernal.h @@ -3240,7 +3240,23 @@ extern void debug_print_ij_bands(CM_t *cm); /* from logsum.c */ extern void init_ilogsum(void); -extern int ILogsum(int s1, int s2); +/* Moved to header to allow inlining */ +/* u16: log-add corrections are 0..1000 */ +extern uint16_t ilogsum_lookup[LOGSUM_TBL]; +static inline int ILogsum(int s1, int s2) +{ + /* Old version: + * This is called billions of times in inside scoring. + * Not fully the same, impossible cells, can shift slighly up in score, but harmless + const int max = ESL_MAX(-INFTY, ESL_MAX(s1, s2)); + const int min = ESL_MIN(s1, s2); + return (min <= -INFTY || (max-min) >= LOGSUM_TBL) ? max : max + ilogsum_lookup[max-min]; + */ + const int d = s1 - s2; + const int max = (d >= 0) ? s1 : s2; + const int adiff = (d >= 0) ? d : -d; + return ((unsigned)adiff >= (unsigned)LOGSUM_TBL) ? max : max + ilogsum_lookup[adiff]; +} extern int ILogsumNI(int s1, int s2); extern int ILogsumNI_diff(int s1a, int s1b, int s2a, int s2b, int db); extern void FLogsumInit(void); diff --git a/lib/infernal/vendor/src/logsum.c b/lib/infernal/vendor/src/logsum.c index e37a4ef..c6e799e 100644 --- a/lib/infernal/vendor/src/logsum.c +++ b/lib/infernal/vendor/src/logsum.c @@ -69,7 +69,7 @@ #include "infernal.h" -static int ilogsum_lookup[LOGSUM_TBL]; +uint16_t ilogsum_lookup[LOGSUM_TBL]; void init_ilogsum(void) { @@ -83,14 +83,6 @@ init_ilogsum(void) } -int -ILogsum(int s1, int s2) -{ - const int max = ESL_MAX(-INFTY, ESL_MAX(s1, s2)); - const int min = ESL_MIN(s1, s2); - return (min <= -INFTY || (max-min) >= LOGSUM_TBL) ? max : max + ilogsum_lookup[max-min]; -} - /* guaranteed s1 >= -INFTY, s2 >= -INFTY */ int ILogsumNI(int s1, int s2) diff --git a/lib/tornadofold/.clang-format b/lib/tornadofold/.clang-format new file mode 100644 index 0000000..ea16f11 --- /dev/null +++ b/lib/tornadofold/.clang-format @@ -0,0 +1,12 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +ContinuationIndentWidth: 4 +ColumnLimit: 100 +InsertBraces: true +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +KeepEmptyLinesAtTheStartOfBlocks: false +SortIncludes: false +DerivePointerAlignment: false +PointerAlignment: Left diff --git a/lib/tornadofold/.clang-format-ignore b/lib/tornadofold/.clang-format-ignore new file mode 100644 index 0000000..dfb8dc3 --- /dev/null +++ b/lib/tornadofold/.clang-format-ignore @@ -0,0 +1 @@ +src/t2004.h diff --git a/lib/tornadofold/.github/workflows/ci.yml b/lib/tornadofold/.github/workflows/ci.yml new file mode 100644 index 0000000..c7c0f56 --- /dev/null +++ b/lib/tornadofold/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: build-and-test + +on: + push: + pull_request: + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build (make, C++11, scalar path on x86) + run: make + + - name: Self-consistency (verify) + run: ./verify + + - name: Smoke test (ArchiveII golden MFE + structure) + run: python3 tests/smoke_test.py + + - name: CMake build (header library + tools) + run: | + cmake -S . -B build + cmake --build build --parallel + ./build/verify + TORNADOFOLD=./build/tornadofold python3 tests/smoke_test.py diff --git a/lib/tornadofold/.github/workflows/deploy-pages.yml b/lib/tornadofold/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..dcc77e8 --- /dev/null +++ b/lib/tornadofold/.github/workflows/deploy-pages.yml @@ -0,0 +1,42 @@ +name: deploy-pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mymindstorm/setup-emsdk@v14 + - name: Build WebAssembly + run: make wasm + - name: Assemble site + run: | + mkdir -p _site + cp web/index.html web/tornadofold-worker.js _site/ + cp web/tornadofold.js web/tornadofold.wasm web/tornadofold-simd.js web/tornadofold-simd.wasm _site/ + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/lib/tornadofold/.gitignore b/lib/tornadofold/.gitignore new file mode 100644 index 0000000..350ea6d --- /dev/null +++ b/lib/tornadofold/.gitignore @@ -0,0 +1,24 @@ +# compiled binaries +/tornadofold +/tornadofold_noneon +/tornadofold_omp +/tornadofold_v2 +/verify +/kbench +*.o +*.dSYM/ + +# stray tool outputs +*.ps + +# benchmark data (fetched) and generated inputs / logs +bench/archiveII/ +bench/archiveII.tar.gz +bench/*.fa +bench/*.log + +# WebAssembly build artifacts (built in CI, deployed to Pages) +/web/tornadofold.js +/web/tornadofold.wasm +/web/tornadofold-simd.js +/web/tornadofold-simd.wasm diff --git a/lib/tornadofold/CMakeLists.txt b/lib/tornadofold/CMakeLists.txt new file mode 100644 index 0000000..9da4ec7 --- /dev/null +++ b/lib/tornadofold/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.14) +project(tornadofold LANGUAGES CXX) + +# tornadofold is a header-only Turner 2004 SIMD MFE folder (everything lives in +# src/*.h). Consumers embed it with: +# +# add_subdirectory(path/to/rna EXCLUDE_FROM_ALL) # headers only, no binaries +# target_link_libraries(myapp PRIVATE tornadofold::tornadofold) +# +# EXCLUDE_FROM_ALL keeps the CLI drivers out of the parent's default build; the +# INTERFACE library has no build artifact of its own, so nothing compiles until +# a consumer actually #includes and uses the headers. + +add_library(tornadofold INTERFACE) +add_library(tornadofold::tornadofold ALIAS tornadofold) +target_include_directories(tornadofold INTERFACE + $ + $) +target_compile_features(tornadofold INTERFACE cxx_std_11) + +# Build the CLI drivers only when tornadofold is the top-level project. Included via +# add_subdirectory() it stays header-only regardless of EXCLUDE_FROM_ALL; set +# -DTORNADOFOLD_BUILD_TOOLS=ON to build them as a subproject anyway. +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(_tornadofold_tools_default ON) +else() + set(_tornadofold_tools_default OFF) +endif() +option(TORNADOFOLD_BUILD_TOOLS "Build the tornadofold CLI and verify binaries" ${_tornadofold_tools_default}) + +if(TORNADOFOLD_BUILD_TOOLS) + add_executable(tornadofold_cli src/main.cpp) + set_target_properties(tornadofold_cli PROPERTIES OUTPUT_NAME tornadofold) + target_link_libraries(tornadofold_cli PRIVATE tornadofold::tornadofold) + + add_executable(tornadofold_verify src/verify.cpp) + set_target_properties(tornadofold_verify PROPERTIES OUTPUT_NAME verify) + target_link_libraries(tornadofold_verify PRIVATE tornadofold::tornadofold) + + # NEON-disabled build (scalar constant-factor baseline). + add_executable(tornadofold_noneon src/main.cpp) + target_compile_definitions(tornadofold_noneon PRIVATE DISABLE_NEON) + target_link_libraries(tornadofold_noneon PRIVATE tornadofold::tornadofold) + + # Optional OpenMP batch build (parallel across sequences). + find_package(OpenMP QUIET COMPONENTS CXX) + if(OpenMP_CXX_FOUND) + add_executable(tornadofold_omp src/main.cpp) + target_link_libraries(tornadofold_omp PRIVATE tornadofold::tornadofold OpenMP::OpenMP_CXX) + endif() +endif() diff --git a/lib/tornadofold/LICENSE b/lib/tornadofold/LICENSE new file mode 100644 index 0000000..3b3f39d --- /dev/null +++ b/lib/tornadofold/LICENSE @@ -0,0 +1,27 @@ +The MIT License (MIT) +===================== + +Copyright © 2026 The Riboseek Development Team + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/lib/tornadofold/Makefile b/lib/tornadofold/Makefile new file mode 100644 index 0000000..6623c2b --- /dev/null +++ b/lib/tornadofold/Makefile @@ -0,0 +1,46 @@ +CXX ?= clang++ +ifneq (,$(filter x86_64 amd64,$(shell uname -m))) +SIMDFLAGS ?= -mavx2 +endif +CXXFLAGS ?= -O3 -std=c++11 -Wall $(SIMDFLAGS) +# OpenMP: plain -fopenmp on Linux (clang/gcc); Apple clang needs the libomp form. +ifeq ($(shell uname -s),Darwin) +OMPFLAGS ?= -Xpreprocessor -fopenmp -lomp +else +OMPFLAGS ?= -fopenmp +endif + +HDRS = src/tornadofold.h src/energy.h src/t2004.h + +all: tornadofold verify + +tornadofold: src/main.cpp $(HDRS) + $(CXX) $(CXXFLAGS) -o $@ src/main.cpp + +# OpenMP batch build (needs libomp: brew install libomp) +tornadofold_omp: src/main.cpp $(HDRS) + $(CXX) $(CXXFLAGS) $(OMPFLAGS) -o $@ src/main.cpp + +# NEON disabled, isolates the scalar constant factor +tornadofold_noneon: src/main.cpp $(HDRS) + $(CXX) $(CXXFLAGS) -DDISABLE_NEON -o $@ src/main.cpp + +verify: src/verify.cpp $(HDRS) + $(CXX) $(CXXFLAGS) -o $@ src/verify.cpp + +EMCC ?= emcc +EMFLAGS ?= -O3 -std=c++17 -Isrc -lembind \ + -sMODULARIZE=1 -sEXPORT_NAME=tornadofold -sENVIRONMENT=web,worker \ + -sINITIAL_MEMORY=268435456 + +wasm: web/tornadofold.js web/tornadofold-simd.js +web/tornadofold.js: web/tornadofold_wasm.cpp $(HDRS) + $(EMCC) $(EMFLAGS) -o $@ web/tornadofold_wasm.cpp +web/tornadofold-simd.js: web/tornadofold_wasm.cpp $(HDRS) + $(EMCC) $(EMFLAGS) -msimd128 -o $@ web/tornadofold_wasm.cpp + +clean: + rm -f tornadofold tornadofold_omp tornadofold_noneon verify \ + web/tornadofold.js web/tornadofold.wasm web/tornadofold-simd.js web/tornadofold-simd.wasm + +.PHONY: all clean wasm diff --git a/lib/tornadofold/README.md b/lib/tornadofold/README.md new file mode 100644 index 0000000..272b135 --- /dev/null +++ b/lib/tornadofold/README.md @@ -0,0 +1,70 @@ +# ToRNAdoFold + +Fast RNA secondary-structure prediction by minimum free energy using the Turner 2004 +nearest-neighbour model ([Mathews et al., 2004](https://www.pnas.org/doi/10.1073/pnas.0401799101)), +with dangling ends scored on both sides of every helix. + +SIMD-accelerated and header-only C++11 library. Runs on the command line, as a library, or in the browser. + +- **Exact** Turner 2004 nearest-neighbour model in exact integer arithmetic. +- **Fast** SIMD-accelerated folding (ARM NEON, x86 SSE2/AVX2/AVX-512, + WebAssembly SIMD) with OpenMP batch mode. +- **Portable** header-only C++11, no dependencies; embed via CMake. +- **In the browser** interactive WebAssembly folding and visualization. + +## Command line + +```sh +make +# prints sequence, structure, and MFE +echo GGGGAAAACCCC | ./tornadofold +# FASTA, or one sequence per line +./tornadofold < sequences.fa +``` + +Fold a batch in parallel: +```sh +make tornadofold_omp +OMP_NUM_THREADS=8 ./tornadofold_omp < sequences.fa +``` + +## As a library + +```cmake +add_subdirectory(path/to/lib EXCLUDE_FROM_ALL) +target_link_libraries(myapp PRIVATE tornadofold::tornadofold) +``` + +```cpp +#include "tornadofold.h" + +tornadofold::TornadoFold f; +// minimum free energy, in 0.01 kcal/mol +int mfe = f.fold("GGGGAAAACCCC"); +// dot-bracket +std::string structure = f.traceback(mfe); +``` + +## In the browser + +```sh +make wasm +python3 -m http.server -d web +# open http://localhost:8000 +``` + +Paste a sequence to fold and view its structure, or upload a FASTA to fold in +parallel across web workers. + +## Benchmark + +ArchiveII (3966 reference structures; +[Sloma & Mathews, 2016](https://rnajournal.cshlp.org/content/22/12/1808)) against +RNAfold ([Lorenz et al., 2011](https://link.springer.com/article/10.1186/1748-7188-6-26)) +2.7.2 `-d2`, on an NVIDIA DGX Spark (20-core ARM CPU): + +| tool | 1 thread | 20 threads | sensitivity | PPV | F1 | F1 (macro) | +|-------------|---------:|-----------:|------------:|------:|------:|-----------:| +| ToRNAdoFold | 41.5 s | 3.3 s | 0.577 | 0.506 | 0.539 | 0.577 | +| RNAfold | 112.3 s | 9.4 s | 0.576 | 0.506 | 0.539 | 0.577 | + diff --git a/lib/tornadofold/src/energy.h b/lib/tornadofold/src/energy.h new file mode 100644 index 0000000..165c335 --- /dev/null +++ b/lib/tornadofold/src/energy.h @@ -0,0 +1,320 @@ +// Exact Turner 2004 / -d2 energy model, using the parsed t2004 tables. +// Conventions reconstructed from the standard nearest-neighbour model: +// bases A,C,G,U = 0..3 (N=4); table base index = base+1 (N->0). +// pair types CG,GC,GU,UG,AU,UA,NN = 1..7; table pair index = type-1. +#pragma once +#include "t2004.h" +#include +#include +#include +#include +#include +#include + +namespace en { +using t2004::INF; + +// N=0,A=1,C=2,G=3,U=4 +inline int bidx(int b) { + return b < 4 ? b + 1 : 0; +} + +// type 1..7 -> 0..6 +inline int pidx(int pt) { + return pt - 1; +} + +// reverse pair type: pairType(b,a) from pairType(a,b) (CG<->GC, GU<->UG, AU<->UA) +inline int revType(int pt) { + static const int R[8] = {0, 2, 1, 4, 3, 6, 5, 7}; + return R[pt]; +} + +inline int pairType(int a, int b) { + static const int T[4][4] = {{0, 0, 0, 5}, {0, 0, 1, 0}, {0, 2, 0, 3}, {6, 0, 4, 0}}; + if (a < 0 || a > 3 || b < 0 || b > 3) { + return 0; + } + return T[a][b]; +} + +inline int auPen(int pt) { + return (pt == 1 || pt == 2) ? 0 : (pt == 0 ? 0 : t2004::TerminalAU); +} + +struct EM { + // base codes 0..3 (4=N) + const int* s = nullptr; + int n = 0; + std::string seq; + + void set(const std::string& str, const int* bases, int len) { + seq = str; + s = bases; + n = len; + } + int pt(int i, int j) const { + return pairType(s[i], s[j]); + } + + int hpInit(int L) const { + if (L <= 30) { + return t2004::hairpin[L]; + } + return (int)std::lround(t2004::hairpin[30] + t2004::lxc * std::log(L / 30.0)); + } + int intInit(int L) const { + if (L <= 30) { + return t2004::internal[L]; + } + return (int)std::lround(t2004::internal[30] + t2004::lxc * std::log(L / 30.0)); + } + int blgInit(int L) const { + if (L <= 30) { + return t2004::bulge[L]; + } + return (int)std::lround(t2004::bulge[30] + t2004::lxc * std::log(L / 30.0)); + } + + int eHairpin(int i, int j) const { + int p = pt(i, j); + if (!p) { + return INF; + } + int L = j - i - 1; + if (L < 3) { + return INF; + } + // Special loops (tetra/tri/hexa): the tabulated value is the complete + // loop free energy and replaces the generic estimate. Allocation-free. + const char* sp = seq.data() + i; + if (L == 3) { + for (int k = 0; k < t2004::tri_n; ++k) { + if (!std::memcmp(sp, t2004::tri[k].seq, 5)) { + return t2004::tri[k].bonus; + } + } + } else if (L == 4) { + for (int k = 0; k < t2004::tetra_n; ++k) { + if (!std::memcmp(sp, t2004::tetra[k].seq, 6)) { + return t2004::tetra[k].bonus; + } + } + } else if (L == 6) { + for (int k = 0; k < t2004::hexa_n; ++k) { + if (!std::memcmp(sp, t2004::hexa[k].seq, 8)) { + return t2004::hexa[k].bonus; + } + } + } + int e; + if (L == 3) { + e = hpInit(3) + auPen(p); + } else { + e = hpInit(L) + t2004::mm_hairpin[pidx(p)][bidx(s[i + 1])][bidx(s[j - 1])]; + } + return e; + } + + // Precomputed (i,j)-invariant part of the internal-loop energy: everything + // that does not depend on the enclosed pair (p,q). Hoisted out of the + // O(MAXLOOP^2) inner scan so each (a,c) evaluation skips recomputing it. + struct IntPre { + int t1 = 0, pit1 = 0, au1 = 0; + int si1 = 0, sj1 = 0, bsi1 = 0, bsj1 = 0; + const int16_t* stackRow = nullptr; // t2004::stack[pit1] + int mm1n = 0, mm23 = 0, mmgen = 0; // mm_TABLE[pit1][bsi1][bsj1] + }; + IntPre intPre(int i, int j) const { + IntPre P; + P.t1 = pt(i, j); + if (!P.t1) { + return P; // not a pair -> eIntLoop returns INF + } + P.pit1 = pidx(P.t1); + P.au1 = auPen(P.t1); + P.si1 = s[i + 1]; + P.sj1 = s[j - 1]; + P.bsi1 = bidx(P.si1); + P.bsj1 = bidx(P.sj1); + P.stackRow = t2004::stack[P.pit1]; + P.mm1n = t2004::mm_internal_1n[P.pit1][P.bsi1][P.bsj1]; + P.mm23 = t2004::mm_internal_23[P.pit1][P.bsi1][P.bsj1]; + P.mmgen = t2004::mm_internal[P.pit1][P.bsi1][P.bsj1]; + return P; + } + + // Internal-loop energy for enclosed pair (p,q), using precomputed (i,j) + // context P == intPre(i,j). Bit-for-bit identical to the standalone form. + int eIntLoop(const IntPre& P, int i, int j, int p, int q) const { + int t2f = pt(p, q); // enclosed pair 5'->3' + if (!P.t1 || !t2f) { + return INF; + } + int pit2r = pidx(revType(t2f)); // enclosed pair reversed (loop's view) + int n1 = p - i - 1, n2 = j - q - 1; + if (n1 == 0 && n2 == 0) { + return P.stackRow[pit2r]; + } + if (n1 == 0 || n2 == 0) { // bulge + int L = n1 + n2, e = blgInit(L); + if (L == 1) { + e += P.stackRow[pit2r]; + } else { + e += P.au1 + auPen(t2f); + } + return e; + } + int sp1 = s[p - 1], sq1 = s[q + 1]; + if (n1 == 1 && n2 == 1) { + return t2004::int11[P.pit1][pit2r][P.bsi1][P.bsj1]; + } + if (n1 == 1 && n2 == 2) { + return t2004::int21[P.pit1][pit2r][P.bsi1][bidx(sq1)][P.bsj1]; + } + if (n1 == 2 && n2 == 1) { // mirror of 1x2 (verified against RNAeval) + return t2004::int21[pit2r][P.pit1][bidx(sq1)][P.bsi1][bidx(sp1)]; + } + if (n1 == 2 && n2 == 2) { + return t2004::int22[P.pit1][pit2r][P.si1][sp1][sq1][P.sj1]; + } + // general: (i,j)-side mismatch is hoisted in P; only (p,q)-side varies. + int L = n1 + n2; + int e = intInit(L) + std::min(t2004::NINIO_max, std::abs(n1 - n2) * t2004::NINIO_m); + const int16_t(*mm)[5][5]; + int mm_ij; + if (n1 == 1 || n2 == 1) { + mm = t2004::mm_internal_1n; + mm_ij = P.mm1n; + } else if ((n1 == 2 && n2 == 3) || (n1 == 3 && n2 == 2)) { + mm = t2004::mm_internal_23; + mm_ij = P.mm23; + } else { + mm = t2004::mm_internal; + mm_ij = P.mmgen; + } + e += mm_ij; + e += mm[pit2r][bidx(sq1)][bidx(sp1)]; + return e; + } + // Convenience overload (recomputes the (i,j) context) for traceback/eval. + int eIntLoop(int i, int j, int p, int q) const { + return eIntLoop(intPre(i, j), i, j, p, q); + } + + // Multiloop interior branch (p,q): d2 terminal mismatch on both flanks. + int mlStem(int p, int q) const { + int t = pt(p, q); + if (!t) { + return INF; + } + int e = t2004::ML_intern + auPen(t); + int mm5 = (p > 0) ? s[p - 1] : 4; + int mm3 = (q < n - 1) ? s[q + 1] : 4; + e += t2004::mm_multi[pidx(t)][bidx(mm5)][bidx(mm3)]; + return e; + } + // Closing pair of a multiloop, viewed from inside (reversed). + int mlClose(int i, int j) const { + int tr = pt(j, i); + if (!tr) { + return INF; + } + int e = t2004::ML_closing + t2004::ML_intern + auPen(tr); + e += t2004::mm_multi[pidx(tr)][bidx(s[j - 1])][bidx(s[i + 1])]; + return e; + } + // Exterior stem (k,j): mismatch if both neighbors present, else single dangle. + int extStem(int k, int j) const { + int t = pt(k, j); + if (!t) { + return INF; + } + int e = auPen(t); + int mm5 = (k > 0) ? s[k - 1] : -1; + int mm3 = (j < n - 1) ? s[j + 1] : -1; + if (mm5 >= 0 && mm3 >= 0) { + e += t2004::mm_exterior[pidx(t)][bidx(mm5)][bidx(mm3)]; + } else if (mm5 >= 0) { + e += t2004::dangle5[pidx(t)][bidx(mm5)]; + } else if (mm3 >= 0) { + e += t2004::dangle3[pidx(t)][bidx(mm3)]; + } + return e; + } + // Evaluate a dot-bracket structure the way the DP scores it (mirror of the + // loop decomposition). Used to localise scoring bugs against RNAeval. + int evalStructure(const std::string& db, bool verbose = false) const { + int N = (int)db.size(); + std::vector pr(N, -1), st; + for (int i = 0; i < N; ++i) { + char c = db[i]; + if (c == '(' || c == '[' || c == '{') { + st.push_back(i); + } else if (c == ')' || c == ']' || c == '}') { + if (st.empty()) { + return INF; + } + pr[st.back()] = i; + pr[i] = st.back(); + st.pop_back(); + } + } + int total = 0; + // exterior: top-level pairs + for (int i = 0; i < N;) { + if (pr[i] > i) { + total += getVvalRegion(pr, i, pr[i], verbose); + total += extStem(i, pr[i]); + if (verbose) { + fprintf(stderr, "ext stem (%d,%d) +%d\n", i, pr[i], extStem(i, pr[i])); + } + i = pr[i] + 1; + } else { + ++i; + } + } + return total; + } + // energy of the loop closed by (i,j) plus everything inside it + int getVvalRegion(const std::vector& pr, int i, int j, bool verbose) const { + // find children + std::vector> ch; + for (int k = i + 1; k < j;) { + if (pr[k] > k) { + ch.push_back({k, pr[k]}); + k = pr[k] + 1; + } else { + ++k; + } + } + int e = 0; + if (ch.empty()) { + e = eHairpin(i, j); + if (verbose) { + fprintf(stderr, "hairpin (%d,%d) %d\n", i, j, e); + } + } else if (ch.size() == 1) { + int p = ch[0].first, q = ch[0].second; + e = eIntLoop(i, j, p, q) + getVvalRegion(pr, p, q, verbose); + if (verbose) { + fprintf(stderr, "intloop (%d,%d;%d,%d) %d\n", i, j, p, q, eIntLoop(i, j, p, q)); + } + } else { + e = mlClose(i, j); + if (verbose) { + fprintf(stderr, "ml close (%d,%d) +%d\n", i, j, mlClose(i, j)); + } + for (auto& c : ch) { + e += mlStem(c.first, c.second) + getVvalRegion(pr, c.first, c.second, verbose); + if (verbose) { + fprintf(stderr, " ml stem (%d,%d) +%d\n", c.first, c.second, + mlStem(c.first, c.second)); + } + } + } + return e; + } +}; + +} // namespace en diff --git a/lib/tornadofold/src/main.cpp b/lib/tornadofold/src/main.cpp new file mode 100644 index 0000000..e418262 --- /dev/null +++ b/lib/tornadofold/src/main.cpp @@ -0,0 +1,121 @@ +// tornadofold driver: reads FASTA (or one seq per line) from stdin/file, prints +// dot-bracket and MFE. Scalar reference path (fold.h). +#include "tornadofold.h" +#include +#include +#include + +int main(int argc, char** argv) { + bool timing = false, evalMode = false, verbose = false; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-t") { + timing = true; + } else if (a == "-e") { + evalMode = true; + } else if (a == "-v") { + verbose = true; + } + } + if (evalMode) { + // read seq then structure lines, print my model's energy + std::string seqL, dbL; + while (std::getline(std::cin, seqL)) { + if (seqL.empty() || seqL[0] == '>') { + continue; + } + if (!std::getline(std::cin, dbL)) { + break; + } + std::string s; + for (char c : seqL) { + if (isspace((unsigned char)c)) { + continue; + } + char u = toupper(c); + if (u == 'T') { + u = 'U'; + } + s += u; + } + std::vector bb(s.size()); + for (size_t i = 0; i < s.size(); ++i) { + char c = s[i]; + bb[i] = (c == 'A') ? 0 : (c == 'C') ? 1 : (c == 'G') ? 2 : ((c == 'U') ? 3 : 4); + } + en::EM em; + em.set(s, bb.data(), (int)s.size()); + int e = em.evalStructure(dbL, verbose); + printf("%s (%.2f)\n", dbL.c_str(), e / 100.0); + } + return 0; + } + + std::string line, name; + std::vector> seqs; + while (std::getline(std::cin, line)) { + if (line.empty()) { + continue; + } + if (line[0] == '>') { + name = line.substr(1); + continue; + } + // preserve length: uppercase, T->U, unknown kept as-is (treated as N) + std::string s; + for (char c : line) { + if (isspace((unsigned char)c)) { + continue; + } + char u = toupper(c); + if (u == 'T') { + u = 'U'; + } + s += u; + } + if (!s.empty()) { + seqs.push_back({name, s}); + name.clear(); + } + } + + // Fold the batch (optionally across cores via OpenMP), preserving order. + int N = (int)seqs.size(); + std::vector dbs(N); + std::vector ens(N); + std::vector tms(N); + auto tw0 = std::chrono::high_resolution_clock::now(); +#ifdef _OPENMP +#pragma omp parallel +#endif + { + // one folder per thread, reused across sequences (buffers amortise) + tornadofold::TornadoFold f; +#ifdef _OPENMP +#pragma omp for schedule(dynamic) +#endif + for (int k = 0; k < N; ++k) { + auto t0 = std::chrono::high_resolution_clock::now(); + int e = f.fold(seqs[k].second); + auto t1 = std::chrono::high_resolution_clock::now(); + tms[k] = std::chrono::duration(t1 - t0).count(); + ens[k] = e; + dbs[k] = f.traceback(e); + } + } + auto tw1 = std::chrono::high_resolution_clock::now(); + double wall = std::chrono::duration(tw1 - tw0).count(); + double total = 0; + for (int k = 0; k < N; ++k) { + total += tms[k]; + printf("%s\n%s (%.2f)", seqs[k].second.c_str(), dbs[k].c_str(), ens[k] / 100.0); + if (timing) { + printf(" [%.2f ms]", tms[k]); + } + printf("\n"); + } + if (timing) { + fprintf(stderr, "sum fold time: %.2f ms | wall: %.2f ms\n", total, wall); + } + return 0; +} diff --git a/lib/tornadofold/src/t2004.h b/lib/tornadofold/src/t2004.h new file mode 100644 index 0000000..88afdd9 --- /dev/null +++ b/lib/tornadofold/src/t2004.h @@ -0,0 +1,40 @@ +// Turner 2004 energy tables for src/energy.h. +// Generated by util/gen_t2004.py directly from the NNDB turner_2004 .dg bundle. +// Layout: pair index = ViennaRNA pair type - 1; mismatch/dangle base index +// bidx(N)=0, A..U=1..4; int22 canonical-only. Sentinel (NN pair, N base) +// entries are folding-unreachable and set to 0. lxc is kept at ViennaRNA's +// precise 107.856 (the .dg file rounds it to 107.9) for RNAfold-exact +// large-loop extrapolation. +#pragma once +#include +namespace t2004 { +static const int INF = 1000000; +static const int16_t stack[7][7] = {{-240,-330,-210,-140,-210,-210,0},{-330,-340,-250,-150,-220,-240,0},{-210,-250,130,-50,-140,-130,0},{-140,-150,-50,30,-60,-100,0},{-210,-220,-140,-60,-110,-90,0},{-210,-240,-130,-100,-90,-130,0},{0,0,0,0,0,0,0}}; +static const int hairpin[31] = {1000000,1000000,1000000,540,560,570,540,600,550,640,650,660,670,680,690,690,700,710,710,720,720,730,730,740,740,750,750,750,760,760,770}; +static const int bulge[31] = {1000000,380,280,320,360,400,440,460,470,480,490,500,510,520,530,540,540,550,550,560,570,570,580,580,580,590,590,600,600,600,610}; +static const int internal[31] = {1000000,1000000,100,100,110,200,200,210,230,240,250,260,270,280,290,290,300,310,310,320,330,330,340,340,350,350,350,360,360,370,370}; +static const int16_t mm_hairpin[7][5][5] = {{{0,0,0,0,0},{0,-150,-150,-140,-150},{0,-100,-110,-100,-80},{0,-230,-150,-240,-150},{0,-100,-140,-100,-210}},{{0,0,0,0,0},{0,-110,-150,-130,-150},{0,-110,-70,-110,-50},{0,-250,-150,-220,-150},{0,-110,-100,-110,-160}},{{0,0,0,0,0},{0,20,-50,-30,-50},{0,-10,-20,-10,-20},{0,-100,-50,-110,-50},{0,-10,-30,-10,-100}},{{0,0,0,0,0},{0,-50,-30,-60,-30},{0,-20,-10,-20,0},{0,-90,-30,-110,-30},{0,-20,-10,-20,-90}},{{0,0,0,0,0},{0,-30,-50,-30,-50},{0,-10,-20,-10,-20},{0,-120,-50,-110,-50},{0,-10,-30,-10,-120}},{{0,0,0,0,0},{0,-50,-30,-50,-30},{0,-20,-10,-20,0},{0,-150,-30,-150,-30},{0,-20,-10,-20,-90}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}; +static const int16_t mm_internal[7][5][5] = {{{0,0,0,0,0},{0,0,0,-80,0},{0,0,0,0,0},{0,-100,0,-100,0},{0,0,0,0,-60}},{{0,0,0,0,0},{0,0,0,-80,0},{0,0,0,0,0},{0,-100,0,-100,0},{0,0,0,0,-60}},{{0,0,0,0,0},{0,70,70,-10,70},{0,70,70,70,70},{0,-30,70,-30,70},{0,70,70,70,10}},{{0,0,0,0,0},{0,70,70,-10,70},{0,70,70,70,70},{0,-30,70,-30,70},{0,70,70,70,10}},{{0,0,0,0,0},{0,70,70,-10,70},{0,70,70,70,70},{0,-30,70,-30,70},{0,70,70,70,10}},{{0,0,0,0,0},{0,70,70,-10,70},{0,70,70,70,70},{0,-30,70,-30,70},{0,70,70,70,10}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}; +static const int16_t mm_internal_1n[7][5][5] = {{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70}},{{0,0,0,0,0},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70}},{{0,0,0,0,0},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70}},{{0,0,0,0,0},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70},{0,70,70,70,70}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}; +static const int16_t mm_internal_23[7][5][5] = {{{0,0,0,0,0},{0,0,0,-50,0},{0,0,0,0,0},{0,-110,0,-70,0},{0,0,0,0,-30}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,-120,0,-70,0},{0,0,0,0,-30}},{{0,0,0,0,0},{0,70,70,70,70},{0,70,70,70,70},{0,-40,70,0,70},{0,70,70,70,40}},{{0,0,0,0,0},{0,70,70,20,70},{0,70,70,70,70},{0,-40,70,0,70},{0,70,70,70,40}},{{0,0,0,0,0},{0,70,70,70,70},{0,70,70,70,70},{0,-40,70,0,70},{0,70,70,70,40}},{{0,0,0,0,0},{0,70,70,20,70},{0,70,70,70,70},{0,-40,70,0,70},{0,70,70,70,40}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}; +static const int16_t mm_multi[7][5][5] = {{{0,0,0,0,0},{0,-110,-110,-160,-110},{0,-150,-70,-150,-100},{0,-130,-110,-140,-110},{0,-150,-50,-150,-70}},{{0,0,0,0,0},{0,-150,-100,-140,-100},{0,-150,-110,-150,-140},{0,-140,-100,-160,-100},{0,-150,-80,-150,-120}},{{0,0,0,0,0},{0,-100,-70,-50,-70},{0,-80,-60,-80,-60},{0,-110,-70,-80,-70},{0,-80,-50,-80,-50}},{{0,0,0,0,0},{0,-30,-60,-60,-60},{0,-100,-70,-100,-80},{0,-80,-60,-80,-60},{0,-100,-70,-100,-60}},{{0,0,0,0,0},{0,-100,-70,-110,-70},{0,-80,-60,-80,-60},{0,-110,-70,-120,-70},{0,-80,-50,-80,-50}},{{0,0,0,0,0},{0,-80,-60,-80,-60},{0,-100,-70,-100,-80},{0,-80,-60,-80,-60},{0,-100,-70,-100,-80}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}; +static const int16_t mm_exterior[7][5][5] = {{{0,0,0,0,0},{0,-110,-110,-160,-110},{0,-150,-70,-150,-100},{0,-130,-110,-140,-110},{0,-150,-50,-150,-70}},{{0,0,0,0,0},{0,-150,-100,-140,-100},{0,-150,-110,-150,-140},{0,-140,-100,-160,-100},{0,-150,-80,-150,-120}},{{0,0,0,0,0},{0,-100,-70,-50,-70},{0,-80,-60,-80,-60},{0,-110,-70,-80,-70},{0,-80,-50,-80,-50}},{{0,0,0,0,0},{0,-30,-60,-60,-60},{0,-100,-70,-100,-80},{0,-80,-60,-80,-60},{0,-100,-70,-100,-60}},{{0,0,0,0,0},{0,-100,-70,-110,-70},{0,-80,-60,-80,-60},{0,-110,-70,-120,-70},{0,-80,-50,-80,-50}},{{0,0,0,0,0},{0,-80,-60,-80,-60},{0,-100,-70,-100,-80},{0,-80,-60,-80,-60},{0,-100,-70,-100,-80}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}; +static const int16_t dangle5[7][5] = {{0,-50,-30,-20,-10},{0,-20,-30,0,0},{0,-30,-30,-40,-20},{0,-30,-10,-20,-20},{0,-30,-30,-40,-20},{0,-30,-10,-20,-20},{0,0,0,0,0}}; +static const int16_t dangle3[7][5] = {{0,-110,-40,-130,-60},{0,-170,-80,-170,-120},{0,-70,-10,-70,-10},{0,-80,-50,-80,-60},{0,-70,-10,-70,-10},{0,-80,-50,-80,-60},{0,0,0,0,0}}; +static const int16_t int11[7][7][5][5] = {{{{0,0,0,0,0},{0,90,50,50,50},{0,50,50,50,50},{0,50,50,-140,50},{0,50,50,50,40}},{{0,0,0,0,0},{0,90,-40,50,50},{0,30,50,50,60},{0,-10,50,-220,50},{0,50,0,50,-10}},{{0,0,0,0,0},{0,60,50,120,120},{0,120,120,120,120},{0,-20,120,-140,120},{0,120,100,120,110}},{{0,0,0,0,0},{0,220,130,120,120},{0,120,170,120,120},{0,120,120,-140,120},{0,120,120,120,110}},{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,80}},{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,120}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,90,30,-10,50},{0,-40,50,50,0},{0,50,50,-220,50},{0,50,60,50,-10}},{{0,0,0,0,0},{0,80,50,50,50},{0,50,50,50,50},{0,50,50,-230,50},{0,50,50,50,-60}},{{0,0,0,0,0},{0,190,120,150,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,150}},{{0,0,0,0,0},{0,160,120,100,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,70}},{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,80}},{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,120}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,60,120,-20,120},{0,50,120,120,100},{0,120,120,-140,120},{0,120,120,120,110}},{{0,0,0,0,0},{0,190,120,120,120},{0,120,120,120,120},{0,150,120,-140,120},{0,120,120,120,150}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,120}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,160}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,120}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,160}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,220,120,120,120},{0,130,170,120,120},{0,120,120,-140,120},{0,120,120,120,110}},{{0,0,0,0,0},{0,160,120,120,120},{0,120,120,120,120},{0,100,120,-140,120},{0,120,120,120,70}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,160}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,190}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,160}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,190}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,80}},{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,80}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,120}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,160}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,120}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,150}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,120}},{{0,0,0,0,0},{0,120,120,120,120},{0,120,120,120,120},{0,120,120,-140,120},{0,120,120,120,120}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,160}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,190}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,150}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,190,190,190},{0,190,190,-70,190},{0,190,190,190,170}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}}; +static const int16_t int21[7][7][5][5][5] = {{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,230,230,110,230},{0,230,230,110,230},{0,110,110,110,110},{0,230,230,110,230}},{{0,0,0,0,0},{0,230,230,230,230},{0,230,230,230,230},{0,230,230,230,230},{0,230,230,230,230}},{{0,0,0,0,0},{0,110,110,110,110},{0,110,230,110,230},{0,110,110,110,110},{0,110,230,110,230}},{{0,0,0,0,0},{0,230,230,230,150},{0,230,230,230,150},{0,230,230,230,150},{0,150,150,150,150}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,250,230,110,230},{0,230,170,110,230},{0,80,110,110,110},{0,230,230,110,230}},{{0,0,0,0,0},{0,230,230,230,230},{0,230,250,230,230},{0,230,230,230,230},{0,250,230,230,230}},{{0,0,0,0,0},{0,170,230,80,230},{0,110,230,110,230},{0,120,110,110,110},{0,110,230,110,230}},{{0,0,0,0,0},{0,230,230,230,150},{0,230,220,230,150},{0,230,230,230,150},{0,150,170,150,140}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}},{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,250,230,210,230},{0,230,230,230,230},{0,120,110,110,110},{0,230,230,230,230}},{{0,0,0,0,0},{0,230,230,230,230},{0,230,230,230,230},{0,230,230,230,230},{0,230,190,230,230}},{{0,0,0,0,0},{0,110,110,110,110},{0,110,230,110,230},{0,110,110,110,110},{0,110,230,110,230}},{{0,0,0,0,0},{0,230,230,230,150},{0,230,230,230,150},{0,230,230,230,150},{0,150,150,150,150}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,230,230,230,230},{0,230,230,230,230},{0,110,110,110,110},{0,230,230,230,230}},{{0,0,0,0,0},{0,230,230,230,230},{0,230,230,230,230},{0,230,230,230,230},{0,230,230,230,230}},{{0,0,0,0,0},{0,110,230,110,230},{0,110,230,110,230},{0,110,110,110,110},{0,110,230,110,230}},{{0,0,0,0,0},{0,230,230,230,150},{0,230,230,230,150},{0,230,230,230,150},{0,150,150,150,150}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,190,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,250,300,210,300},{0,300,300,300,300},{0,120,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,190,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,190,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,190,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}},{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,250,300,210,300},{0,300,300,300,300},{0,120,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,190,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,190,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,250,370,210,370},{0,370,370,370,370},{0,120,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,190,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}},{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}},{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,190,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,190,190,190,190},{0,300,300,300,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,260,260,260,260},{0,370,370,370,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}},{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,190,190,190},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,300,300,190,300},{0,300,300,190,300},{0,190,190,190,190},{0,300,300,190,300}},{{0,0,0,0,0},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300},{0,300,300,300,300}},{{0,0,0,0,0},{0,190,300,190,300},{0,190,300,190,300},{0,190,190,190,190},{0,190,300,190,300}},{{0,0,0,0,0},{0,300,300,300,220},{0,300,300,300,220},{0,300,300,300,220},{0,220,220,220,220}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,370,260,370},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,370,370,260,370},{0,370,370,260,370},{0,260,260,260,260},{0,370,370,260,370}},{{0,0,0,0,0},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370},{0,370,370,370,370}},{{0,0,0,0,0},{0,260,260,260,260},{0,260,370,260,370},{0,260,260,260,260},{0,260,370,260,370}},{{0,0,0,0,0},{0,370,370,370,300},{0,370,370,370,300},{0,370,370,370,300},{0,300,300,300,300}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}},{{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}},{{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}},{{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,0,0,0}}}}}; +static const int16_t int22[6][6][4][4][4][4] = {{{{{{120,160,20,160},{110,150,20,150},{20,60,-70,60},{110,150,20,150}},{{160,200,60,200},{140,180,110,180},{160,200,60,200},{130,170,90,170}},{{20,60,-70,60},{110,150,20,150},{-30,10,0,10},{110,150,20,150}},{{160,200,60,200},{130,170,90,170},{160,200,60,200},{100,80,-50,80}}},{{{110,140,110,130},{110,140,110,120},{20,110,20,90},{110,140,110,120}},{{150,180,150,170},{140,170,140,150},{150,180,150,170},{120,150,120,140}},{{20,110,20,90},{110,140,110,120},{-40,-10,-40,-20},{110,140,110,120}},{{150,180,150,170},{120,150,120,140},{150,180,150,170},{30,60,30,50}}},{{{20,160,-30,160},{20,150,-40,150},{-70,60,0,60},{20,150,-40,150}},{{60,200,10,200},{110,180,-10,180},{60,200,10,200},{90,170,-20,170}},{{-70,60,0,60},{20,150,-40,150},{0,10,80,10},{20,150,-40,150}},{{60,200,10,200},{90,170,-20,170},{60,200,10,200},{-50,80,20,80}}},{{{110,130,110,100},{110,120,110,30},{20,90,20,-50},{110,120,110,30}},{{150,170,150,80},{140,150,140,60},{150,170,150,80},{120,140,120,50}},{{20,90,20,-50},{110,120,110,30},{-40,-20,-40,20},{110,120,110,30}},{{150,170,150,80},{120,140,120,50},{150,170,150,80},{30,50,30,-40}}}},{{{{130,60,0,170},{110,150,-70,150},{-30,10,-160,-30},{110,150,10,150}},{{100,50,-100,140},{110,150,-60,150},{100,140,10,140},{110,150,70,150}},{{40,30,-70,30},{110,150,10,150},{-30,-30,0,10},{110,150,10,150}},{{100,140,10,140},{110,150,80,150},{100,140,10,140},{150,0,90,70}}},{{{130,220,130,140},{100,130,100,120},{-70,70,-70,0},{100,130,100,120}},{{110,190,100,110},{100,130,100,120},{100,130,100,110},{100,130,100,170}},{{70,70,-10,60},{100,130,100,120},{-40,-10,-40,20},{100,130,100,120}},{{100,130,100,110},{110,140,110,120},{100,130,100,110},{-20,-10,30,20}}},{{{-20,170,-10,170},{-40,150,-40,150},{-170,-30,-90,-30},{10,150,-40,150}},{{70,140,-50,140},{70,150,-40,150},{10,140,-50,140},{70,150,20,150}},{{-50,30,-30,30},{10,150,-40,150},{-30,10,80,10},{10,150,-40,150}},{{10,140,-50,140},{80,150,-50,150},{10,140,-50,140},{90,70,140,70}}},{{{130,140,130,140},{100,120,100,30},{-70,0,-70,50},{100,120,100,30}},{{100,110,100,30},{100,120,100,30},{100,110,100,20},{100,120,100,30}},{{-10,50,-10,140},{100,120,100,30},{-40,-60,-40,70},{100,120,100,30}},{{100,110,100,20},{110,120,110,30},{100,110,100,20},{30,40,30,-60}}}},{{{{270,300,170,300},{230,270,130,270},{150,190,50,190},{230,270,130,270}},{{230,270,130,270},{230,270,190,270},{230,270,130,270},{230,270,190,270}},{{190,230,90,230},{230,270,130,270},{100,140,130,140},{230,270,130,270}},{{230,270,130,270},{230,270,190,270},{230,270,130,270},{290,270,130,270}}},{{{260,290,260,270},{220,250,220,240},{140,230,140,220},{220,250,220,240}},{{220,250,220,240},{220,250,220,240},{220,250,220,240},{220,250,220,240}},{{180,270,180,260},{220,250,220,240},{90,120,90,110},{220,250,220,240}},{{220,250,220,240},{220,250,220,240},{220,250,220,240},{220,250,220,240}}},{{{170,300,110,300},{130,270,80,270},{50,190,130,190},{130,270,80,270}},{{130,270,80,270},{190,270,80,270},{130,270,80,270},{190,270,80,270}},{{90,230,170,230},{130,270,80,270},{130,140,210,140},{130,270,80,270}},{{130,270,80,270},{190,270,80,270},{130,270,80,270},{130,270,210,270}}},{{{260,270,260,240},{220,240,220,150},{140,220,140,70},{220,240,220,150}},{{220,240,220,150},{220,240,220,150},{220,240,220,150},{220,240,220,150}},{{180,260,180,110},{220,240,220,150},{90,110,90,150},{220,240,220,150}},{{220,240,220,150},{220,240,220,150},{220,240,220,150},{220,240,220,150}}}},{{{{160,200,70,200},{200,240,100,240},{60,100,-30,100},{200,240,100,240}},{{200,240,100,240},{200,240,160,240},{200,240,100,240},{200,240,160,240}},{{230,270,130,270},{200,240,100,240},{70,110,100,110},{200,240,100,240}},{{200,240,100,240},{200,240,160,240},{200,240,100,240},{260,240,100,240}}},{{{160,190,160,170},{190,220,190,210},{60,150,60,130},{190,220,190,210}},{{190,220,190,210},{190,220,190,210},{190,220,190,210},{190,220,190,210}},{{220,310,220,300},{190,220,190,210},{60,90,60,80},{190,220,190,210}},{{190,220,190,210},{190,220,190,210},{190,220,190,210},{190,220,190,210}}},{{{70,200,10,200},{100,240,50,240},{-30,100,40,100},{100,240,50,240}},{{100,240,50,240},{160,240,50,240},{100,240,50,240},{160,240,50,240}},{{130,270,210,270},{100,240,50,240},{100,110,180,110},{100,240,50,240}},{{100,240,50,240},{160,240,50,240},{100,240,50,240},{100,240,180,240}}},{{{160,170,160,140},{190,210,190,120},{60,130,60,-10},{190,210,190,120}},{{190,210,190,120},{190,210,190,120},{190,210,190,120},{190,210,190,120}},{{220,300,220,150},{190,210,190,120},{60,80,60,120},{190,210,190,120}},{{190,210,190,120},{190,210,190,120},{190,210,190,120},{190,210,190,120}}}},{{{{200,240,100,240},{170,210,80,210},{70,110,-20,110},{170,210,80,210}},{{180,220,90,220},{180,220,140,220},{180,220,90,220},{180,220,140,220}},{{140,180,50,180},{170,210,80,210},{20,60,60,60},{170,210,80,210}},{{180,220,90,220},{180,220,140,220},{180,220,90,220},{150,130,0,130}}},{{{190,220,190,210},{170,200,170,180},{70,160,70,140},{170,200,170,180}},{{180,210,180,190},{170,200,170,190},{180,210,180,190},{170,200,170,190}},{{140,230,140,210},{170,200,170,180},{20,50,20,30},{170,200,170,180}},{{180,210,180,190},{170,200,170,190},{180,210,180,190},{80,110,80,100}}},{{{100,240,50,240},{80,210,20,210},{-20,110,50,110},{80,210,20,210}},{{90,220,30,220},{140,220,30,220},{90,220,30,220},{140,220,30,220}},{{50,180,120,180},{80,210,20,210},{60,60,130,60},{80,210,20,210}},{{90,220,30,220},{140,220,30,220},{90,220,30,220},{0,130,70,130}}},{{{190,210,190,180},{170,180,170,90},{70,140,70,0},{170,180,170,90}},{{180,190,180,100},{170,190,170,100},{180,190,180,100},{170,190,170,100}},{{140,210,140,60},{170,180,170,90},{20,30,20,70},{170,180,170,90}},{{180,190,180,100},{170,190,170,100},{180,190,180,100},{80,100,80,10}}}},{{{{200,240,100,240},{150,190,60,190},{90,130,0,130},{150,190,60,190}},{{200,240,100,240},{200,240,160,240},{200,240,100,240},{200,240,160,240}},{{100,140,10,140},{150,190,60,190},{40,80,80,80},{150,190,60,190}},{{200,240,100,240},{170,210,130,210},{200,240,100,240},{170,150,20,150}}},{{{190,220,190,210},{150,180,150,160},{90,180,90,160},{150,180,150,160}},{{190,220,190,210},{190,220,190,210},{190,220,190,210},{190,220,190,210}},{{100,190,100,170},{150,180,150,160},{40,70,40,50},{150,180,150,160}},{{190,220,190,210},{160,190,160,180},{190,220,190,210},{110,140,110,120}}},{{{100,240,50,240},{60,190,0,190},{0,130,70,130},{60,190,0,190}},{{100,240,50,240},{160,240,50,240},{100,240,50,240},{160,240,50,240}},{{10,140,80,140},{60,190,0,190},{80,80,150,80},{60,190,0,190}},{{100,240,50,240},{130,210,20,210},{100,240,50,240},{20,150,90,150}}},{{{190,210,190,180},{150,160,150,70},{90,160,90,10},{150,160,150,70}},{{190,210,190,120},{190,210,190,120},{190,210,190,120},{190,210,190,120}},{{100,170,100,20},{150,160,150,70},{40,50,40,90},{150,160,150,70}},{{190,210,190,120},{160,180,160,90},{190,210,190,120},{110,120,110,30}}}}},{{{{{130,100,40,100},{130,110,70,100},{-20,70,-50,10},{130,100,-10,100}},{{60,50,30,140},{220,190,70,130},{170,140,30,140},{140,110,50,110}},{{0,-100,-70,10},{130,100,-10,100},{-10,-50,-30,-50},{130,100,-10,100}},{{170,140,30,140},{140,110,60,110},{170,140,30,140},{140,30,140,20}}},{{{110,110,110,110},{100,100,100,110},{-40,70,10,80},{100,100,100,110}},{{150,150,150,150},{130,130,130,140},{150,150,150,150},{120,120,120,120}},{{-70,-60,10,80},{100,100,100,110},{-40,-40,-40,-50},{100,100,100,110}},{{150,150,150,150},{120,120,120,120},{150,150,150,150},{30,30,30,30}}},{{{-30,100,-30,100},{-70,100,-40,100},{-170,10,-30,10},{-70,100,-40,100}},{{10,140,-30,140},{70,130,-10,130},{-30,140,10,140},{0,110,-60,110}},{{-160,10,0,10},{-70,100,-40,100},{-90,-50,80,-50},{-70,100,-40,100}},{{-30,140,10,140},{0,110,20,110},{-30,140,10,140},{50,20,70,20}}},{{{110,110,110,150},{100,100,100,-20},{10,70,10,90},{100,100,100,30}},{{150,150,150,0},{130,130,130,-10},{150,150,150,70},{120,120,120,40}},{{10,70,10,90},{100,100,100,30},{-40,20,-40,140},{100,100,100,30}},{{150,150,150,70},{120,170,120,20},{150,150,150,70},{30,30,30,-60}}}},{{{{150,120,10,120},{120,90,-10,90},{-50,-80,-190,-80},{120,90,-10,90}},{{120,90,-20,90},{120,90,50,90},{120,90,-20,90},{120,90,50,90}},{{10,-20,-130,-20},{120,90,-10,90},{-20,-50,-20,-50},{120,90,-10,90}},{{120,90,-20,90},{130,100,50,100},{120,90,-20,90},{110,20,-90,20}}},{{{120,120,120,130},{100,100,100,100},{-80,-20,-80,-10},{100,100,100,100}},{{90,90,90,100},{100,100,100,100},{90,90,90,100},{100,100,100,100}},{{-10,50,-10,50},{100,100,100,100},{-40,-40,-40,-40},{100,100,100,100}},{{90,90,90,100},{100,100,100,110},{90,90,90,100},{20,20,20,30}}},{{{-50,120,-20,120},{-80,90,-40,90},{-260,-80,-90,-80},{-80,90,-40,90}},{{-80,90,-50,90},{-20,90,-40,90},{-80,90,-50,90},{-20,90,-40,90}},{{-190,-20,-20,-20},{-80,90,-40,90},{-90,-50,80,-50},{-80,90,-40,90}},{{-80,90,-50,90},{-10,100,-40,100},{-80,90,-50,90},{-150,20,10,20}}},{{{120,120,120,110},{100,100,100,20},{-80,-20,-80,-150},{100,100,100,20}},{{90,90,90,20},{100,100,100,20},{90,90,90,20},{100,100,100,20}},{{-10,50,-10,-90},{100,100,100,20},{-40,-40,-40,10},{100,100,100,20}},{{90,90,90,20},{100,100,100,30},{90,90,90,20},{20,20,20,-50}}}},{{{{280,250,140,250},{240,210,100,210},{160,130,20,130},{240,210,100,210}},{{240,210,100,210},{240,210,160,210},{240,210,100,210},{240,210,160,210}},{{200,170,60,170},{240,210,100,210},{110,80,100,80},{240,210,100,210}},{{240,210,100,210},{240,210,160,210},{240,210,100,210},{300,210,100,210}}},{{{250,250,250,260},{220,220,220,220},{140,200,140,200},{220,220,220,220}},{{220,220,220,220},{220,220,220,220},{220,220,220,220},{220,220,220,220}},{{180,240,180,240},{220,220,220,220},{90,90,90,90},{220,220,220,220}},{{220,220,220,220},{220,220,220,220},{220,220,220,220},{220,220,220,220}}},{{{70,250,110,250},{40,210,80,210},{-40,130,130,130},{40,210,80,210}},{{40,210,80,210},{100,210,80,210},{40,210,80,210},{100,210,80,210}},{{0,170,170,170},{40,210,80,210},{40,80,210,80},{40,210,80,210}},{{40,210,80,210},{100,210,80,210},{40,210,80,210},{40,210,210,210}}},{{{250,250,250,240},{220,220,220,140},{140,200,140,60},{220,220,220,140}},{{220,220,220,140},{220,220,220,140},{220,220,220,140},{220,220,220,140}},{{180,240,180,100},{220,220,220,140},{90,90,90,140},{220,220,220,140}},{{220,220,220,140},{220,220,220,140},{220,220,220,140},{220,220,220,140}}}},{{{{190,150,40,150},{210,180,70,180},{80,50,-60,50},{210,180,70,180}},{{210,180,70,180},{210,180,130,180},{210,180,70,180},{210,180,130,180}},{{240,210,100,210},{210,180,70,180},{80,50,70,50},{210,180,70,180}},{{210,180,70,180},{210,180,130,180},{210,180,70,180},{270,180,70,180}}},{{{150,150,150,160},{190,190,190,190},{50,110,50,120},{190,190,190,190}},{{190,190,190,190},{190,190,190,190},{190,190,190,190},{190,190,190,190}},{{220,280,220,280},{190,190,190,190},{60,60,60,60},{190,190,190,190}},{{190,190,190,190},{190,190,190,190},{190,190,190,190},{190,190,190,190}}},{{{-20,150,10,150},{10,180,50,180},{-120,50,40,50},{10,180,50,180}},{{10,180,50,180},{70,180,50,180},{10,180,50,180},{70,180,50,180}},{{40,210,210,210},{10,180,50,180},{10,50,180,50},{10,180,50,180}},{{10,180,50,180},{70,180,50,180},{10,180,50,180},{10,180,180,180}}},{{{150,150,150,140},{190,190,190,110},{50,110,50,-20},{190,190,190,110}},{{190,190,190,110},{190,190,190,110},{190,190,190,110},{190,190,190,110}},{{220,280,220,140},{190,190,190,110},{60,60,60,110},{190,190,190,110}},{{190,190,190,110},{190,190,190,110},{190,190,190,110},{190,190,190,110}}}},{{{{210,180,70,180},{190,160,50,160},{90,60,-50,60},{190,160,50,160}},{{200,170,60,170},{190,160,110,160},{200,170,60,170},{190,160,110,160}},{{160,130,20,130},{190,160,50,160},{40,10,30,10},{190,160,50,160}},{{200,170,60,170},{190,160,110,160},{200,170,60,170},{160,70,-30,70}}},{{{190,190,190,190},{160,160,160,170},{60,120,60,130},{160,160,160,170}},{{170,170,170,180},{170,170,170,170},{170,170,170,180},{170,170,170,170}},{{130,190,130,200},{160,160,160,170},{10,10,10,20},{160,160,160,170}},{{170,170,170,180},{170,170,170,170},{170,170,170,180},{80,80,80,80}}},{{{10,180,50,180},{-10,160,20,160},{-110,60,50,60},{-10,160,20,160}},{{0,170,30,170},{50,160,30,160},{0,170,30,170},{50,160,30,160}},{{-40,130,120,130},{-10,160,20,160},{-30,10,130,10},{-10,160,20,160}},{{0,170,30,170},{50,160,30,160},{0,170,30,170},{-100,70,70,70}}},{{{190,190,190,170},{160,160,160,90},{60,120,60,-10},{160,160,160,90}},{{170,170,170,100},{170,170,170,90},{170,170,170,100},{170,170,170,90}},{{130,190,130,60},{160,160,160,90},{10,10,10,70},{160,160,160,90}},{{170,170,170,100},{170,170,170,90},{170,170,170,100},{80,80,80,0}}}},{{{{210,180,70,180},{170,140,30,140},{110,80,-30,80},{170,140,30,140}},{{210,180,70,180},{210,180,130,180},{210,180,70,180},{210,180,130,180}},{{120,90,-20,90},{170,140,30,140},{60,30,50,30},{170,140,30,140}},{{210,180,70,180},{180,150,100,150},{210,180,70,180},{190,100,-10,100}}},{{{190,190,190,190},{140,140,140,150},{80,140,80,150},{140,140,140,150}},{{190,190,190,190},{190,190,190,190},{190,190,190,190},{190,190,190,190}},{{90,150,90,160},{140,140,140,150},{30,30,30,40},{140,140,140,150}},{{190,190,190,190},{160,160,160,160},{190,190,190,190},{100,100,100,110}}},{{{10,180,50,180},{-30,140,0,140},{-90,80,70,80},{-30,140,0,140}},{{10,180,50,180},{70,180,50,180},{10,180,50,180},{70,180,50,180}},{{-80,90,80,90},{-30,140,0,140},{-10,30,150,30},{-30,140,0,140}},{{10,180,50,180},{40,150,20,150},{10,180,50,180},{-70,100,90,100}}},{{{190,190,190,170},{140,140,140,70},{80,140,80,10},{140,140,140,70}},{{190,190,190,110},{190,190,190,110},{190,190,190,110},{190,190,190,110}},{{90,150,90,20},{140,140,140,70},{30,30,30,90},{140,140,140,70}},{{190,190,190,110},{160,160,160,80},{190,190,190,110},{100,100,100,30}}}}},{{{{{270,230,190,230},{260,220,180,220},{170,130,90,130},{260,220,180,220}},{{300,270,230,270},{290,250,270,250},{300,270,230,270},{270,240,260,240}},{{170,130,90,130},{260,220,180,220},{110,80,170,80},{260,220,180,220}},{{300,270,230,270},{270,240,260,240},{300,270,230,270},{240,150,110,150}}},{{{230,230,230,230},{220,220,220,220},{130,190,130,190},{220,220,220,220}},{{270,270,270,270},{250,250,250,250},{270,270,270,270},{240,240,240,240}},{{130,190,130,190},{220,220,220,220},{80,80,80,80},{220,220,220,220}},{{270,270,270,270},{240,240,240,240},{270,270,270,270},{150,150,150,150}}},{{{150,230,100,230},{140,220,90,220},{50,130,130,130},{140,220,90,220}},{{190,270,140,270},{230,250,120,250},{190,270,140,270},{220,240,110,240}},{{50,130,130,130},{140,220,90,220},{130,80,210,80},{140,220,90,220}},{{190,270,140,270},{220,240,110,240},{190,270,140,270},{70,150,150,150}}},{{{230,230,230,290},{220,220,220,220},{130,190,130,130},{220,220,220,220}},{{270,270,270,270},{250,250,250,250},{270,270,270,270},{240,240,240,240}},{{130,190,130,130},{220,220,220,220},{80,80,80,210},{220,220,220,220}},{{270,270,270,270},{240,240,240,240},{270,270,270,270},{150,150,150,150}}}},{{{{280,240,200,240},{250,220,180,220},{70,40,0,40},{250,220,180,220}},{{250,210,170,210},{250,220,240,220},{250,210,170,210},{250,220,240,220}},{{140,100,60,100},{250,220,180,220},{110,80,170,80},{250,220,180,220}},{{250,210,170,210},{260,220,240,220},{250,210,170,210},{240,140,100,140}}},{{{240,240,240,240},{220,220,220,220},{40,100,40,100},{220,220,220,220}},{{210,210,210,210},{220,220,220,220},{210,210,210,210},{220,220,220,220}},{{100,160,100,160},{220,220,220,220},{80,80,80,80},{220,220,220,220}},{{210,210,210,210},{220,220,220,220},{210,210,210,210},{140,140,140,140}}},{{{160,240,110,240},{140,220,90,220},{-40,40,40,40},{140,220,90,220}},{{130,210,80,210},{200,220,90,220},{130,210,80,210},{200,220,90,220}},{{20,100,100,100},{140,220,90,220},{130,80,210,80},{140,220,90,220}},{{130,210,80,210},{200,220,90,220},{130,210,80,210},{60,140,140,140}}},{{{240,240,240,300},{220,220,220,220},{40,100,40,40},{220,220,220,220}},{{210,210,210,210},{220,220,220,220},{210,210,210,210},{220,220,220,220}},{{100,160,100,100},{220,220,220,220},{80,80,80,210},{220,220,220,220}},{{210,210,210,210},{220,220,220,220},{210,210,210,210},{140,140,140,140}}}},{{{{410,370,330,370},{370,340,300,340},{290,260,220,260},{370,340,300,340}},{{370,340,300,340},{370,340,360,340},{370,340,300,340},{370,340,360,340}},{{330,300,260,300},{370,340,300,340},{240,210,300,210},{370,340,300,340}},{{370,340,300,340},{370,340,360,340},{370,340,300,340},{430,340,300,340}}},{{{370,370,370,370},{340,340,340,340},{260,320,260,320},{340,340,340,340}},{{340,340,340,340},{340,340,340,340},{340,340,340,340},{340,340,340,340}},{{300,360,300,360},{340,340,340,340},{210,210,210,210},{340,340,340,340}},{{340,340,340,340},{340,340,340,340},{340,340,340,340},{340,340,340,340}}},{{{290,370,240,370},{260,340,210,340},{180,260,260,260},{260,340,210,340}},{{260,340,210,340},{320,340,210,340},{260,340,210,340},{320,340,210,340}},{{220,300,300,300},{260,340,210,340},{260,210,340,210},{260,340,210,340}},{{260,340,210,340},{320,340,210,340},{260,340,210,340},{260,340,340,340}}},{{{370,370,370,430},{340,340,340,340},{260,320,260,260},{340,340,340,340}},{{340,340,340,340},{340,340,340,340},{340,340,340,340},{340,340,340,340}},{{300,360,300,300},{340,340,340,340},{210,210,210,340},{340,340,340,340}},{{340,340,340,340},{340,340,340,340},{340,340,340,340},{340,340,340,340}}}},{{{{360,270,360,270},{340,310,270,310},{220,170,130,170},{340,310,270,310}},{{340,310,270,310},{340,310,330,310},{340,310,270,310},{340,310,330,310}},{{370,340,300,340},{340,310,270,310},{210,180,270,180},{340,310,270,310}},{{340,310,270,310},{340,310,330,310},{340,310,270,310},{400,310,270,310}}},{{{270,270,270,270},{310,310,310,310},{170,230,170,230},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}},{{340,400,340,400},{310,310,310,310},{180,180,180,180},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}}},{{{190,270,140,270},{230,310,180,310},{20,170,170,170},{230,310,180,310}},{{230,310,180,310},{290,310,180,310},{230,310,180,310},{290,310,180,310}},{{260,340,340,340},{230,310,180,310},{230,180,310,180},{230,310,180,310}},{{230,310,180,310},{290,310,180,310},{230,310,180,310},{230,310,310,310}}},{{{270,270,270,330},{310,310,310,310},{170,230,170,170},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}},{{340,400,340,340},{310,310,310,310},{180,180,180,310},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}}}},{{{{340,310,270,310},{320,280,240,280},{220,180,140,180},{320,280,240,280}},{{330,290,250,290},{320,290,310,290},{330,290,250,290},{320,290,310,290}},{{290,250,210,250},{320,280,240,280},{170,130,220,130},{320,280,240,280}},{{330,290,250,290},{320,290,310,290},{330,290,250,290},{290,200,160,200}}},{{{310,310,310,310},{280,280,280,280},{180,240,180,240},{280,280,280,280}},{{290,290,290,290},{290,290,290,290},{290,290,290,290},{290,290,290,290}},{{250,310,250,310},{280,280,280,280},{130,130,130,130},{280,280,280,280}},{{290,290,290,290},{290,290,290,290},{290,290,290,290},{200,200,200,200}}},{{{230,310,180,310},{200,280,150,280},{100,180,180,180},{200,280,150,280}},{{210,290,160,290},{270,290,160,290},{210,290,160,290},{270,290,160,290}},{{170,250,250,250},{200,280,150,280},{180,130,260,130},{200,280,150,280}},{{210,290,160,290},{270,290,160,290},{210,290,160,290},{120,200,200,200}}},{{{310,310,310,370},{280,280,280,280},{180,240,180,180},{280,280,280,280}},{{290,290,290,290},{290,290,290,290},{290,290,290,290},{290,290,290,290}},{{250,310,250,250},{280,280,280,280},{130,130,130,260},{280,280,280,280}},{{290,290,290,290},{290,290,290,290},{290,290,290,290},{200,200,200,200}}}},{{{{340,310,270,310},{300,260,220,260},{240,200,160,200},{300,260,220,260}},{{340,310,270,310},{340,310,330,310},{340,310,270,310},{340,310,330,310}},{{250,210,170,210},{300,260,220,260},{190,150,240,150},{300,260,220,260}},{{340,310,270,310},{310,280,300,280},{340,310,270,310},{320,220,180,220}}},{{{310,310,310,310},{260,260,260,260},{200,260,200,260},{260,260,260,260}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}},{{210,270,210,270},{260,260,260,260},{150,150,150,150},{260,260,260,260}},{{310,310,310,310},{280,280,280,280},{310,310,310,310},{220,220,220,220}}},{{{230,310,180,310},{180,260,130,260},{120,200,200,200},{180,260,130,260}},{{230,310,180,310},{290,310,180,310},{230,310,180,310},{290,310,180,310}},{{130,210,210,210},{180,260,130,260},{200,150,280,150},{180,260,130,260}},{{230,310,180,310},{260,280,150,280},{230,310,180,310},{140,220,220,220}}},{{{310,310,310,370},{260,260,260,260},{200,260,200,200},{260,260,260,260}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}},{{210,270,210,210},{260,260,260,260},{150,150,150,280},{260,260,260,260}},{{310,310,310,310},{280,280,280,280},{310,310,310,310},{220,220,220,220}}}}},{{{{{160,200,230,200},{160,190,220,190},{70,100,130,100},{160,190,220,190}},{{200,240,270,240},{190,220,310,220},{200,240,270,240},{170,210,300,210}},{{70,100,130,100},{160,190,220,190},{10,50,210,50},{160,190,220,190}},{{200,240,270,240},{170,210,300,210},{200,240,270,240},{140,120,150,120}}},{{{200,200,200,200},{190,190,190,190},{100,160,100,160},{190,190,190,190}},{{240,240,240,240},{220,220,220,220},{240,240,240,240},{210,210,210,210}},{{100,160,100,160},{190,190,190,190},{50,50,50,50},{190,190,190,190}},{{240,240,240,240},{210,210,210,210},{240,240,240,240},{120,120,120,120}}},{{{60,200,70,200},{60,190,60,190},{-30,100,100,100},{60,190,60,190}},{{100,240,110,240},{150,220,90,220},{100,240,110,240},{130,210,80,210}},{{-30,100,100,100},{60,190,60,190},{40,50,180,50},{60,190,60,190}},{{100,240,110,240},{130,210,80,210},{100,240,110,240},{-10,120,120,120}}},{{{200,200,200,260},{190,190,190,190},{100,160,100,100},{190,190,190,190}},{{240,240,240,240},{220,220,220,220},{240,240,240,240},{210,210,210,210}},{{100,160,100,100},{190,190,190,190},{50,50,50,180},{190,190,190,190}},{{240,240,240,240},{210,210,210,210},{240,240,240,240},{120,120,120,120}}}},{{{{190,210,240,210},{150,190,220,190},{-20,10,40,10},{150,190,220,190}},{{150,180,210,180},{150,190,280,190},{150,180,210,180},{150,190,280,190}},{{40,70,100,70},{150,190,220,190},{10,50,210,50},{150,190,220,190}},{{150,180,210,180},{160,190,280,190},{150,180,210,180},{140,110,140,110}}},{{{210,210,210,210},{190,190,190,190},{10,70,10,70},{190,190,190,190}},{{180,180,180,180},{190,190,190,190},{180,180,180,180},{190,190,190,190}},{{70,130,70,130},{190,190,190,190},{50,50,50,50},{190,190,190,190}},{{180,180,180,180},{190,190,190,190},{180,180,180,180},{110,110,110,110}}},{{{80,210,80,210},{50,190,60,190},{-120,10,10,10},{50,190,60,190}},{{50,180,50,180},{110,190,60,190},{50,180,50,180},{110,190,60,190}},{{-60,70,70,70},{50,190,60,190},{40,50,180,50},{50,190,60,190}},{{50,180,50,180},{120,190,60,190},{50,180,50,180},{-20,110,110,110}}},{{{210,210,210,270},{190,190,190,190},{10,70,10,10},{190,190,190,190}},{{180,180,180,180},{190,190,190,190},{180,180,180,180},{190,190,190,190}},{{70,130,70,70},{190,190,190,190},{50,50,50,180},{190,190,190,190}},{{180,180,180,180},{190,190,190,190},{180,180,180,180},{110,110,110,110}}}},{{{{360,340,370,340},{270,310,340,310},{190,230,260,230},{270,310,340,310}},{{270,310,340,310},{270,310,400,310},{270,310,340,310},{270,310,400,310}},{{360,270,300,270},{270,310,340,310},{140,180,340,180},{270,310,340,310}},{{270,310,340,310},{270,310,400,310},{270,310,340,310},{330,310,340,310}}},{{{340,340,340,340},{310,310,310,310},{230,290,230,290},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}},{{270,330,270,330},{310,310,310,310},{180,180,180,180},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}}},{{{220,340,210,340},{170,310,180,310},{20,230,230,230},{170,310,180,310}},{{170,310,180,310},{230,310,180,310},{170,310,180,310},{230,310,180,310}},{{130,270,270,270},{170,310,180,310},{170,180,310,180},{170,310,180,310}},{{170,310,180,310},{230,310,180,310},{170,310,180,310},{170,310,310,310}}},{{{340,340,340,400},{310,310,310,310},{230,290,230,230},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}},{{270,330,270,270},{310,310,310,310},{180,180,180,310},{310,310,310,310}},{{310,310,310,310},{310,310,310,310},{310,310,310,310},{310,310,310,310}}}},{{{{210,240,270,240},{240,280,310,280},{110,140,170,140},{240,280,310,280}},{{240,280,310,280},{240,280,370,280},{240,280,310,280},{240,280,370,280}},{{270,310,340,310},{240,280,310,280},{110,150,310,150},{240,280,310,280}},{{240,280,310,280},{240,280,370,280},{240,280,310,280},{300,280,310,280}}},{{{240,240,240,240},{280,280,280,280},{140,200,140,200},{280,280,280,280}},{{280,280,280,280},{280,280,280,280},{280,280,280,280},{280,280,280,280}},{{310,370,310,370},{280,280,280,280},{150,150,150,150},{280,280,280,280}},{{280,280,280,280},{280,280,280,280},{280,280,280,280},{280,280,280,280}}},{{{110,240,110,240},{140,280,150,280},{10,140,140,140},{140,280,150,280}},{{140,280,150,280},{200,280,150,280},{140,280,150,280},{200,280,150,280}},{{170,310,310,310},{140,280,150,280},{140,150,280,150},{140,280,150,280}},{{140,280,150,280},{200,280,150,280},{140,280,150,280},{140,280,280,280}}},{{{240,240,240,300},{280,280,280,280},{140,200,140,140},{280,280,280,280}},{{280,280,280,280},{280,280,280,280},{280,280,280,280},{280,280,280,280}},{{310,370,310,310},{280,280,280,280},{150,150,150,280},{280,280,280,280}},{{280,280,280,280},{280,280,280,280},{280,280,280,280},{280,280,280,280}}}},{{{{240,280,310,280},{220,250,280,250},{120,150,180,150},{220,250,280,250}},{{230,260,290,260},{220,260,350,260},{230,260,290,260},{220,260,350,260}},{{190,220,250,220},{220,250,280,250},{70,100,260,100},{220,250,280,250}},{{230,260,290,260},{220,260,350,260},{230,260,290,260},{190,170,200,170}}},{{{280,280,280,280},{250,250,250,250},{150,210,150,210},{250,250,250,250}},{{260,260,260,260},{260,260,260,260},{260,260,260,260},{260,260,260,260}},{{220,280,220,280},{250,250,250,250},{100,100,100,100},{250,250,250,250}},{{260,260,260,260},{260,260,260,260},{260,260,260,260},{170,170,170,170}}},{{{140,280,150,280},{120,250,120,250},{20,150,150,150},{120,250,120,250}},{{130,260,130,260},{180,260,130,260},{130,260,130,260},{180,260,130,260}},{{90,220,220,220},{120,250,120,250},{100,100,230,100},{120,250,120,250}},{{130,260,130,260},{180,260,130,260},{130,260,130,260},{30,170,170,170}}},{{{280,280,280,340},{250,250,250,250},{150,210,150,150},{250,250,250,250}},{{260,260,260,260},{260,260,260,260},{260,260,260,260},{260,260,260,260}},{{220,280,220,220},{250,250,250,250},{100,100,100,230},{250,250,250,250}},{{260,260,260,260},{260,260,260,260},{260,260,260,260},{170,170,170,170}}}},{{{{240,280,310,280},{200,230,260,230},{140,170,200,170},{200,230,260,230}},{{240,280,310,280},{240,280,370,280},{240,280,310,280},{240,280,370,280}},{{150,180,210,180},{200,230,260,230},{90,120,280,120},{200,230,260,230}},{{240,280,310,280},{210,250,340,250},{240,280,310,280},{220,190,220,190}}},{{{280,280,280,280},{230,230,230,230},{170,230,170,230},{230,230,230,230}},{{280,280,280,280},{280,280,280,280},{280,280,280,280},{280,280,280,280}},{{180,240,180,240},{230,230,230,230},{120,120,120,120},{230,230,230,230}},{{280,280,280,280},{250,250,250,250},{280,280,280,280},{190,190,190,190}}},{{{140,280,150,280},{100,230,100,230},{40,170,170,170},{100,230,100,230}},{{140,280,150,280},{200,280,150,280},{140,280,150,280},{200,280,150,280}},{{50,180,180,180},{100,230,100,230},{120,120,250,120},{100,230,100,230}},{{140,280,150,280},{170,250,120,250},{140,280,150,280},{60,190,190,190}}},{{{280,280,280,340},{230,230,230,230},{170,230,170,170},{230,230,230,230}},{{280,280,280,280},{280,280,280,280},{280,280,280,280},{280,280,280,280}},{{180,240,180,180},{230,230,230,230},{120,120,120,250},{230,230,230,230}},{{280,280,280,280},{250,250,250,250},{280,280,280,280},{190,190,190,190}}}}},{{{{{200,180,140,180},{190,180,140,180},{100,90,50,90},{190,180,140,180}},{{240,220,180,220},{220,210,230,210},{240,220,180,220},{210,190,210,190}},{{100,90,50,90},{190,180,140,180},{50,30,120,30},{190,180,140,180}},{{240,220,180,220},{210,190,210,190},{240,220,180,220},{180,100,60,100}}},{{{170,180,170,180},{170,170,170,170},{80,140,80,140},{170,170,170,170}},{{210,220,210,220},{200,200,200,200},{210,220,210,220},{180,190,180,190}},{{80,140,80,140},{170,170,170,170},{20,30,20,30},{170,170,170,170}},{{210,220,210,220},{180,190,180,190},{210,220,210,220},{90,100,90,100}}},{{{70,180,20,180},{70,180,20,180},{-20,90,60,90},{70,180,20,180}},{{110,220,60,220},{160,210,50,210},{110,220,60,220},{140,190,30,190}},{{-20,90,60,90},{70,180,20,180},{50,30,130,30},{70,180,20,180}},{{110,220,60,220},{140,190,30,190},{110,220,60,220},{0,100,70,100}}},{{{170,180,170,150},{170,170,170,80},{80,140,80,0},{170,170,170,80}},{{210,220,210,130},{200,200,200,110},{210,220,210,130},{180,190,180,100}},{{80,140,80,0},{170,170,170,80},{20,30,20,70},{170,170,170,80}},{{210,220,210,130},{180,190,180,100},{210,220,210,130},{90,100,90,10}}}},{{{{210,200,160,200},{190,170,130,170},{10,0,-40,0},{190,170,130,170}},{{180,170,130,170},{190,170,190,170},{180,170,130,170},{190,170,190,170}},{{70,60,20,60},{190,170,130,170},{50,30,120,30},{190,170,130,170}},{{180,170,130,170},{190,180,200,180},{180,170,130,170},{170,100,60,100}}},{{{190,190,190,190},{160,170,160,170},{-10,50,-10,50},{160,170,160,170}},{{160,160,160,160},{160,170,160,170},{160,160,160,160},{160,170,160,170}},{{50,110,50,110},{160,170,160,170},{20,30,20,30},{160,170,160,170}},{{160,160,160,160},{170,170,170,170},{160,160,160,160},{90,90,90,90}}},{{{90,200,40,200},{60,170,10,170},{-110,0,-30,0},{60,170,10,170}},{{60,170,10,170},{120,170,10,170},{60,170,10,170},{120,170,10,170}},{{-50,60,30,60},{60,170,10,170},{50,30,130,30},{60,170,10,170}},{{60,170,10,170},{130,180,20,180},{60,170,10,170},{-10,100,70,100}}},{{{190,190,190,160},{160,170,160,80},{-10,50,-10,-100},{160,170,160,80}},{{160,160,160,70},{160,170,160,80},{160,160,160,70},{160,170,160,80}},{{50,110,50,-30},{160,170,160,80},{20,30,20,70},{160,170,160,80}},{{160,160,160,70},{170,170,170,80},{160,160,160,70},{90,90,90,0}}}},{{{{340,330,290,330},{310,290,250,290},{230,210,170,210},{310,290,250,290}},{{310,290,250,290},{310,290,310,290},{310,290,250,290},{310,290,310,290}},{{270,250,210,250},{310,290,250,290},{180,160,250,160},{310,290,250,290}},{{310,290,250,290},{310,290,310,290},{310,290,250,290},{370,290,250,290}}},{{{320,320,320,320},{280,290,280,290},{200,270,200,270},{280,290,280,290}},{{280,290,280,290},{280,290,280,290},{280,290,280,290},{280,290,280,290}},{{240,310,240,310},{280,290,280,290},{150,160,150,160},{280,290,280,290}},{{280,290,280,290},{280,290,280,290},{280,290,280,290},{280,290,280,290}}},{{{220,330,170,330},{180,290,130,290},{100,210,180,210},{180,290,130,290}},{{180,290,130,290},{240,290,130,290},{180,290,130,290},{240,290,130,290}},{{140,250,220,250},{180,290,130,290},{180,160,260,160},{180,290,130,290}},{{180,290,130,290},{240,290,130,290},{180,290,130,290},{180,290,260,290}}},{{{320,320,320,290},{280,290,280,200},{200,270,200,120},{280,290,280,200}},{{280,290,280,200},{280,290,280,200},{280,290,280,200},{280,290,280,200}},{{240,310,240,160},{280,290,280,200},{150,160,150,200},{280,290,280,200}},{{280,290,280,200},{280,290,280,200},{280,290,280,200},{280,290,280,200}}}},{{{{240,230,190,230},{280,260,220,260},{140,130,90,130},{280,260,220,260}},{{280,260,220,260},{280,260,280,260},{280,260,220,260},{280,260,280,260}},{{310,290,250,290},{280,260,220,260},{150,130,220,130},{280,260,220,260}},{{280,260,220,260},{280,260,280,260},{280,260,220,260},{340,260,220,260}}},{{{220,220,220,220},{250,260,250,260},{120,180,120,180},{250,260,250,260}},{{250,260,250,260},{250,260,250,260},{250,260,250,260},{250,260,250,260}},{{280,350,280,350},{250,260,250,260},{120,130,120,130},{250,260,250,260}},{{250,260,250,260},{250,260,250,260},{250,260,250,260},{250,260,250,260}}},{{{120,230,70,230},{150,260,100,260},{20,130,100,130},{150,260,100,260}},{{150,260,100,260},{210,260,100,260},{150,260,100,260},{210,260,100,260}},{{180,290,260,290},{150,260,100,260},{150,130,230,130},{150,260,100,260}},{{150,260,100,260},{210,260,100,260},{150,260,100,260},{150,260,230,260}}},{{{220,220,220,190},{250,260,250,170},{120,180,120,30},{250,260,250,170}},{{250,260,250,170},{250,260,250,170},{250,260,250,170},{250,260,250,170}},{{280,350,280,200},{250,260,250,170},{120,130,120,170},{250,260,250,170}},{{250,260,250,170},{250,260,250,170},{250,260,250,170},{250,260,250,170}}}},{{{{280,260,220,260},{250,240,200,240},{150,140,100,140},{250,240,200,240}},{{260,250,210,250},{260,240,260,240},{260,250,210,250},{260,240,260,240}},{{220,210,170,210},{250,240,200,240},{100,90,180,90},{250,240,200,240}},{{260,250,210,250},{260,240,260,240},{260,250,210,250},{230,150,110,150}}},{{{250,260,250,260},{230,230,230,230},{130,190,130,190},{230,230,230,230}},{{240,240,240,240},{230,240,230,240},{240,240,240,240},{230,240,230,240}},{{200,260,200,260},{230,230,230,230},{80,80,80,80},{230,230,230,230}},{{240,240,240,240},{230,240,230,240},{240,240,240,240},{140,150,140,150}}},{{{150,260,100,260},{130,240,80,240},{30,140,110,140},{130,240,80,240}},{{140,250,90,250},{190,240,80,240},{140,250,90,250},{190,240,80,240}},{{100,210,180,210},{130,240,80,240},{110,90,190,90},{130,240,80,240}},{{140,250,90,250},{190,240,80,240},{140,250,90,250},{40,150,120,150}}},{{{250,260,250,230},{230,230,230,140},{130,190,130,40},{230,230,230,140}},{{240,240,240,150},{230,240,230,150},{240,240,240,150},{230,240,230,150}},{{200,260,200,110},{230,230,230,140},{80,80,80,120},{230,230,230,140}},{{240,240,240,150},{230,240,230,150},{240,240,240,150},{140,150,140,60}}}},{{{{280,260,220,260},{230,220,180,220},{170,160,120,160},{230,220,180,220}},{{280,260,220,260},{280,260,280,260},{280,260,220,260},{280,260,280,260}},{{180,170,130,170},{230,220,180,220},{120,110,200,110},{230,220,180,220}},{{280,260,220,260},{250,230,250,230},{280,260,220,260},{250,180,140,180}}},{{{250,260,250,260},{210,210,210,210},{150,210,150,210},{210,210,210,210}},{{250,260,250,260},{250,260,250,260},{250,260,250,260},{250,260,250,260}},{{160,220,160,220},{210,210,210,210},{100,100,100,100},{210,210,210,210}},{{250,260,250,260},{220,230,220,230},{250,260,250,260},{170,170,170,170}}},{{{150,260,100,260},{110,220,60,220},{50,160,130,160},{110,220,60,220}},{{150,260,100,260},{210,260,100,260},{150,260,100,260},{210,260,100,260}},{{60,170,140,170},{110,220,60,220},{130,110,210,110},{110,220,60,220}},{{150,260,100,260},{180,230,70,230},{150,260,100,260},{70,180,150,180}}},{{{250,260,250,230},{210,210,210,120},{150,210,150,60},{210,210,210,120}},{{250,260,250,170},{250,260,250,170},{250,260,250,170},{250,260,250,170}},{{160,220,160,70},{210,210,210,120},{100,100,100,140},{210,210,210,120}},{{250,260,250,170},{220,230,220,140},{250,260,250,170},{170,170,170,80}}}}},{{{{{200,200,100,200},{190,190,100,190},{100,100,10,100},{190,190,100,190}},{{240,240,140,240},{220,220,190,220},{240,240,140,240},{210,210,170,210}},{{100,100,10,100},{190,190,100,190},{50,50,80,50},{190,190,100,190}},{{240,240,140,240},{210,210,170,210},{240,240,140,240},{180,120,20,120}}},{{{150,200,150,170},{150,190,150,160},{60,160,60,130},{150,190,150,160}},{{190,240,190,210},{180,220,180,190},{190,240,190,210},{160,210,160,180}},{{60,160,60,130},{150,190,150,160},{0,50,0,20},{150,190,150,160}},{{190,240,190,210},{160,210,160,180},{190,240,190,210},{70,120,70,90}}},{{{90,200,40,200},{90,190,40,190},{0,100,80,100},{90,190,40,190}},{{130,240,80,240},{180,220,70,220},{130,240,80,240},{160,210,50,210}},{{0,100,80,100},{90,190,40,190},{70,50,150,50},{90,190,40,190}},{{130,240,80,240},{160,210,50,210},{130,240,80,240},{10,120,90,120}}},{{{150,200,150,170},{150,190,150,110},{60,160,60,20},{150,190,150,110}},{{190,240,190,150},{180,220,180,140},{190,240,190,150},{160,210,160,120}},{{60,160,60,20},{150,190,150,110},{0,50,0,90},{150,190,150,110}},{{190,240,190,150},{160,210,160,120},{190,240,190,150},{70,120,70,30}}}},{{{{210,210,120,210},{190,190,90,190},{10,10,-80,10},{190,190,90,190}},{{180,180,90,180},{190,190,150,190},{180,180,90,180},{190,190,150,190}},{{70,70,-20,70},{190,190,90,190},{50,50,80,50},{190,190,90,190}},{{180,180,90,180},{190,190,160,190},{180,180,90,180},{170,110,20,110}}},{{{170,210,170,180},{140,190,140,160},{-30,70,-30,40},{140,190,140,160}},{{140,180,140,150},{140,190,140,160},{140,180,140,150},{140,190,140,160}},{{30,130,30,100},{140,190,140,160},{0,50,0,20},{140,190,140,160}},{{140,180,140,150},{150,190,150,160},{140,180,140,150},{70,110,70,80}}},{{{110,210,60,210},{80,190,30,190},{-90,10,-10,10},{80,190,30,190}},{{80,180,30,180},{140,190,30,190},{80,180,30,180},{140,190,30,190}},{{-30,70,50,70},{80,190,30,190},{70,50,150,50},{80,190,30,190}},{{80,180,30,180},{150,190,40,190},{80,180,30,180},{10,110,90,110}}},{{{170,210,170,190},{140,190,140,100},{-30,70,-30,-70},{140,190,140,100}},{{140,180,140,100},{140,190,140,100},{140,180,140,100},{140,190,140,100}},{{30,130,30,-10},{140,190,140,100},{0,50,0,90},{140,190,140,100}},{{140,180,140,100},{150,190,150,110},{140,180,140,100},{70,110,70,30}}}},{{{{340,340,250,340},{310,310,210,310},{230,230,130,230},{310,310,210,310}},{{310,310,210,310},{310,310,270,310},{310,310,210,310},{310,310,270,310}},{{270,270,170,270},{310,310,210,310},{180,180,210,180},{310,310,210,310}},{{310,310,210,310},{310,310,270,310},{310,310,210,310},{370,310,210,310}}},{{{300,340,300,310},{260,310,260,280},{180,290,180,260},{260,310,260,280}},{{260,310,260,280},{260,310,260,280},{260,310,260,280},{260,310,260,280}},{{220,330,220,300},{260,310,260,280},{130,180,130,150},{260,310,260,280}},{{260,310,260,280},{260,310,260,280},{260,310,260,280},{260,310,260,280}}},{{{240,340,190,340},{200,310,150,310},{120,230,200,230},{200,310,150,310}},{{200,310,150,310},{260,310,150,310},{200,310,150,310},{260,310,150,310}},{{160,270,240,270},{200,310,150,310},{200,180,280,180},{200,310,150,310}},{{200,310,150,310},{260,310,150,310},{200,310,150,310},{200,310,280,310}}},{{{300,340,300,320},{260,310,260,220},{180,290,180,140},{260,310,260,220}},{{260,310,260,220},{260,310,260,220},{260,310,260,220},{260,310,260,220}},{{220,330,220,180},{260,310,260,220},{130,180,130,220},{260,310,260,220}},{{260,310,260,220},{260,310,260,220},{260,310,260,220},{260,310,260,220}}}},{{{{240,240,150,240},{280,280,180,280},{140,140,50,140},{280,280,180,280}},{{280,280,180,280},{280,280,240,280},{280,280,180,280},{280,280,240,280}},{{310,310,210,310},{280,280,180,280},{150,150,180,150},{280,280,180,280}},{{280,280,180,280},{280,280,240,280},{280,280,180,280},{340,280,180,280}}},{{{200,240,200,210},{230,280,230,250},{100,200,100,170},{230,280,230,250}},{{230,280,230,250},{230,280,230,250},{230,280,230,250},{230,280,230,250}},{{260,370,260,340},{230,280,230,250},{100,150,100,120},{230,280,230,250}},{{230,280,230,250},{230,280,230,250},{230,280,230,250},{230,280,230,250}}},{{{140,240,90,240},{170,280,120,280},{40,140,120,140},{170,280,120,280}},{{170,280,120,280},{230,280,120,280},{170,280,120,280},{230,280,120,280}},{{200,310,280,310},{170,280,120,280},{170,150,250,150},{170,280,120,280}},{{170,280,120,280},{230,280,120,280},{170,280,120,280},{170,280,250,280}}},{{{200,240,200,220},{230,280,230,190},{100,200,100,60},{230,280,230,190}},{{230,280,230,190},{230,280,230,190},{230,280,230,190},{230,280,230,190}},{{260,370,260,220},{230,280,230,190},{100,150,100,190},{230,280,230,190}},{{230,280,230,190},{230,280,230,190},{230,280,230,190},{230,280,230,190}}}},{{{{280,280,180,280},{250,250,160,250},{150,150,60,150},{250,250,160,250}},{{260,260,170,260},{260,260,220,260},{260,260,170,260},{260,260,220,260}},{{220,220,130,220},{250,250,160,250},{100,100,140,100},{250,250,160,250}},{{260,260,170,260},{260,260,220,260},{260,260,170,260},{230,170,70,170}}},{{{230,280,230,250},{210,250,210,220},{110,210,110,180},{210,250,210,220}},{{220,260,220,230},{210,260,210,230},{220,260,220,230},{210,260,210,230}},{{180,280,180,250},{210,250,210,220},{60,100,60,70},{210,250,210,220}},{{220,260,220,230},{210,260,210,230},{220,260,220,230},{120,170,120,140}}},{{{170,280,120,280},{150,250,100,250},{50,150,130,150},{150,250,100,250}},{{160,260,110,260},{210,260,100,260},{160,260,110,260},{210,260,100,260}},{{120,220,200,220},{150,250,100,250},{130,100,210,100},{150,250,100,250}},{{160,260,110,260},{210,260,100,260},{160,260,110,260},{60,170,140,170}}},{{{230,280,230,250},{210,250,210,170},{110,210,110,70},{210,250,210,170}},{{220,260,220,180},{210,260,210,170},{220,260,220,180},{210,260,210,170}},{{180,280,180,140},{210,250,210,170},{60,100,60,150},{210,250,210,170}},{{220,260,220,180},{210,260,210,170},{220,260,220,180},{120,170,120,80}}}},{{{{280,280,180,280},{230,230,140,230},{170,170,80,170},{230,230,140,230}},{{280,280,180,280},{280,280,240,280},{280,280,180,280},{280,280,240,280}},{{180,180,90,180},{230,230,140,230},{120,120,160,120},{230,230,140,230}},{{280,280,180,280},{250,250,210,250},{280,280,180,280},{250,190,100,190}}},{{{230,280,230,250},{190,230,190,200},{130,230,130,200},{190,230,190,200}},{{230,280,230,250},{230,280,230,250},{230,280,230,250},{230,280,230,250}},{{140,240,140,210},{190,230,190,200},{80,120,80,90},{190,230,190,200}},{{230,280,230,250},{200,250,200,220},{230,280,230,250},{150,190,150,160}}},{{{170,280,120,280},{130,230,80,230},{70,170,150,170},{130,230,80,230}},{{170,280,120,280},{230,280,120,280},{170,280,120,280},{230,280,120,280}},{{80,180,160,180},{130,230,80,230},{150,120,230,120},{130,230,80,230}},{{170,280,120,280},{200,250,90,250},{170,280,120,280},{90,190,170,190}}},{{{230,280,230,250},{190,230,190,150},{130,230,130,90},{190,230,190,150}},{{230,280,230,190},{230,280,230,190},{230,280,230,190},{230,280,230,190}},{{140,240,140,100},{190,230,190,150},{80,120,80,170},{190,230,190,150}},{{230,280,230,190},{200,250,200,160},{230,280,230,190},{150,190,150,110}}}}}}; +static const int ML_BASE=0, ML_closing=930, ML_intern=-90; +static const int NINIO_m=60, NINIO_max=300; +static const int TerminalAU=50; +static const double lxc=107.856; +struct SPtetra{const char* seq;int bonus;}; +static const SPtetra tetra[]={{"CAACGG",550},{"CCAAGG",330},{"CCACGG",370},{"CCCAGG",340},{"CCGAGG",350},{"CCGCGG",360},{"CCUAGG",370},{"CCUCGG",250},{"CUAAGG",360},{"CUACGG",280},{"CUCAGG",370},{"CUCCGG",270},{"CUGCGG",280},{"CUUAGG",350},{"CUUCGG",370},{"CUUUGG",370}}; +static const int tetra_n=16; +struct SPtri{const char* seq;int bonus;}; +static const SPtri tri[]={{"CAACG",680},{"GUUAC",690}}; +static const int tri_n=2; +struct SPhexa{const char* seq;int bonus;}; +static const SPhexa hexa[]={{"ACAGUACU",280},{"ACAGUGAU",360},{"ACAGUGCU",290},{"ACAGUGUU",180}}; +static const int hexa_n=4; +} // namespace t2004 diff --git a/lib/tornadofold/src/tornadofold.h b/lib/tornadofold/src/tornadofold.h new file mode 100644 index 0000000..5227be6 --- /dev/null +++ b/lib/tornadofold/src/tornadofold.h @@ -0,0 +1,393 @@ +// tornadofold — wavefront (anti-diagonal) SIMD folder with exact Turner 2004 energy. +// Matrices stored diagonal-by-diagonal: D[s][i] holds cell (i, i+s). The O(N^3) +// multibranch bifurcation FMbif(i,j)=min_k FM(i,k)+FM1(k+1,j) becomes, per split +// span, a unit-stride vector add + min over i (NEON, 4x int32). +#pragma once +#include "energy.h" +#include +#include +#include +#if (defined(__aarch64__) || defined(__ARM_NEON)) && !defined(DISABLE_NEON) +#include +#define HAVE_NEON 1 +#endif +#if defined(__wasm_simd128__) && !defined(DISABLE_NEON) +#include +#define HAVE_WASM_SIMD 1 +#endif +#if defined(__AVX512F__) && !defined(DISABLE_NEON) +#include +#define HAVE_AVX512 1 +#endif +#if defined(__AVX2__) && !defined(DISABLE_NEON) +#include +#define HAVE_AVX2 1 +#endif +#if defined(__SSE2__) && !defined(DISABLE_NEON) +#include +#define HAVE_SSE2 1 +#endif + +namespace tornadofold { +using en::INF; + +// Lower bound on any single internal-loop closure energy: the most stabilizing +// case is the best nearest-neighbour stack (-3.40 kcal/mol = -340 in centikcal +// units) across the Turner 2004 tables (stack/int11/int21/int22/general all +// >= -340). Used for an exact prune of the O(MAXLOOP^2) internal-loop scan. +static const int SINGLE_LOOP_LB = -340; + +#ifdef HAVE_SSE2 +// SSE2 has no signed 32-bit min (that is SSE4.1); emulate with compare + select. +static inline __m128i sse2_min_epi32(__m128i a, __m128i b) { + __m128i lt = _mm_cmpgt_epi32(b, a); // 0xFFFFFFFF where a < b + return _mm_or_si128(_mm_and_si128(lt, a), _mm_andnot_si128(lt, b)); +} +#endif + +struct TornadoFold { + int n = 0; + std::vector b; + std::string seq; + en::EM em; + // Flat span-major storage: cell (i, i+s) lives at off[s] + i. Replaces + // vector to drop per-row heap overhead and the double-indirection + // in the internal-loop V gather. + std::vector Vf, FMf, FM1f; + // FMbif is not stored as a full O(N^2) matrix: each span's bifurcation row is + // consumed by FM (same span) and by V two spans later, so a 3-row rolling + // buffer (cycled by pointer) is enough. Traceback recomputes it from FM/FM1. + std::vector bifBuf; // three n-length rows, cycled by pointer + int32_t *bifCur, *bifP1, *bifP2; // FMbif rows for span s, s-1, s-2 + std::vector off; // off[s] = flat start of span s; size n+1 + std::vector Erow; + static const int PAD = 8; + + int pt(int i, int j) const { + return em.pt(i, j); + } + int eHairpin(int i, int j) const { + return em.eHairpin(i, j); + } + int eIntLoop(int i, int j, int p, int q) const { + return em.eIntLoop(i, j, p, q); + } + int mlStem(int p, int q) const { + return em.mlStem(p, q); + } + int extStem(int k, int j) const { + return em.extStem(k, j); + } + int mlClose(int i, int j) const { + return em.mlClose(i, j); + } + + void bifKernel(int s) { + int m = n - s; + int32_t* out = bifCur; + for (int i = 0; i < m; ++i) { + out[i] = INF; + } + for (int a = 0; a < s; ++a) { + int bsp = s - 1 - a; + const int32_t* pa = FMf.data() + off[a]; + const int32_t* pb = FM1f.data() + off[bsp] + (a + 1); + int i = 0; +#ifdef HAVE_NEON + for (; i + 4 <= m; i += 4) { + int32x4_t va = vld1q_s32(pa + i), vb = vld1q_s32(pb + i); + int32x4_t sum = vaddq_s32(va, vb); + sum = vminq_s32(sum, vdupq_n_s32(2 * INF)); + int32x4_t cur = vld1q_s32(out + i); + vst1q_s32(out + i, vminq_s32(cur, sum)); + } +#elif defined(HAVE_WASM_SIMD) + for (; i + 4 <= m; i += 4) { + v128_t va = wasm_v128_load(pa + i), vb = wasm_v128_load(pb + i); + v128_t sum = wasm_i32x4_add(va, vb); + sum = wasm_i32x4_min(sum, wasm_i32x4_splat(2 * INF)); + v128_t cur = wasm_v128_load(out + i); + wasm_v128_store(out + i, wasm_i32x4_min(cur, sum)); + } +#elif defined(HAVE_AVX512) + for (; i + 16 <= m; i += 16) { + __m512i va = _mm512_loadu_si512((const void*)(pa + i)); + __m512i vb = _mm512_loadu_si512((const void*)(pb + i)); + __m512i sum = _mm512_add_epi32(va, vb); + sum = _mm512_min_epi32(sum, _mm512_set1_epi32(2 * INF)); + __m512i cur = _mm512_loadu_si512((const void*)(out + i)); + _mm512_storeu_si512((void*)(out + i), _mm512_min_epi32(cur, sum)); + } +#elif defined(HAVE_AVX2) + for (; i + 8 <= m; i += 8) { + __m256i va = _mm256_loadu_si256((const __m256i*)(pa + i)); + __m256i vb = _mm256_loadu_si256((const __m256i*)(pb + i)); + __m256i sum = _mm256_add_epi32(va, vb); + sum = _mm256_min_epi32(sum, _mm256_set1_epi32(2 * INF)); + __m256i cur = _mm256_loadu_si256((const __m256i*)(out + i)); + _mm256_storeu_si256((__m256i*)(out + i), _mm256_min_epi32(cur, sum)); + } +#elif defined(HAVE_SSE2) + for (; i + 4 <= m; i += 4) { + __m128i va = _mm_loadu_si128((const __m128i*)(pa + i)); + __m128i vb = _mm_loadu_si128((const __m128i*)(pb + i)); + __m128i sum = _mm_add_epi32(va, vb); + sum = sse2_min_epi32(sum, _mm_set1_epi32(2 * INF)); + __m128i cur = _mm_loadu_si128((const __m128i*)(out + i)); + _mm_storeu_si128((__m128i*)(out + i), sse2_min_epi32(cur, sum)); + } +#endif + // scalar tail (SIMD remainder), or the whole span when SIMD is off + for (; i < m; ++i) { + int va = pa[i], vb = pb[i]; + if (va >= INF || vb >= INF) { + continue; + } + int sm = va + vb; + if (sm < out[i]) { + out[i] = sm; + } + } + } + } + + int fold(const std::string& str) { + seq = str; + n = (int)str.size(); + b.resize(n); + for (int i = 0; i < n; ++i) { + char c = str[i]; + b[i] = (c == 'A') ? 0 + : (c == 'C') ? 1 + : (c == 'G') ? 2 + : ((c == 'U' || c == 'T') ? 3 : 4); + } + em.set(seq, b.data(), n); + off.resize(n + 1); + off[0] = 0; + for (int s = 0; s < n; ++s) { + off[s + 1] = off[s] + (n - s); + } + const int tot = off[n]; + Vf.assign(tot + PAD, INF); + FMf.assign(tot + PAD, INF); + FM1f.assign(tot + PAD, INF); + bifBuf.assign(3 * (size_t)n + PAD, INF); + bifCur = bifBuf.data(); + bifP1 = bifBuf.data() + n; + bifP2 = bifBuf.data() + 2 * (size_t)n; + const int MLB = t2004::ML_BASE; + for (int s = 1; s < n; ++s) { + int m = n - s; + for (int i = 0; i < m; ++i) { + int j = i + s, p = pt(i, j), best = INF; + if (p && s >= 4) { + best = eHairpin(i, j); + en::EM::IntPre pre = em.intPre(i, j); // (i,j)-invariant, hoisted + int maxp = std::min(j - 1, i + 31); + for (int a = i + 1; a <= maxp; ++a) { + int n1 = a - i - 1; + int minq = std::max(a + 1, j - 1 - (30 - n1)); + for (int c = j - 1; c >= minq; --c) { + // minq bounds n1+n2 <= 30, so no explicit MAXLOOP check needed + int v2 = Vf[off[c - a] + a]; + if (v2 >= INF) { + continue; + } + // Exact lower-bound prune: eIntLoop >= SINGLE_LOOP_LB, + // so if v2 + SINGLE_LOOP_LB already >= best this enclosed + // pair cannot improve V(i,j); skip its energy evaluation. + if (v2 + SINGLE_LOOP_LB >= best) { + continue; + } + int e = em.eIntLoop(pre, i, j, a, c); + if (e < INF && e + v2 < best) { + best = e + v2; + } + } + } + if (s - 2 >= 1) { + int bif = bifP2[i + 1]; + if (bif < INF) { + int cl = mlClose(i, j); + if (bif + cl < best) { + best = bif + cl; + } + } + } + } + Vf[off[s] + i] = best; + } + for (int i = 0; i < m; ++i) { + int j = i + s, f1 = INF; + if (pt(i, j) && Vf[off[s] + i] < INF) { + f1 = Vf[off[s] + i] + mlStem(i, j); + } + int ext = FM1f[off[s - 1] + i]; + if (ext < INF && ext + MLB < f1) { + f1 = ext + MLB; + } + FM1f[off[s] + i] = f1; + } + bifKernel(s); + for (int i = 0; i < m; ++i) { + int fm = FM1f[off[s] + i]; + int u = FMf[off[s - 1] + (i + 1)]; + if (u < INF && u + MLB < fm) { + fm = u + MLB; + } + int bv = bifCur[i]; + if (bv < fm) { + fm = bv; + } + FMf[off[s] + i] = fm; + } + // cycle rolling FMbif rows: cur -> P1 -> P2 -> recycled -> cur + int32_t* t = bifCur; + bifCur = bifP2; + bifP2 = bifP1; + bifP1 = t; + } + Erow.assign(n, 0); + for (int j = 0; j < n; ++j) { + int e = (j > 0) ? Erow[j - 1] : 0; + for (int k = 0; k <= j; ++k) { + int v = Vf[off[j - k] + k]; + if (v >= INF) { + continue; + } + int pre = (k > 0) ? Erow[k - 1] : 0; + int cand = pre + v + extStem(k, j); + if (cand < e) { + e = cand; + } + } + Erow[j] = e; + } + return n ? Erow[n - 1] : 0; + } + + int getV(int i, int j) const { + return Vf[off[j - i] + i]; + } + int getFM(int i, int j) const { + return FMf[off[j - i] + i]; + } + int getFM1(int i, int j) const { + return FM1f[off[j - i] + i]; + } + int getFMbif(int i, int j) const { + int v = INF; + for (int k = i; k < j; ++k) { + int a = getFM(i, k), c = getFM1(k + 1, j); + if (a < INF && c < INF && a + c < v) { + v = a + c; + } + } + return v; + } + + std::string traceback(int) { + std::string dot(n, '.'); + int j = n - 1; + while (j >= 0) { + int e = Erow[j]; + if (j > 0 && Erow[j - 1] == e) { + --j; + continue; + } + bool found = false; + for (int k = 0; k <= j; ++k) { + int v = getV(k, j); + if (v >= INF) { + continue; + } + int pre = (k > 0) ? Erow[k - 1] : 0; + if (pre + v + extStem(k, j) == e) { + dot[k] = '('; + dot[j] = ')'; + traceV(k, j, dot); + j = k - 1; + found = true; + break; + } + } + if (!found) { + --j; + } + } + return dot; + } + void traceV(int i, int j, std::string& dot) { + int v = getV(i, j); + if (eHairpin(i, j) == v) { + return; + } + int maxp = std::min(j - 1, i + 31); + for (int a = i + 1; a <= maxp; ++a) { + int n1 = a - i - 1; + int minq = std::max(a + 1, j - 1 - (30 - n1)); + for (int c = j - 1; c >= minq; --c) { + // minq bounds n1+n2 <= 30 (see fold()) + int v2 = getV(a, c); + if (v2 >= INF) { + continue; + } + int e = eIntLoop(i, j, a, c); + if (e < INF && e + v2 == v) { + dot[a] = '('; + dot[c] = ')'; + traceV(a, c, dot); + return; + } + } + } + if ((j - 1) - (i + 1) >= 1) { + int cl = mlClose(i, j); + if (getFMbif(i + 1, j - 1) + cl == v) { + traceFMbif(i + 1, j - 1, dot); + return; + } + } + } + void traceFMbif(int i, int j, std::string& dot) { + int val = getFMbif(i, j); + for (int k = i; k < j; ++k) { + int a = getFM(i, k), c = getFM1(k + 1, j); + if (a >= INF || c >= INF) { + continue; + } + if (a + c == val) { + traceFM(i, k, dot); + traceFM1(k + 1, j, dot); + return; + } + } + } + void traceFM1(int i, int j, std::string& dot) { + int f1 = getFM1(i, j); + if (pt(i, j) && getV(i, j) < INF && getV(i, j) + mlStem(i, j) == f1) { + dot[i] = '('; + dot[j] = ')'; + traceV(i, j, dot); + return; + } + if (j - 1 >= i && getFM1(i, j - 1) < INF && getFM1(i, j - 1) + t2004::ML_BASE == f1) { + traceFM1(i, j - 1, dot); + } + } + void traceFM(int i, int j, std::string& dot) { + int fm = getFM(i, j); + if (getFM1(i, j) == fm) { + traceFM1(i, j, dot); + return; + } + if (i + 1 <= j && getFM(i + 1, j) < INF && getFM(i + 1, j) + t2004::ML_BASE == fm) { + traceFM(i + 1, j, dot); + return; + } + traceFMbif(i, j, dot); + } +}; + +} // namespace tornadofold diff --git a/lib/tornadofold/src/verify.cpp b/lib/tornadofold/src/verify.cpp new file mode 100644 index 0000000..8f643d4 --- /dev/null +++ b/lib/tornadofold/src/verify.cpp @@ -0,0 +1,34 @@ +// Self-consistency test: for random sequences the DP optimum must equal the +// energy of its own traceback structure re-scored by the energy model. +#include "tornadofold.h" +#include +#include + +int main() { + std::mt19937 rng(42); + const char* B = "ACGU"; + std::uniform_int_distribution d(0, 3); + int fails = 0, tot = 0; + for (int L : {20, 35, 50, 80, 120, 200}) { + for (int t = 0; t < 40; ++t) { + std::string s; + for (int i = 0; i < L; ++i) { + s += B[d(rng)]; + } + tornadofold::TornadoFold f; + int e = f.fold(s); + std::string db = f.traceback(e); + int e2 = f.em.evalStructure(db); // re-score the traceback + tot++; + if (e != e2) { + if (fails < 8) { + printf("MISMATCH L=%d dp=%d eval=%d\n%s\n%s\n", L, e, e2, s.c_str(), + db.c_str()); + } + fails++; + } + } + } + printf("verify: %d/%d consistent (%d mismatches)\n", tot - fails, tot, fails); + return fails ? 1 : 0; +} diff --git a/lib/tornadofold/tests/data/archiveii_smoke.tsv b/lib/tornadofold/tests/data/archiveii_smoke.tsv new file mode 100644 index 0000000..c352c73 --- /dev/null +++ b/lib/tornadofold/tests/data/archiveii_smoke.tsv @@ -0,0 +1,25 @@ +name seq ref_db mfe db +srp_Shig.flex._CP000266 CCGUCAGGUCCGGAAGGAAGCAGCGGUA ((((....(((....)))....)))).. -8.30 ((((....(((....)))....)))).. +srp_Baci.amyl._CP000560 AACCAUGUCAGGUCCGGAAGGAAGCAGCAU ....((((....(((....)))....)))) -5.00 ....((((....(((....)))....)))) +srp_Erwi.caro._BX950851 AACCUGGUCAGGCCCGGAAGGGAGCAGCCA ....((((....(((....)))....)))) -8.60 ....((((....(((....)))....)))) +srp_List.wels._AM263198 AACCAUGUCAGGUCCGGAAGGAAGCAGCAU ....((((....(((....)))....)))) -5.00 ....((((....(((....)))....)))) +srp_Lact.sali._CP000233 CGUGUCAGGUCCGGAAGGAAGCAGCACUAAG .((((....(((....)))....)))).... -7.70 .((((....(((....)))....)))).... +srp_Dein.geot._CP000359 UGAACCUGGUCAGGGCCGGAAGGCAGCAGCCA ......((((....(((....)))....)))) -9.50 .....(((((....)))))..(((....))). +srp_Haem.infl._L42023 AACCUGGUCAGAGCCGGAAGGCAGCAGCCAUA ....((((....(((....)))....)))).. -9.50 ...(((((....)))))..(((....)))... +srp_Brad.japo._BA000040 AACCGGGUCAGGUCCGGAAGGAAGCAGCCCUAA ....((((....(((....)))....))))... -10.00 ....((((....(((....)))....))))... +srp_Brad.spec._CP000494 AACCGGGUCAGGUCCGGAAGGAAGCAGCCCUAA ....((((....(((....)))....))))... -10.00 ....((((....(((....)))....))))... +srp_Bruc.abor._AE017223 AACCGGGUCAGGUCCGGAAGGAAGCAGCCCUAA ....((((....(((....)))....))))... -10.00 ....((((....(((....)))....))))... +srp_Aqui.aeol._AE000759 AUUAGCCCUGCGGCGGGACAGGGUGAACUCCCCCAGGCCCGAAAGGGAGCAAGGGUAAGCCCGCCGUCCCGUGCGCAGGGUCCUAAAACAA ....(((((((((((((...((((.....(((.....(((....))).....)))...)))).....))))).)))))))).......... -46.60 ....(((((((((((((((..((((..((..(((..((((.....)).))..)))..))..))))))))))).)))))))).......... +srp_Camp.jeju._CP000025 UUCUUAGACCUGUGCAAUGCCGUUUAUGAGCACCGCUUCAGGGUGGGAACACAGCAGAGCACUUGUUUAUGGUGUGUGCCGCAGUUAUCUG ....(.(..((((((.(((((((..(((((....((((....(((....)))....)))).)))))..))))))))...))))).....)) -25.90 ........((((.((..((((......).)))..))..))))((((..(((((.((((((....)))).)).))))).))))......... +srp_Cory.jeik._CR931997 GCUCCCCACGUGGCGCCAUCCAGGCCAACUCCCCCAGGGUGGAAACGCAGCAAGGGUAACCGGGCUCUGGCGGGUGCGUGGGGGGUCUUUU ((.((((((((..(((((.((.((......(((.....(((....))).....)))...)).))...))))).)..))))))).))..... -45.00 (((((((((...(((((..(((((((......(((..(.((......)).)..)))......))).))))..))))))))))))))..... +srp_Fran.alni._CT573213 GGGGACCCCGCGCACCCGACAGAGCCCGUUGACCCUUGCUGCCUUCCAGCCCUGGGGGAGUUCACAGGAUAGACGCCGCGCGGGGUCCACC ((((.(...(.(...(.(.....(.(....).).....).).).)...)))))...................................... -40.70 (((((((((((((...((((.......)))).(((..((((.....))))...)))((.(((.........))).))))))))))))).)) +srp_Geob.meta._CP000148 CGGGAGCCGGGCGGCGGUAACGCUGCCAACCCCGCCAGGUCCGAAAGGAAGCAACGGUAACAGUUGUUGCCGGGUGUCCGGCUCCCUGAAG .((((((((.((..((((((.((((......(((.....(((....))).....)))...))))..)))))).))...))))))))..... -47.40 .(((((((((((((((((...))))))....((....))(((....))).....(((((((....)))))))...)))))))))))..... +16s_T.maritima_domain4 ACCGCCCGUCACGCCACCCGAGUCGGGGGCUCCCGAAGACACCUACCCCAACCCGAAAGGGAGGGGGGGUGUCGAGGGAGAACCUGGCGAGGGGGGCGAAGUCGUAACAAGGUAGCCGUACCGGAAGGUGCGGCUGGAUCACCUCCUUUCU .(.(..((...((((.(((..((((((..((((....(((((((.((((..(((....))).)))))))))))...))))..))))))..))).))))....))...)..).((((((((((....))))))))))............... -83.70 (((...((.(.((((.(((..((((((..(((((...(((((((.((((..(((....))).)))))))))))..)))))..))))))..))).))))..).))......)))(((((((((....)))))))))(((.....)))..... +srp_Meth.ferv._S49762 AGGCUAGGCCGGGGGGUUAGGGGUCCCCUGUAAGCGCAAAUCCCCUAUAUGGCGCGGCCGAAGCCCAGGAGGCGGCAAGACCGCCAGACAUCGGCCUGAGGGUUAAACAAUGAAGCCUCGUCCCACAGGGCCACCGGUGGCGAGGGUCCAGCUG ......(((((((((((.....)))))))....((((.(..........).))))))))............................................................................................... -60.10 .((((.(((((.(....((((((.....(((....)))...)))))).....).)))))..))))(((..(((((.....))))).......((..(((((.((........)).)))))..))...(((((..((....))..)))))..))) +srp_Anto.locu._GSP-278021 AUGCAGUCACCGCACCUCUGUGGAUGCCGCCAUGCCAGAAGCAGUGCUGUGUAGGCAGCGCCUGUGGUGGCUUAAAUGGGUGAACGGGCCAGGGCGGUAACGCAGCAACCAUAAGCCCAGUGGCACGCAGUCUGCAGUCCCGUGCGUAGCAUUUU .....((.(.(((((((..((((.(((.(((((((((...((((((((((....))))))).)))..))))(...)(((((.....((.....(((....))).....))....))))))))))..)))..))))))....)))))).))..... -58.30 ((((......(((((..((((((((((.(((((((((...((((((((((....)))))).))))..)))).....(((((....((......(((....))).....))....))))))))))..))).)))))))....)))))..))))... +srp_Ajel.caps._CV620298 GGGGCAACUUCUUCUCCCGCACUCGUGCGAGUAGUCCACGACCGGCUGUCGGUGCGCUAAGCCCUGGCCCGUACCCUCGAGGGAAGCGAUUCUGCAGAGACCGGGCCCCCCGGCGAUGGCGGCACCAGAUCACACCGCCGCUGCACACCCACCGCGCU ((..(...((.(.(.((.(((.(((((.((....))))))).....))).)).)..(...)....((((((....(((....(((....)))....)))..)))))).....).))....)...))................................ -53.80 ((((..........))))......(((((.((..........(((((((((((((((((.(((..((((((....(((...((((....))))...)))..))))))....)))..)))).))))).)).))....))))..........))))))). +srp_Aspe.oryz._AP007169 UUAAUCUGUCAUGGAUAACCCAGUGGGAGGUCAGCCGCUAAGACGAUAACCUUGCUUCGGUUCUCCACACCAGCGGGACCGGUGCCUGGUACGAAUCCUGGGGUCGUCGUUGUACUCGUGCGAGUAAUCUACGAUCGCUACAACGCGCUAAGCAAUGGAGAUGAUUCUUGAGGGAAGCAAUUCUGCAGAGACAUCUCCACCCUGGGGUGGCGUCGCCAGAAUACACCGACCCGUUACAGGGAAAUUGGCUACUUGGCUGGACAACUACAUCUCUCUUCUUU .(.(((.......))).)....((((..(.(((((((.((...((.(..(((.(..(.((...)).)..).(((((...(((((.((((((((.(((((((((.(((...((((.(((((.((....)))))))....))))..)))(...)...(((((((...((((....(((....)))....)))).))))))))))))))))..))).)))))....)))))..)))))..)))..).).)..))..))))))).)..))))............. -94.00 ...((((.((((((.....))).))).))))((((((...((.((((..(((((....(((.......)))((((((..(((((.((((((((.((((((((((((.(((((((((((((.((....)))))))..).))))))))).........(((((((..((((...((((....))))...)))))))))))))))))))))..))).)))))....))))).)))))).)))))..)))).))...))))))...................... +srp_Eime.tene._GSP-413949 GGCGGGCGUAGCGAGCCGCUGUUACCCGUGCGGGGGUCGGCUCGGUGGAGGCAUCAGUGGUGCCGCUGUAGUGUGGGGUGUUAGCGGCCAAACGCCCCACUGGGAUCGCAGCCCCCGGCGAUCCCCGCGCGGAGGCGGGAGGAUCGCUGGAGAUGCUGCGGCGCGCAACGCCCCAGGCUGGAAACAGAGCAGGGAAAAGUGCCCGCUGCGUUCCGCGGUGGGACAACGGGGGGCCGCGCCCGGGCUGCGGCGGCCCGCGGCGGCCAACGCCGAGCUUUUUU (((.((((........))))....((((())))).)))(((((((((..(((..(.((((.(((((((((((.((((......((((((...((.((((((((((.(((((((.((((((((((.(.(((....))).).)))))))))).).(...).(((((......(((....(((....)))....)))....))))).)))))).))).)))))))....))...)))))).)))).))))))))))))))).)..)))..)))))))))..... -160.40 (((((.(.......))))))...((((......)))).(((((((((..(((....((((.(((((((((((.((((......((((((...((.((((((((((.((((((..((((((((((((.(((....))))).)))))))))).....(((.((((.....)))).)))(((........))).(((.......))))))))).))).)))))))....))...)))))).)))).)))))))))))))))....)))..)))))))))..... +srp_Lotu.japo._BW598799 AGCUUGUAACCCAAGUGGGGGCACCAAGGUGAUGGAACAUGGCUCUGGUGUCGUUGGGCUGGGCUGUUAGCUGCAAUAAGUGGCCUGCCCACUCCAAGUUGAGAGUUGGGCCAUGGGGCUUUAGCGAAGGCUCUAGCUUCCUGGUUCCUAAACUGGAGGGCACGGCGUGAGGCUGGUUUCACAGAGCAGCGAUAACCUCCGGCUCCUGACAGUGGAGGGAUAACGGGCCGCUGCACUCUCAGCCCACUACGCCCGGUGAGCCUGGCCUCUUGAACCAUCAU .........((((..)))).........(((((((..((.((.((.((..(..(..(((.(((..(..(..((..(....(((((((.((.(((((.((((.((((.((((((.((((((..(((....)))..)))).)))))))).(..).((((((....(.(((....(((......)))....))).)..)))))))))).))))..)))))))....))))))).).)).).)...))).....))).)..)..)).))..)).))..))))))) -121.80 ..(((((..(((.....)))..).))))(((((((..((.((((..(((.((((((((((((((((..((.((((....(((((((((((((((((...)).))).))))(((.((((((..(((....)))..)))))).)))(((((..((((.(((((.(((.((...((((.(((...))).))))....))..))))).)))..))))..)))))...))))))))))))))..)))))))....)))))))))))).))))...))..))))))) +srp_Dani.rrer._BX469912 GCCGGGUUCAGUGGCGCGCGCCUGUAAUCCAAGCUACUGGGAGGCUGAGGCUGCGGAUCGCUUGAGCUCAGGGUGUCUGGGCGGCAGUGGACUAUGUCGAUUGGGUGUCUGCACUAAGUUCGGUAUUGAUAUGGUGCUUCUGGGGGAGCUCGGGACCACCAGGUUGAGUAAGGAGGGGUGAACCGGCCCAGGACGGAGACGGAGCAGGUCAAAGCCCCCGUGCUGAUCAGUAGUGGGAUCGCGCCUAAGAAUAGACACUGCAGUGCAGCCUGAGCAACACAG (((((((.(........).))))....(((.........))))))...............((...(((((((....(((.((.((((((..((((.(...((((((((((.(((((.(.(((((((((((.(((((.(((((((....))))))).))))).))))........(((((.....((((.....((....)).....))))...))))).))))))).)..)))))))...))))))))).)))).)))))).)).)))))))))).....)) -106.60 ((.(((((((..(((....))))).)))))..))..(((.(..(((.((((((((..........((((((.....)))))).((((((..((((.....((((((((.(.(((((...(((((((.(((.(((((.(((((((....))))))).))))).))).......(.(((((..(((.((((.(........))).)).)))....)))))))))))))....))))).)...))))))))..)))).))))))..)))))))).)))..).))) +srp_Dani.rrer._BX470258 GCCGGGUUCAGUGGUGCGCGCCUGUAAUCCAAGCUACUGGGAGGCUGAGGCUGCGGAUCGCUUGAGCUCAGGGCUUCUGGACUGCAGUGGACUAUGUUGAUCAGGUGUCCACACUAAGUUCGGUAUCGAUAUGGUGCUCCUGGGGGAGCUUGGGACCACCAGGUCGUGUUAUGAGGGGUGAACCGGCCCAGGUCGGCGACGGAGCAGGUCAAAUCCCCCGUGCUGAUCAGUAGUGGGAUCACACCUGAGAAUAGACACUGCAGUGCAGCCUGAGUGACACAG (((((((.(........).))))....(((.........))))))...............((...(((((((....(((.(((((((((..((((.(...((((((((((.(((((.(.(((((((((((.(((((.(((((((....))))))).))))).))))((...)).((((......((((....(((....)))....))))....)))).))))))).)..)))))))...))))))))).)))).))))))))).)))))))))).....)) -121.90 .((((((((((((((.(((((((....(((.((...)).))).....)))).))).))))).))))))).))(((((((.(((((((((..((((.....((((((((((.(((((..((((....((((.(((((.(((((((....))))))).))))).)))).....))))((((......)))).(((((((.((((.(..((......))))))))))))))..))))))))...)))))))..)))).))))))))).)))...))))....... diff --git a/lib/tornadofold/tests/smoke_test.py b/lib/tornadofold/tests/smoke_test.py new file mode 100644 index 0000000..cbd8569 --- /dev/null +++ b/lib/tornadofold/tests/smoke_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Smoke / regression test: fold a stratified sample of ArchiveII sequences +# (28-282 nt) and check the MFE *and* the traceback structure against +# precomputed golden values in data/archiveii_smoke.tsv. +# +# The golden values are tornadofold's own deterministic output; every MFE was +# verified byte-exact against RNAfold -d2 when the file was generated (see +# util/gen note below). Structure is checked against tornadofold's golden rather +# than RNAfold's because co-optimal (equal-energy) structures are resolved by +# implementation-specific traceback tie-breaking. The sample avoids hairpin +# loops > 30 nt so the golden reproduces exactly across platforms/libm. +# +# Usage: python3 tests/smoke_test.py (set TORNADOFOLD=path to override the binary) +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +BIN = os.environ.get("TORNADOFOLD", os.path.join(HERE, "..", "tornadofold")) +DATA = os.path.join(HERE, "data", "archiveii_smoke.tsv") + + +def main(): + if not os.path.exists(BIN): + print(f"error: tornadofold binary not found at {BIN} (build it first: `make`)") + return 2 + rows = [l.rstrip("\n").split("\t") for l in open(DATA)][1:] + fails = 0 + for name, seq, ref, mfe, db in rows: + out = subprocess.run([BIN], input=seq + "\n", capture_output=True, + text=True).stdout.strip().split("\n") + got = out[1].rsplit(" ", 1) + got_db, got_mfe = got[0], got[1].strip("()") + if got_mfe != mfe or got_db != db: + fails += 1 + print(f"FAIL {name} (len {len(seq)}): " + f"MFE {got_mfe} vs {mfe}; structure {'ok' if got_db == db else 'DIFFERS'}") + print(f"smoke: {len(rows) - fails}/{len(rows)} sequences reproduce golden MFE + structure") + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/lib/tornadofold/util/accuracy.py b/lib/tornadofold/util/accuracy.py new file mode 100644 index 0000000..46cdaee --- /dev/null +++ b/lib/tornadofold/util/accuracy.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# Accuracy benchmark on ArchiveII: compare tornadofold vs RNAfold -d2 against +# reference structures. Reports micro- and macro-averaged sensitivity/PPV/F1. +# +# Point it at a directory of .ct or .bpseq reference files (the ArchiveII +# tarballs ship .bpseq): +# ARCHIVEII=path/to/archiveII TORNADOFOLD=./tornadofold RNAFOLD=RNAfold \ +# python3 util/accuracy.py [maxlen] +import os, sys, glob, subprocess, re + +DATADIR = os.environ.get("ARCHIVEII", + os.path.join(os.path.dirname(__file__), "..", "archiveII")) +TORNADOFOLD = os.environ.get("TORNADOFOLD", os.path.join(os.path.dirname(__file__), "..", "tornadofold")) +# RNAfold comparison is opt-in: only run it when RNAFOLD points at a binary. +RNAFOLD = os.environ.get("RNAFOLD") + +def parse_bpseq(path): + seq, pairs = [], {} + for ln in open(path): + t = ln.split() + if len(t) < 3 or not t[0].isdigit(): continue + i = int(t[0]); base = t[1].upper().replace('T','U'); j = int(t[2]) + seq.append(base if base in 'ACGU' else 'N') + if j > i: pairs[i-1] = j-1 + return "".join(seq), pairs + +def parse_ct(path): + seq, pairs = [], {} + with open(path) as fh: + lines = fh.read().splitlines() + # header line: " title"; body lines: idx base prev next pair natidx + body = [] + started = False + for ln in lines: + t = ln.split() + if not t: continue + if not started: + # first numeric-leading line is header + if re.match(r'^\d+', t[0]): + started = True + continue + if len(t) >= 5 and t[0].isdigit(): + body.append(t) + for t in body: + i = int(t[0]); base = t[1].upper().replace('T','U') + j = int(t[4]) + seq.append(base if base in 'ACGU' else 'N') + if j > i: + pairs[i-1] = j-1 + return "".join(seq), pairs + +def db_to_pairs(db): + st, pr = [], {} + for i,c in enumerate(db): + if c=='(' : st.append(i) + elif c==')': + if st: + k=st.pop(); pr[k]=i + return pr + +def load_dataset(maxlen=None): + recs=[] + files = sorted(glob.glob(os.path.join(DATADIR,"*.ct")) + + glob.glob(os.path.join(DATADIR,"*.bpseq"))) + for f in files: + seq,pairs = parse_bpseq(f) if f.endswith(".bpseq") else parse_ct(f) + if len(seq)==0: continue + if maxlen and len(seq)>maxlen: continue + recs.append((os.path.basename(f), seq, pairs)) + return recs + +def run_tool_fasta(recs, cmd, kind): + fa = "\n".join(f">{i}\n{seq}" for i,(_,seq,_) in enumerate(recs))+"\n" + p = subprocess.run(cmd, input=fa, capture_output=True, text=True) + out = p.stdout + # extract dot-bracket lines in order + structs=[] + for ln in out.splitlines(): + s=ln.strip() + # a structure line: first token is only structure chars + tok=s.split()[0] if s else "" + if tok and set(tok) <= set("().[]{}<>") and len(tok)>0: + structs.append(tok) + return structs + +def score(recs, structs): + # returns (micro dict, macro dict) + TP=FP=FN=0 + mf1=ms=mp=0.0; nm=0 + per=[] + for (name,seq,ref),db in zip(recs,structs): + pred = db_to_pairs(db) + refset = set((k,v) for k,v in ref.items()) + predset = set((k,v) for k,v in pred.items()) + tp=len(refset & predset); fp=len(predset-refset); fn=len(refset-predset) + TP+=tp; FP+=fp; FN+=fn + sens = tp/(tp+fn) if (tp+fn) else (1.0 if not predset else 0.0) + ppv = tp/(tp+fp) if (tp+fp) else (1.0 if not refset else 0.0) + f1 = 2*sens*ppv/(sens+ppv) if (sens+ppv) else 0.0 + ms+=sens; mp+=ppv; mf1+=f1; nm+=1 + per.append((name,len(seq),sens,ppv,f1)) + micro_s = TP/(TP+FN) if (TP+FN) else 0 + micro_p = TP/(TP+FP) if (TP+FP) else 0 + micro_f = 2*micro_s*micro_p/(micro_s+micro_p) if (micro_s+micro_p) else 0 + macro = (ms/nm, mp/nm, mf1/nm) + return (micro_s,micro_p,micro_f), macro, per + +if __name__=="__main__": + maxlen = int(sys.argv[1]) if len(sys.argv)>1 else None + recs = load_dataset(maxlen) + print(f"loaded {len(recs)} structures"+(f" (<= {maxlen} nt)" if maxlen else ""), flush=True) + import time + tools = [("tornadofold",[TORNADOFOLD])] + if RNAFOLD: + tools.append(("RNAfold -d2",[RNAFOLD,"--noPS","-d2"])) + for label,cmd in tools: + t0=time.time() + structs = run_tool_fasta(recs, cmd, label) + dt=time.time()-t0 + if len(structs)!=len(recs): + print(f" WARN {label}: got {len(structs)} structs for {len(recs)} recs") + micro,macro,per = score(recs, structs[:len(recs)]) + print(f"\n{label} ({dt:.1f}s)") + print(f" micro sens={micro[0]:.3f} ppv={micro[1]:.3f} F1={micro[2]:.3f}") + print(f" macro sens={macro[0]:.3f} ppv={macro[1]:.3f} F1={macro[2]:.3f}") + # per-family macro F1 + fam={} + for name,L,s,p,f in per: + fk=name.split('_')[0] + fam.setdefault(fk,[]).append(f) + fl=" ".join(f"{k}={sum(v)/len(v):.2f}" for k,v in sorted(fam.items())) + print(f" family macroF1: {fl}", flush=True) diff --git a/lib/tornadofold/util/gen_t2004.py b/lib/tornadofold/util/gen_t2004.py new file mode 100644 index 0000000..f7be2b7 --- /dev/null +++ b/lib/tornadofold/util/gen_t2004.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +"""Generate src/t2004.h directly from the NNDB Turner 2004 .dg bundle. + +Reads the primary turner_2004 .dg source files (never a pre-baked header), +builds the nearest-neighbour tables, re-indexes them into this folder's compact +layout, and emits src/t2004.h. Also diffs every folding-reachable value against +the in-tree tables so any divergence is surfaced, not hidden. + + python3 util/gen_t2004.py --turner-source util/turner_2004.zip --out src/t2004.h + python3 util/gen_t2004.py --turner-source util/turner_2004.zip --check-only + +Layout: pair index = ViennaRNA pair type - 1 (CG..NN = 0..6); mismatch/dangle +base index bidx(N)=0, A..U=1..4; int22 canonical-only [6][6][4][4][4][4]. +lxc defaults to ViennaRNA's precise 107.856 (the .dg file rounds it to 107.9); +pass --lxc 107.9 for pure .dg provenance (folds identically at real loop sizes). +""" +import argparse +import re +import shutil +import tempfile +import zipfile +from decimal import Decimal, ROUND_HALF_UP +from pathlib import Path + +INF = 1_000_000 +VIE_INF = 10_000_000 +NT = "ACGU" +NT_INDEX = {b: i for i, b in enumerate(NT)} +CANON = ["CG", "GC", "GU", "UG", "AU", "UA"] # ViennaRNA pairs 1..6 +VALUE_RE = re.compile(r"^[+-]?(?:\d+\.?\d*|\.\d+|\.)$") + + +# --- .dg parsing (from the NNDB primary sources) ---------------------------- + +def scale(tok): + if tok == ".": + return VIE_INF + return int((Decimal(tok) * 100).quantize(Decimal(1), rounding=ROUND_HALF_UP)) + + +def reverse_pair(p): + return p[::-1] + + +def read_grid_blocks(path, ncols): + blocks, labels, rows = [], [], [] + for raw in path.read_text().splitlines(): + stripped = raw.strip() + if not stripped or stripped.startswith("#"): + if rows: + blocks.append((labels, rows)); labels, rows = [], [] + continue + values = [t for t in raw.split() if VALUE_RE.match(t)] + if len(values) == ncols: + rows.append(values) + else: + if rows: + blocks.append((labels, rows)); labels, rows = [], [] + labels.append(stripped) + if rows: + blocks.append((labels, rows)) + return blocks + + +def scaled_grid(rows): + return [[scale(t) for t in row] for row in rows] + + +def read_number_groups(path): + groups, current = [], [] + for raw in path.read_text().splitlines(): + stripped = raw.strip() + if not stripped or stripped.startswith("#"): + if current: + groups.append(current); current = [] + continue + current.extend(stripped.split()) + if current: + groups.append(current) + return groups + + +def two_base_label(label): + toks = label.split() + return len(toks) == 2 and all(b in NT for t in toks for b in t) + + +def parse_pair_grids(path, key_from_labels): + table = {} + for labels, rows in read_grid_blocks(path, 4): + table[key_from_labels(labels)] = scaled_grid(rows) + return table + + +def stack_key(labels): + top = next(l for l in labels if "X" in l) + bottom = next(l for l in labels if "Y" in l) + return top[top.index("X") - 1] + bottom[bottom.index("Y") - 1] + + +def parse_stack(path): + return parse_pair_grids(path, stack_key) + + +def parse_mismatch(path): + return parse_pair_grids(path, stack_key) + + +def parse_dangles(path): + blocks = read_grid_blocks(path, 4) + dangle3, dangle5 = {}, {} + for index, (labels, rows) in enumerate(blocks): + top_base = next(l for l in labels if "X" in l).replace("X", "")[0] + plain_base = next(l for l in labels if "X" not in l and l in NT) + values = [scale(t) for t in rows[0]] + pair = plain_base + top_base + (dangle3 if index < 16 else dangle5)[pair if index < 16 else reverse_pair(pair)] = values + return dangle3, dangle5 + + +def parse_specials(path): + entries = [] + for raw in path.read_text().splitlines(): + s = raw.strip() + if not s or s.startswith("#"): + continue + seq, energy = s.split() + entries.append((seq, scale(energy))) + entries.sort() + return entries + + +def parse_loops(path): + internal, bulge, hairpin = [VIE_INF], [VIE_INF], [VIE_INF] + for raw in path.read_text().splitlines(): + s = raw.strip() + if not s or s.startswith("#"): + continue + _, i_tok, b_tok, h_tok = s.split() + internal.append(scale(i_tok)); bulge.append(scale(b_tok)); hairpin.append(scale(h_tok)) + return internal, bulge, hairpin + + +def parse_int11(path): + table = {} + for labels, rows in read_grid_blocks(path, 4): + top, bottom = (l.split() for l in labels if two_base_label(l)) + table[(top[0] + bottom[0], top[1] + bottom[1])] = scaled_grid(rows) + return table + + +def parse_int21(path): + table = {} + for labels, rows in read_grid_blocks(path, 4): + pair_lines = [l.split() for l in labels if two_base_label(l)] + top, bottom = pair_lines[0], pair_lines[1] + panel_label = next(l for l in labels if "Y" in l) + panel = panel_label[panel_label.index("Y") + 1] + table[(top[0] + bottom[0], top[1] + bottom[1], panel)] = scaled_grid(rows) + return table + + +def parse_int22(path): + table = {} + for labels, rows in read_grid_blocks(path, 16): + top = next(l for l in labels if "X1" in l).split() + bottom = next(l for l in labels if "X2" in l).split() + table[(top[0] + bottom[0], top[-1] + bottom[-1])] = scaled_grid(rows) + return table + + +# --- build ViennaRNA-layout [8]/[5] nested arrays --------------------------- + +def pair_matrix(value_of): + m = [] + for i in range(8): + row = [] + for j in range(8): + if i == 0 or j == 0: + row.append(VIE_INF) + elif i == 7 or j == 7: + row.append(0) + else: + row.append(value_of(i, j)) + m.append(row) + return m + + +def mismatch_block(pair_index, values_of): + block = [[0] * 5 for _ in range(5)] + for i in range(1, 5): + for j in range(1, 5): + block[i][j] = values_of(pair_index, i - 1, j - 1) + return block + + +def build_mismatch(values_of): + mm = [[[VIE_INF] * 5 for _ in range(5)]] # pair 0 sentinel + mm += [mismatch_block(p, values_of) for p in range(1, 7)] + mm.append([[0] * 5 for _ in range(5)]) # pair 7 = NN + return mm + + +def build_dangle(dangle): + dg = [[VIE_INF] * 5] # N sentinel + dg += [[0] + dangle[label] for label in CANON] + dg.append([0] * 5) # NN + return dg + + +def build_int11(table): + def value(p1, p2, a, b): + if not (1 <= p1 <= 6 and 1 <= p2 <= 6 and 1 <= a <= 4 and 1 <= b <= 4): + return VIE_INF + return table[(CANON[p1 - 1], reverse_pair(CANON[p2 - 1]))][a - 1][b - 1] + return [[[[value(p1, p2, a, b) for b in range(5)] for a in range(5)] + for p2 in range(8)] for p1 in range(8)] + + +def build_int21(table): + def value(p1, p2, a3, a4, a5): + if not (1 <= p1 <= 6 and 1 <= p2 <= 6 and 1 <= a3 <= 4 and 1 <= a4 <= 4 and 1 <= a5 <= 4): + return VIE_INF + grid = table[(CANON[p1 - 1], reverse_pair(CANON[p2 - 1]), NT[a4 - 1])] + return grid[a3 - 1][a5 - 1] + return [[[[[value(p1, p2, a3, a4, a5) for a5 in range(5)] for a4 in range(5)] + for a3 in range(5)] for p2 in range(8)] for p1 in range(8)] + + +def build_int22(table): + def value(p1, p2, a3, a4, a5, a6): + if not (1 <= p1 <= 6 and 1 <= p2 <= 6 and min(a3, a4, a5, a6) >= 1 and max(a3, a4, a5, a6) <= 4): + return VIE_INF + grid = table[(CANON[p1 - 1], reverse_pair(CANON[p2 - 1]))] + return grid[4 * (a3 - 1) + (a6 - 1)][4 * (a4 - 1) + (a5 - 1)] + return [[[[[[value(p1, p2, a3, a4, a5, a6) for a6 in range(5)] for a5 in range(5)] + for a4 in range(5)] for a3 in range(5)] for p2 in range(8)] for p1 in range(8)] + + +def build_vienna(src): + groups = read_number_groups(src / "rna.miscloop.dg") + stack_d = parse_stack(src / "rna.stack.dg") + internal, bulge, hairpin = parse_loops(src / "rna.loop.dg") + internal[2] = internal[3] = 100 # ViennaRNA convention (small internal loops) + dangle3, dangle5 = parse_dangles(src / "rna.dangle.dg") + + def parsed_value(tab): + return lambda p, i, j: tab[CANON[p - 1]][i][j] + + def reversed_value(tab): + return lambda p, i, j: tab[reverse_pair(CANON[p - 1])][j][i] + + return { + "stack": pair_matrix(lambda i, j: stack_d[CANON[i - 1]] + [NT_INDEX[CANON[j - 1][1]]][NT_INDEX[CANON[j - 1][0]]]), + "mm_hairpin": build_mismatch(parsed_value(parse_mismatch(src / "rna.tstackh.dg"))), + "mm_internal": build_mismatch(parsed_value(parse_mismatch(src / "rna.tstacki.dg"))), + "mm_internal_1n": build_mismatch(parsed_value(parse_mismatch(src / "rna.tstacki1n.dg"))), + "mm_internal_23": build_mismatch(parsed_value(parse_mismatch(src / "rna.tstacki23.dg"))), + "mm_multi": build_mismatch(reversed_value(parse_mismatch(src / "rna.tstackm.dg"))), + "mm_exterior": build_mismatch(reversed_value(parse_mismatch(src / "rna.tstack.dg"))), + "dangle5": build_dangle(dangle5), + "dangle3": build_dangle(dangle3), + "hairpin": hairpin, "bulge": bulge, "internal": internal, + "int11": build_int11(parse_int11(src / "rna.int11.dg")), + "int21": build_int21(parse_int21(src / "rna.int21.dg")), + "int22": build_int22(parse_int22(src / "rna.int22.dg")), + "tri": parse_specials(src / "rna.triloop.dg"), + "tetra": parse_specials(src / "rna.tloop.dg"), + "hexa": parse_specials(src / "rna.hexaloop.dg"), + "ML_closing": scale(groups[3][0]), + "ML_BASE": scale(groups[3][1]), + "ML_intern": -90, # ViennaRNA convention + "NINIO_max": scale(groups[1][0]), + "NINIO_m": scale(groups[2][0]), + "TerminalAU": scale(groups[7][0]), + "lxc": float(Decimal(groups[0][0]) * 100), + } + + +# --- re-index ViennaRNA [8]/[5] arrays into t2004.h [7]/[6] layout ----------- + +def reindex(v): + t = {} + t["stack"] = [[v["stack"][a + 1][b + 1] for b in range(7)] for a in range(7)] + for name in ("mm_hairpin", "mm_internal", "mm_internal_1n", "mm_internal_23", + "mm_multi", "mm_exterior"): + t[name] = [[[v[name][a + 1][x][y] for y in range(5)] for x in range(5)] for a in range(7)] + for name in ("dangle5", "dangle3"): + t[name] = [[v[name][a + 1][x] for x in range(5)] for a in range(7)] + t["int11"] = [[[[v["int11"][a + 1][b + 1][x][y] for y in range(5)] for x in range(5)] + for b in range(7)] for a in range(7)] + t["int21"] = [[[[[v["int21"][a + 1][b + 1][x][y][z] for z in range(5)] for y in range(5)] + for x in range(5)] for b in range(7)] for a in range(7)] + t["int22"] = [[[[[[v["int22"][a + 1][b + 1][w + 1][x + 1][y + 1][z + 1] + for z in range(4)] for y in range(4)] for x in range(4)] + for w in range(4)] for b in range(6)] for a in range(6)] + for k in ("hairpin", "bulge", "internal", "tri", "tetra", "hexa", "ML_closing", + "ML_intern", "ML_BASE", "NINIO_max", "NINIO_m", "TerminalAU", "lxc"): + t[k] = v[k] + return t + + +# --- diff against the in-tree tables (folding-reachable entries only) -------- + +def flat(x): + if isinstance(x, list): + for e in x: + yield from flat(e) + else: + yield x + + +def reshape(flat_list, shape): + if len(shape) == 1: + return list(flat_list) + step = len(flat_list) // shape[0] + return [reshape(flat_list[i * step:(i + 1) * step], shape[1:]) for i in range(shape[0])] + + +CUR_SHAPES = { + "stack": (7, 7), "mm_hairpin": (7, 5, 5), "mm_internal": (7, 5, 5), + "mm_internal_1n": (7, 5, 5), "mm_internal_23": (7, 5, 5), "mm_multi": (7, 5, 5), + "mm_exterior": (7, 5, 5), "dangle5": (7, 5), "dangle3": (7, 5), + "int11": (7, 7, 5, 5), "int21": (7, 7, 5, 5, 5), "int22": (6, 6, 4, 4, 4, 4), + "hairpin": (31,), "bulge": (31,), "internal": (31,), +} + + +def parse_c_array(text, name): + m = re.search(re.escape(name) + r"\s*(?:\[[^\]]*\])+\s*=\s*", text) + if not m: + raise KeyError(name) + i = text.index("{", m.end()); depth = 0; j = i + while j < len(text): + depth += (text[j] == "{") - (text[j] == "}") + if depth == 0: + break + j += 1 + py = re.sub(r"/\*.*?\*/", "", text[i:j + 1]).replace("{", "[").replace("}", "]") + return eval(re.sub(r",\s*\]", "]", py)) + + +def scalar(text, name, cast=int): + return cast(re.search(name + r"\s*=\s*([-0-9.]+)", text).group(1)) + + +def load_current(path): + text = path.read_text() + d = {n: reshape(list(flat(parse_c_array(text, n))), CUR_SHAPES[n]) for n in CUR_SHAPES} + for k, cast in (("ML_closing", int), ("ML_intern", int), ("ML_BASE", int), + ("NINIO_max", int), ("NINIO_m", int), ("TerminalAU", int), ("lxc", float)): + d[k] = scalar(text, k, cast) + return d + + +def canon(name, arr): + """Folding-reachable entries: canonical pairs (0..5), real bases (1..4).""" + P, B = range(6), range(1, 5) + if name == "stack": + return [arr[a][b] for a in P for b in P] + if name.startswith("mm_"): + return [arr[a][x][y] for a in P for x in B for y in B] + if name.startswith("dangle"): + return [arr[a][x] for a in P for x in B] + if name == "int11": + return [arr[a][b][x][y] for a in P for b in P for x in B for y in B] + if name == "int21": + return [arr[a][b][x][y][z] for a in P for b in P for x in B for y in B for z in B] + if name in ("hairpin", "bulge", "internal"): + return arr[3:31] + return list(flat(arr)) # int22 already canonical + + +def diff_canonical(new, cur): + reps = [] + for name in CUR_SHAPES: + a, b = canon(name, new[name]), canon(name, cur[name]) + d = [(x, y) for x, y in zip(a, b) if x != y] + if len(a) != len(b): + reps.append(f" {name}: SHAPE new={len(a)} cur={len(b)}") + elif d: + reps.append(f" {name}: {len(d)}/{len(a)} diffs, e.g. new/cur {d[:4]}") + for k in ("ML_closing", "ML_intern", "ML_BASE", "NINIO_max", "NINIO_m", "TerminalAU", "lxc"): + if new[k] != cur[k]: + reps.append(f" {k}: new={new[k]} cur={cur[k]}") + return reps + + +# --- emit t2004.h ----------------------------------------------------------- + +def cflat(arr, int16=True): + # Nested brace-initializer matching the array shape (so the header needs no + # -Wno-missing-braces). Dead sentinel entries (VIE_INF) are clamped to fit + # the target type: 0 for the int16 tables, our INF for the (int) loop arrays. + if isinstance(arr, list): + return "{" + ",".join(cflat(e, int16) for e in arr) + "}" + return str(0 if (int16 and arr >= VIE_INF) else (INF if arr >= VIE_INF else arr)) + + +def emit_t2004(new, lxc): + L = [ + "// Turner 2004 energy tables for src/energy.h.", + "// Generated by util/gen_t2004.py directly from the NNDB turner_2004 .dg bundle.", + "// Layout: pair index = ViennaRNA pair type - 1; mismatch/dangle base index", + "// bidx(N)=0, A..U=1..4; int22 canonical-only. Sentinel (NN pair, N base)", + "// entries are folding-unreachable and set to 0. lxc is kept at ViennaRNA's", + "// precise 107.856 (the .dg file rounds it to 107.9) for RNAfold-exact", + "// large-loop extrapolation.", + "#pragma once", + "#include ", + "namespace t2004 {", + "static const int INF = 1000000;", + f"static const int16_t stack[7][7] = {cflat(new['stack'])};", + ] + for nm in ("hairpin", "bulge", "internal"): + L.append(f"static const int {nm}[31] = {cflat(new[nm], int16=False)};") + for nm in ("mm_hairpin", "mm_internal", "mm_internal_1n", "mm_internal_23", + "mm_multi", "mm_exterior"): + L.append(f"static const int16_t {nm}[7][5][5] = {cflat(new[nm])};") + for nm in ("dangle5", "dangle3"): + L.append(f"static const int16_t {nm}[7][5] = {cflat(new[nm])};") + L.append(f"static const int16_t int11[7][7][5][5] = {cflat(new['int11'])};") + L.append(f"static const int16_t int21[7][7][5][5][5] = {cflat(new['int21'])};") + L.append(f"static const int16_t int22[6][6][4][4][4][4] = {cflat(new['int22'])};") + L.append(f"static const int ML_BASE={new['ML_BASE']}, ML_closing={new['ML_closing']}, ML_intern={new['ML_intern']};") + L.append(f"static const int NINIO_m={new['NINIO_m']}, NINIO_max={new['NINIO_max']};") + L.append(f"static const int TerminalAU={new['TerminalAU']};") + L.append(f"static const double lxc={lxc};") + for struct, name, data in (("SPtetra", "tetra", new["tetra"]), + ("SPtri", "tri", new["tri"]), + ("SPhexa", "hexa", new["hexa"])): + L.append("struct %s{const char* seq;int bonus;};" % struct) + L.append("static const %s %s[]={%s};" % ( + struct, name, ",".join('{"%s",%d}' % (s, v) for s, v in data))) + L.append(f"static const int {name}_n={len(data)};") + L.append("} // namespace t2004") + return "\n".join(L) + "\n" + + +def _find_dg_dir(root): + for c in (root, root / "turner_2004"): + if (c / "rna.stack.dg").is_file(): + return c + matches = list(root.rglob("rna.stack.dg")) + if matches: + return matches[0].parent + raise SystemExit(f"No .dg files under {root}") + + +def resolve_source(raw): + path = Path(raw) + if path.is_file() and path.suffix == ".zip": + tmp = Path(tempfile.mkdtemp(prefix="turner2004_")) + with zipfile.ZipFile(path) as z: + z.extractall(tmp) + return _find_dg_dir(tmp), tmp + return _find_dg_dir(path), None + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--turner-source", required=True, help="turner_2004 .dg bundle (zip or dir)") + ap.add_argument("--current", default="src/t2004.h", help="in-tree tables to diff against") + ap.add_argument("--out", help="write t2004.h here (omit to only report the diff)") + ap.add_argument("--lxc", type=float, default=107.856, + help="lxc override (default 107.856 = ViennaRNA/RNAfold; .dg rounds to 107.9)") + ap.add_argument("--check-only", action="store_true") + args = ap.parse_args() + + src, tmp = resolve_source(args.turner_source) + try: + new = reindex(build_vienna(src)) + cur_path = Path(args.current) + if cur_path.is_file(): + reps = diff_canonical(new, load_current(cur_path)) + print("Canonical values IDENTICAL to current t2004.h." if not reps + else "DIFFERENCES from current t2004.h (folding-reachable):\n" + "\n".join(reps)) + if args.out and not args.check_only: + Path(args.out).write_text(emit_t2004(new, args.lxc)) + print(f"wrote {args.out} (lxc={args.lxc})") + finally: + if tmp: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/lib/tornadofold/util/turner_2004.zip b/lib/tornadofold/util/turner_2004.zip new file mode 100644 index 0000000..056f720 Binary files /dev/null and b/lib/tornadofold/util/turner_2004.zip differ diff --git a/lib/tornadofold/web/index.html b/lib/tornadofold/web/index.html new file mode 100644 index 0000000..2fa66df --- /dev/null +++ b/lib/tornadofold/web/index.html @@ -0,0 +1,267 @@ + + + + + +ToRNAdoFold: RNA MFE folding + + + + +
+

ToRNAdoFold

+

Minimum-free-energy RNA secondary structure folded via WebAssembly (SIMD) and drawn with fornac.

+ + + +
+ + Loading… +
+ +
+
MFE kcal/mol
+
+
+ Dot-bracket +

+    
+
+ +
+

Batch — fold a file

+

FASTA, or one sequence per line. Folded in parallel with ? web workers.

+
+ + + +
+
+
+ + + + + + + diff --git a/lib/tornadofold/web/tornadofold-worker.js b/lib/tornadofold/web/tornadofold-worker.js new file mode 100644 index 0000000..0e18f1f --- /dev/null +++ b/lib/tornadofold/web/tornadofold-worker.js @@ -0,0 +1,16 @@ +// Batch worker: loads the wasm module once, folds one sequence per message. +// The main thread passes the module to load (?m=tornadofold[-simd].js) so the +// worker matches the page's SIMD-support decision. +const module = new URLSearchParams(self.location.search).get("m") || "tornadofold.js"; +importScripts(module); + +const ready = tornadofold(); + +self.onmessage = (e) => { + const { id, seq } = e.data; + ready.then((mod) => { + const res = mod.foldSeq(seq); + const tab = res.lastIndexOf("\t"); + self.postMessage({ id, db: res.slice(0, tab), mfe: parseInt(res.slice(tab + 1), 10) }); + }); +}; diff --git a/lib/tornadofold/web/tornadofold_wasm.cpp b/lib/tornadofold/web/tornadofold_wasm.cpp new file mode 100644 index 0000000..af8578c --- /dev/null +++ b/lib/tornadofold/web/tornadofold_wasm.cpp @@ -0,0 +1,35 @@ +// WebAssembly entry point: expose the folder to JavaScript via embind. +// Built with emcc (see `make wasm`); the NEON block in tornadofold.h is inactive +// on wasm, so this is the exact-int32 scalar path. +#include "tornadofold.h" +#include +#include +#include + +// Fold a sequence and return "\t". The input is +// sanitized to match the CLI: whitespace dropped, lowercase upper-cased, T->U, +// anything else treated as N (kept, so positions still line up). +static std::string foldSeq(const std::string& seq) { + std::string s; + for (char c : seq) { + if (std::isspace((unsigned char)c)) { + continue; + } + char u = (char)std::toupper((unsigned char)c); + if (u == 'T') { + u = 'U'; + } + s += u; + } + if (s.empty()) { + return std::string("\t0"); + } + tornadofold::TornadoFold f; + int e = f.fold(s); + std::string db = f.traceback(e); + return db + "\t" + std::to_string(e); +} + +EMSCRIPTEN_BINDINGS(tornadofold) { + emscripten::function("foldSeq", &foldSeq); +} diff --git a/lib/viennarna/CMakeLists.txt b/lib/viennarna/CMakeLists.txt deleted file mode 100644 index 0860098..0000000 --- a/lib/viennarna/CMakeLists.txt +++ /dev/null @@ -1,502 +0,0 @@ -# ViennaRNA minimal static library for MFE folding -# Built from the ViennaRNA package source tree - -# ViennaRNA source: look relative to the riboseek root, then fall back to env/cache -set(VRNA_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ViennaRNA/src/ViennaRNA") - -if(NOT EXISTS "${VRNA_SRC_DIR}/fold_compound.c") - message(WARNING "ViennaRNA source not found at ${VRNA_SRC_DIR}, skipping viennarna library") - return() -endif() - -# --------------------------------------------------------------------------- -# Codegen: turn ViennaRNA's data files (.par/.json/.data/.ps/.svg) into the -# .hex byte-arrays + the four umbrella .h headers that the C sources include. -# Replaces the autotools step that's normally driven by configure+xxd. -# Output is fully arch-independent (xxd hex dumps of text data). -# --------------------------------------------------------------------------- -find_program(XXD_EXECUTABLE xxd) -if(NOT XXD_EXECUTABLE) - message(FATAL_ERROR - "xxd not found — required to generate ViennaRNA .hex headers. " - "Install it (Debian/Ubuntu: 'sudo apt install xxd'; " - "macOS: ships with vim).") -endif() - -set(VRNA_MISC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ViennaRNA/misc") -set(VRNA_STATIC_DIR "${VRNA_SRC_DIR}/static") -set(VRNA_PS_DIR "${VRNA_STATIC_DIR}/postscript") -set(VRNA_SVG_DIR "${VRNA_STATIC_DIR}/svg") -set(VRNA_HEX_DIR "${VRNA_STATIC_DIR}/misc") - -# Source basenames (without extension) that are baked into each header. -# Order matters: must match the #include order ViennaRNA expects. -set(VRNA_PARAM_NAMES - dna_mathews1999 - dna_mathews2004 - rna_andronescu2007 - rna_langdon2018 - rna_misc_special_hairpins - rna_turner1999 - rna_turner2004) - -set(VRNA_RNA_MOD_NAMES - rna_mod_7DA_parameters - rna_mod_inosine_parameters - rna_mod_m6A_parameters - rna_mod_pseudouridine_parameters - rna_mod_purine_parameters - rna_mod_dihydrouridine_parameters) - -set(VRNA_PROBING_NAMES - prior_probing_1M7_paired - prior_probing_1M7_unpaired - prior_probing_DMS_paired - prior_probing_DMS_unpaired) - -set(VRNA_PS_NAMES - structure_plot_macro_base - structure_plot_macro_extras - dot_plot_macro_base - dot_plot_macro_turn - dot_plot_macro_sd - dot_plot_macro_ud - dot_plot_macro_sc_motifs - dot_plot_macro_linear_data - aln_macro_base) - -set(VRNA_SVG_NAMES - structure_plot_header - structure_plot_footer) - -# Helper: emit a custom_command that produces ${out} from ${in} via xxd -i + null terminator. -function(vrna_xxd_hex in out) - get_filename_component(_outdir "${out}" DIRECTORY) - add_custom_command( - OUTPUT "${out}" - DEPENDS "${in}" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${_outdir}" - COMMAND sh -c "'${XXD_EXECUTABLE}' -i < '${in}' > '${out}' && echo ',0x00' >> '${out}'" - COMMENT "xxd -i ${in} -> ${out}" - VERBATIM) -endfunction() - -set(VRNA_GENERATED_FILES "") - -# .par -> static/misc/.hex -foreach(_n ${VRNA_PARAM_NAMES}) - vrna_xxd_hex("${VRNA_MISC_DIR}/${_n}.par" "${VRNA_HEX_DIR}/${_n}.hex") - list(APPEND VRNA_GENERATED_FILES "${VRNA_HEX_DIR}/${_n}.hex") -endforeach() - -# .json (rna_mod_*) -> static/misc/.hex -foreach(_n ${VRNA_RNA_MOD_NAMES}) - vrna_xxd_hex("${VRNA_MISC_DIR}/${_n}.json" "${VRNA_HEX_DIR}/${_n}.hex") - list(APPEND VRNA_GENERATED_FILES "${VRNA_HEX_DIR}/${_n}.hex") -endforeach() - -# .data (probing priors) -> static/misc/.hex -foreach(_n ${VRNA_PROBING_NAMES}) - vrna_xxd_hex("${VRNA_MISC_DIR}/${_n}.data" "${VRNA_HEX_DIR}/${_n}.hex") - list(APPEND VRNA_GENERATED_FILES "${VRNA_HEX_DIR}/${_n}.hex") -endforeach() - -# .ps -> static/.hex (note: .h includes them with no subdir) -foreach(_n ${VRNA_PS_NAMES}) - vrna_xxd_hex("${VRNA_PS_DIR}/${_n}.ps" "${VRNA_STATIC_DIR}/${_n}.hex") - list(APPEND VRNA_GENERATED_FILES "${VRNA_STATIC_DIR}/${_n}.hex") -endforeach() - -# .svg -> static/.hex -foreach(_n ${VRNA_SVG_NAMES}) - vrna_xxd_hex("${VRNA_SVG_DIR}/${_n}.svg" "${VRNA_STATIC_DIR}/${_n}.hex") - list(APPEND VRNA_GENERATED_FILES "${VRNA_STATIC_DIR}/${_n}.hex") -endforeach() - -# Build the four umbrella headers. Their structure is content-stable — -# one `static const unsigned char NAME[] = { #include "X.hex" };` block per -# entry — so we synthesize them from the basename lists above. -function(vrna_emit_header out_path guard prefix include_subdir cdecl_type names_var) - set(_body "#ifndef ${guard}\n#define ${guard}\n\n") - foreach(_n ${${names_var}}) - if(include_subdir) - set(_inc "${include_subdir}/${_n}.hex") - else() - set(_inc "${_n}.hex") - endif() - string(APPEND _body - "static const ${cdecl_type} ${prefix}${_n}[] = {\n" - "#include \"${_inc}\"\n" - "};\n\n") - endforeach() - string(APPEND _body "#endif\n") - file(WRITE "${out_path}.tmp" "${_body}") - # Only overwrite if changed, so we don't trigger unnecessary rebuilds - execute_process(COMMAND ${CMAKE_COMMAND} -E compare_files - "${out_path}.tmp" "${out_path}" - RESULT_VARIABLE _diff - OUTPUT_QUIET ERROR_QUIET) - if(NOT _diff EQUAL 0) - file(RENAME "${out_path}.tmp" "${out_path}") - else() - file(REMOVE "${out_path}.tmp") - endif() -endfunction() - -# energy_parameter_sets.h: combines params + rna_mod sets -set(VRNA_ENERGY_HEADER_NAMES ${VRNA_PARAM_NAMES} ${VRNA_RNA_MOD_NAMES}) -vrna_emit_header( - "${VRNA_STATIC_DIR}/energy_parameter_sets.h" - "VIENNA_RNA_PACKAGE_ENERGY_PARAMETER_SETS_H" - "parameter_set_" - "misc" - "unsigned char" - VRNA_ENERGY_HEADER_NAMES) - -vrna_emit_header( - "${VRNA_STATIC_DIR}/probing_data_priors.h" - "VIENNA_RNA_PACKAGE_PROBING_DATA_PRIORS_H" - "probing_data_" - "misc" - "unsigned char" - VRNA_PROBING_NAMES) - -vrna_emit_header( - "${VRNA_STATIC_DIR}/templates_postscript.h" - "VIENNA_RNA_PACKAGE_PLOT_PS_TEMPLATES_H" - "PS_" - "" - "unsigned char" - VRNA_PS_NAMES) - -vrna_emit_header( - "${VRNA_STATIC_DIR}/templates_svg.h" - "VIENNA_RNA_PACKAGE_PLOT_SVG_TEMPLATES_H" - "SVG_" - "" - "char" - VRNA_SVG_NAMES) - -# Custom target that bundles all .hex generation; sources will depend on it. -add_custom_target(viennarna_static_headers DEPENDS ${VRNA_GENERATED_FILES}) - -# Core sources (from Makefile.am libRNA_conv_la_SOURCES) -set(VRNA_CONV_SOURCES - ${VRNA_SRC_DIR}/fold_compound.c - ${VRNA_SRC_DIR}/dist_vars.c - ${VRNA_SRC_DIR}/treedist.c - ${VRNA_SRC_DIR}/ProfileDist.c - ${VRNA_SRC_DIR}/RNAstruct.c - ${VRNA_SRC_DIR}/stringdist.c - ${VRNA_SRC_DIR}/ProfileAln.c - ${VRNA_SRC_DIR}/duplex.c - ${VRNA_SRC_DIR}/mm.c - ${VRNA_SRC_DIR}/2Dfold.c - ${VRNA_SRC_DIR}/2Dpfold.c - ${VRNA_SRC_DIR}/plex_functions.c - ${VRNA_SRC_DIR}/ali_plex.c - ${VRNA_SRC_DIR}/c_plex.c - ${VRNA_SRC_DIR}/plex.c - ${VRNA_SRC_DIR}/snofold.c - ${VRNA_SRC_DIR}/snoop.c - ${VRNA_SRC_DIR}/perturbation_fold.c - ${VRNA_SRC_DIR}/model.c - ${VRNA_SRC_DIR}/unstructured_domains.c - ${VRNA_SRC_DIR}/heat_capacity.c -) - -# JSON (bundled with ViennaRNA) -set(VRNA_JSON_SOURCES - ${VRNA_SRC_DIR}/../json/json.c -) - -# Cephes math library -set(VRNA_CEPHES_SOURCES - ${VRNA_SRC_DIR}/../cephes/kn.c - ${VRNA_SRC_DIR}/../cephes/const.c - ${VRNA_SRC_DIR}/../cephes/mtherr.c - ${VRNA_SRC_DIR}/../cephes/expn.c -) - -# Eval -set(VRNA_EVAL_SOURCES - ${VRNA_SRC_DIR}/eval/eval_exterior.c - ${VRNA_SRC_DIR}/eval/eval_hairpin.c - ${VRNA_SRC_DIR}/eval/eval_internal.c - ${VRNA_SRC_DIR}/eval/eval_multibranch.c - ${VRNA_SRC_DIR}/eval/eval_gquad.c - ${VRNA_SRC_DIR}/eval/exp_eval_exterior.c - ${VRNA_SRC_DIR}/eval/exp_eval_hairpin.c - ${VRNA_SRC_DIR}/eval/exp_eval_internal.c - ${VRNA_SRC_DIR}/eval/exp_eval_multibranch.c - ${VRNA_SRC_DIR}/eval/eval_structures.c - ${VRNA_SRC_DIR}/eval/eval_covariance.c - ${VRNA_SRC_DIR}/eval/eval_wrappers.c -) - -# Constraints -set(VRNA_CONSTRAINTS_SOURCES - ${VRNA_SRC_DIR}/constraints/constraints.c - ${VRNA_SRC_DIR}/constraints/hard.c - ${VRNA_SRC_DIR}/constraints/soft.c - ${VRNA_SRC_DIR}/constraints/soft_cb_multi.c - ${VRNA_SRC_DIR}/constraints/sc_cb_mod.c - ${VRNA_SRC_DIR}/constraints/sc_cb_mod_parser.c - ${VRNA_SRC_DIR}/constraints/sc_cb_mod_wrappers.c - ${VRNA_SRC_DIR}/constraints/ligand.c -) - -# Probing -set(VRNA_PROBING_SOURCES - ${VRNA_SRC_DIR}/probing/probing.c - ${VRNA_SRC_DIR}/probing/strategy_chain.c - ${VRNA_SRC_DIR}/probing/strategy_deigan.c - ${VRNA_SRC_DIR}/probing/strategy_eddy.c - ${VRNA_SRC_DIR}/probing/strategy_nlogp.c - ${VRNA_SRC_DIR}/probing/strategy_zarringhalam.c - ${VRNA_SRC_DIR}/probing/SHAPE.c -) - -# Data -set(VRNA_DATA_SOURCES - ${VRNA_SRC_DIR}/data/transform.c -) - -# Math -set(VRNA_MATH_SOURCES - ${VRNA_SRC_DIR}/math/fun_bin.c - ${VRNA_SRC_DIR}/math/fun_chain.c - ${VRNA_SRC_DIR}/math/fun_gaussian.c - ${VRNA_SRC_DIR}/math/fun_kde.c - ${VRNA_SRC_DIR}/math/fun_linear.c - ${VRNA_SRC_DIR}/math/fun_log.c - ${VRNA_SRC_DIR}/math/fun_logistic.c -) - -# Structures -set(VRNA_STRUCTURES_SOURCES - ${VRNA_SRC_DIR}/structures/structure_benchmark.c - ${VRNA_SRC_DIR}/structures/centroid.c - ${VRNA_SRC_DIR}/structures/structure_dotbracket.c - ${VRNA_SRC_DIR}/structures/structure_helix.c - ${VRNA_SRC_DIR}/structures/mea.c - ${VRNA_SRC_DIR}/structures/structure_metrics.c - ${VRNA_SRC_DIR}/structures/structure_pairtable.c - ${VRNA_SRC_DIR}/structures/structure_problist.c - ${VRNA_SRC_DIR}/structures/structure_shapes.c - ${VRNA_SRC_DIR}/structures/structure_tree.c - ${VRNA_SRC_DIR}/structures/structure_utils.c -) - -# Sequences -set(VRNA_SEQUENCES_SOURCES - ${VRNA_SRC_DIR}/sequences/alphabet.c - ${VRNA_SRC_DIR}/sequences/sequence.c - ${VRNA_SRC_DIR}/sequences/msa.c - ${VRNA_SRC_DIR}/sequences/seq_utils.c -) - -# Utils -set(VRNA_UTILS_SOURCES - ${VRNA_SRC_DIR}/utils/utils.c - ${VRNA_SRC_DIR}/utils/string_utils.c - ${VRNA_SRC_DIR}/utils/higher_order_functions.c - ${VRNA_SRC_DIR}/utils/cpu.c - ${VRNA_SRC_DIR}/utils/units.c - ${VRNA_SRC_DIR}/utils/log.c - ${VRNA_SRC_DIR}/io/io_utils.c - ${VRNA_SRC_DIR}/io/file_formats.c - ${VRNA_SRC_DIR}/io/file_formats_msa.c - ${VRNA_SRC_DIR}/search/BoyerMoore.c - ${VRNA_SRC_DIR}/io/commands.c -) - -# Plotting -set(VRNA_PLOTTING_SOURCES - ${VRNA_SRC_DIR}/plotting/alignments.c - ${VRNA_SRC_DIR}/plotting/layouts.c - ${VRNA_SRC_DIR}/plotting/probabilities.c - ${VRNA_SRC_DIR}/plotting/structures.c - ${VRNA_SRC_DIR}/plotting/plot_utils.c - ${VRNA_SRC_DIR}/plotting/svg.c - ${VRNA_SRC_DIR}/plotting/eps.c - ${VRNA_SRC_DIR}/plotting/gml.c - ${VRNA_SRC_DIR}/plotting/xrna.c - ${VRNA_SRC_DIR}/plotting/ssv.c - ${VRNA_SRC_DIR}/plotting/RNApuzzler/RNApuzzler.c - ${VRNA_SRC_DIR}/plotting/RNApuzzler/RNAturtle.c -) - -# Naview layout (optional, always include) -set(VRNA_NAVIEW_SOURCES - ${VRNA_SRC_DIR}/plotting/naview/naview.c -) - -# MFE -set(VRNA_MFE_SOURCES - ${VRNA_SRC_DIR}/mfe/mfe.c - ${VRNA_SRC_DIR}/mfe/mfe_window.c - ${VRNA_SRC_DIR}/mfe/mfe_wrappers.c - ${VRNA_SRC_DIR}/mfe/mfe_window_wrappers.c - ${VRNA_SRC_DIR}/mfe/mfe_exterior.c - ${VRNA_SRC_DIR}/mfe/mfe_exterior_window.c - ${VRNA_SRC_DIR}/mfe/mfe_internal.c - ${VRNA_SRC_DIR}/mfe/mfe_multibranch.c - ${VRNA_SRC_DIR}/mfe/mfe_gquad.c - ${VRNA_SRC_DIR}/mfe/alifold.c - ${VRNA_SRC_DIR}/mfe/cofold.c - ${VRNA_SRC_DIR}/mfe/Lfold.c - ${VRNA_SRC_DIR}/mfe/fold.c -) - -# Backtrack -set(VRNA_BACKTRACK_SOURCES - ${VRNA_SRC_DIR}/backtrack/bt_exterior.c - ${VRNA_SRC_DIR}/backtrack/bt_exterior_f5.c - ${VRNA_SRC_DIR}/backtrack/bt_exterior_f3.c - ${VRNA_SRC_DIR}/backtrack/bt_hairpin.c - ${VRNA_SRC_DIR}/backtrack/bt_internal.c - ${VRNA_SRC_DIR}/backtrack/bt_multibranch.c - ${VRNA_SRC_DIR}/backtrack/bt_gquad.c -) - -# Partition function -set(VRNA_PARTFUNC_SOURCES - ${VRNA_SRC_DIR}/partfunc/partfunc.c - ${VRNA_SRC_DIR}/partfunc/pf_window.c - ${VRNA_SRC_DIR}/partfunc/pf_wrappers.c - ${VRNA_SRC_DIR}/partfunc/pf_multifold.c - ${VRNA_SRC_DIR}/partfunc/pf_exterior.c - ${VRNA_SRC_DIR}/partfunc/pf_internal.c - ${VRNA_SRC_DIR}/partfunc/pf_multibranch.c - ${VRNA_SRC_DIR}/partfunc/pf_gquad.c - ${VRNA_SRC_DIR}/partfunc/alipfold.c - ${VRNA_SRC_DIR}/partfunc/part_func_up.c - ${VRNA_SRC_DIR}/partfunc/part_func_co.c - ${VRNA_SRC_DIR}/partfunc/pf_fold.c -) - -# Probabilities -set(VRNA_PROBABILITIES_SOURCES - ${VRNA_SRC_DIR}/probabilities/equilibrium_probs.c - ${VRNA_SRC_DIR}/probabilities/probs_structures.c -) - -# Subopt -set(VRNA_SUBOPT_SOURCES - ${VRNA_SRC_DIR}/subopt/subopt.c - ${VRNA_SRC_DIR}/subopt/subopt_zuker.c - ${VRNA_SRC_DIR}/subopt/subopt_gquad.c -) - -# Sampling -set(VRNA_SAMPLING_SOURCES - ${VRNA_SRC_DIR}/sampling/boltzmann_sampling.c - ${VRNA_SRC_DIR}/sampling/bs_wrappers.c -) - -# Params -set(VRNA_PARAMS_SOURCES - ${VRNA_SRC_DIR}/params/io.c - ${VRNA_SRC_DIR}/params/default.c - ${VRNA_SRC_DIR}/params/params.c - ${VRNA_SRC_DIR}/params/convert.c - ${VRNA_SRC_DIR}/params/salt.c - ${VRNA_SRC_DIR}/params/ribosum.c -) - -# Inverse -set(VRNA_INVERSE_SOURCES - ${VRNA_SRC_DIR}/inverse/inverse.c -) - -# Combinatorics -set(VRNA_COMBINATORICS_SOURCES - ${VRNA_SRC_DIR}/combinatorics/combinatorics.c -) - -# Grammar -set(VRNA_GRAMMAR_SOURCES - ${VRNA_SRC_DIR}/grammar/grammar.c - ${VRNA_SRC_DIR}/grammar/gr_extension_mfe.c - ${VRNA_SRC_DIR}/grammar/gr_extension_pf.c -) - -# Datastructures -set(VRNA_DATASTRUCTURES_SOURCES - ${VRNA_SRC_DIR}/datastructures/array.c - ${VRNA_SRC_DIR}/datastructures/basic_datastructures.c - ${VRNA_SRC_DIR}/datastructures/lists.c - ${VRNA_SRC_DIR}/datastructures/char_stream.c - ${VRNA_SRC_DIR}/datastructures/stream_output.c - ${VRNA_SRC_DIR}/datastructures/string.c - ${VRNA_SRC_DIR}/datastructures/hash_tables.c - ${VRNA_SRC_DIR}/datastructures/heap.c - ${VRNA_SRC_DIR}/datastructures/sparse_mx.c - ${VRNA_SRC_DIR}/datastructures/dp_matrices.c -) - -# Landscape -set(VRNA_LANDSCAPE_SOURCES - ${VRNA_SRC_DIR}/move_set.c - ${VRNA_SRC_DIR}/landscape/move.c - ${VRNA_SRC_DIR}/landscape/findpath.c - ${VRNA_SRC_DIR}/landscape/neighbor.c - ${VRNA_SRC_DIR}/landscape/local_neighbors.c - ${VRNA_SRC_DIR}/landscape/walk.c -) - -# Special const + sanitize -set(VRNA_SPECIAL_SOURCES - ${VRNA_SRC_DIR}/params/special_const.c - ${VRNA_SRC_DIR}/io/sanitize.c -) - -add_library(viennarna OBJECT - ${VRNA_CONV_SOURCES} - ${VRNA_JSON_SOURCES} - ${VRNA_CEPHES_SOURCES} - ${VRNA_EVAL_SOURCES} - ${VRNA_CONSTRAINTS_SOURCES} - ${VRNA_PROBING_SOURCES} - ${VRNA_DATA_SOURCES} - ${VRNA_MATH_SOURCES} - ${VRNA_STRUCTURES_SOURCES} - ${VRNA_SEQUENCES_SOURCES} - ${VRNA_UTILS_SOURCES} - ${VRNA_PLOTTING_SOURCES} - ${VRNA_NAVIEW_SOURCES} - ${VRNA_MFE_SOURCES} - ${VRNA_BACKTRACK_SOURCES} - ${VRNA_PARTFUNC_SOURCES} - ${VRNA_PROBABILITIES_SOURCES} - ${VRNA_SUBOPT_SOURCES} - ${VRNA_SAMPLING_SOURCES} - ${VRNA_PARAMS_SOURCES} - ${VRNA_INVERSE_SOURCES} - ${VRNA_COMBINATORICS_SOURCES} - ${VRNA_GRAMMAR_SOURCES} - ${VRNA_DATASTRUCTURES_SOURCES} - ${VRNA_LANDSCAPE_SOURCES} - ${VRNA_SPECIAL_SOURCES} -) - -# ViennaRNA sources include headers as so we need the parent dir -# Also add VRNA_SRC_DIR itself for relative .inc includes (e.g. "landscape/local_neighbors.inc") -target_include_directories(viennarna PUBLIC - ${VRNA_SRC_DIR}/.. - ${VRNA_SRC_DIR}/../json - ${VRNA_SRC_DIR}/../cephes - ${CMAKE_CURRENT_SOURCE_DIR} # for vrna_config.h as "config.h" - ${VRNA_SRC_DIR} # for relative .inc includes -) - -# Provide config.h — we don't use autotools, so define HAVE_CONFIG_H -# and provide a minimal config.h in this directory -target_compile_definitions(viennarna PRIVATE - HAVE_CONFIG_H - VRNA_WITH_NAVIEW_LAYOUT -) - -# Ensure the .hex byte arrays are generated before any source compiles -add_dependencies(viennarna viennarna_static_headers) diff --git a/lib/viennarna/ViennaRNA b/lib/viennarna/ViennaRNA deleted file mode 160000 index 1ffec79..0000000 --- a/lib/viennarna/ViennaRNA +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1ffec79f5e258896160f7362ced8263450f371dc diff --git a/lib/viennarna/config.h b/lib/viennarna/config.h deleted file mode 100644 index f728c11..0000000 --- a/lib/viennarna/config.h +++ /dev/null @@ -1,37 +0,0 @@ -/* Minimal config.h for ViennaRNA built within MMseqs2 */ -#ifndef VRNA_MMSEQS_CONFIG_H -#define VRNA_MMSEQS_CONFIG_H - -/* Version */ -#define VRNA_VERSION "2.7.2" -#define VRNA_VERSION_MAJOR 2 -#define VRNA_VERSION_MINOR 7 -#define VRNA_VERSION_PATCH 2 - -/* We always have math.h */ -#define HAVE_MATH_H 1 - -/* We always have strdup */ -#define HAVE_STRDUP 1 - -/* Check for erand48 (POSIX) */ -#if defined(__unix__) || defined(__APPLE__) -#define HAVE_ERAND48 1 -#endif - -/* No SVM support */ -/* #undef VRNA_WITH_SVM */ - -/* No GSL support */ -/* #undef VRNA_WITH_GSL */ - -/* Naview layout support */ -#define VRNA_WITH_NAVIEW_LAYOUT 1 - -/* JSON support */ -#define VRNA_WITH_JSON_SUPPORT 1 - -/* No colored TTY output in library mode */ -#define VRNA_WITHOUT_TTY_COLORS 1 - -#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d9efafa..5eb0219 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,21 +21,18 @@ target_include_directories(riboseek-framework BEFORE PUBLIC ${CMAKE_BINARY_DIR}/ add_dependencies(riboseek-framework generated_riboseek) target_include_directories(riboseek-framework PUBLIC ${CMAKE_SOURCE_DIR}/lib) -# Infernal bridge -if (TARGET infernal-bridge) - target_link_libraries(riboseek-framework infernal-bridge) - target_compile_definitions(riboseek-framework PUBLIC -DHAVE_INFERNAL_BRIDGE=1) -endif() +target_link_libraries(riboseek-framework + infernal-vendor-core + infernal-vendor-hmmer + infernal-vendor-easel) +target_include_directories(riboseek-framework PRIVATE + ${INFERNAL_CONFIG_DIR} + ${INFERNAL_ROOT}/src + ${INFERNAL_ROOT}/hmmer/src + ${INFERNAL_ROOT}/easel +) +target_link_libraries(riboseek-framework tornadofold::tornadofold) -# ViennaRNA -if (TARGET viennarna) - get_target_property(VRNA_INCLUDES viennarna INTERFACE_INCLUDE_DIRECTORIES) - if (VRNA_INCLUDES) - target_include_directories(riboseek-framework PUBLIC ${VRNA_INCLUDES}) - endif() - target_link_libraries(riboseek-framework viennarna) - target_compile_definitions(riboseek-framework PUBLIC -DHAVE_RNAFOLD_PREDICT=1) -endif() if (NOT RIBOSEEK_FRAMEWORK_ONLY) add_executable(riboseek riboseek.cpp) diff --git a/src/LocalParameters.cpp b/src/LocalParameters.cpp index 933655a..ebab679 100644 --- a/src/LocalParameters.cpp +++ b/src/LocalParameters.cpp @@ -4,35 +4,62 @@ #include LocalParameters::LocalParameters() : Parameters(), - PARAM_CM_REGION(PARAM_CM_REGION_ID, "--cm-region", "CM region flanking", - "Extract target subregion around prefilter hit for CM alignment.\n" - "Value is flanking fraction of prefilter alignment length (qend-qstart+1) added on each side (0.0 = disabled, use full target)", + PARAM_CM_REGION(PARAM_CM_REGION_ID, "--cm-region", "CM alignment window", + "CM alignment window: flanking pad on each side of the prefilter region, as a fraction of the\n" + "model max hit length W.\n 1.0: full W (default, safest); smaller tightens the window (faster on\n" + "long/genomic targets, may clip hits near the prefilter edge)\n 0: full target (no windowing)", typeid(float), (void *) &cmRegionFlanking, "^[0-9]*(\\.[0-9]+)?$", MMseqsParameter::COMMAND_ALIGN), - PARAM_CM_MODE(PARAM_CM_MODE_ID, "--cm-mode", "CM mode", - "CM alignment mode: 0 = CYK, 1 = inside", + PARAM_CM_MODE(PARAM_CM_MODE_ID, "--cm-mode", "CM scan scoring", + "CM detection scoring: 0 = CYK (max, faster), 1 = Inside (sum, more sensitive)", typeid(int), (void *) &cmMode, "^[0-1]$", MMseqsParameter::COMMAND_ALIGN), + PARAM_CM_ALIGN(PARAM_CM_ALIGN_ID, "--cm-align", "CM alignment method", + "CM hit alignment:\n0: CYK (max-likelihood parse)\n1: Optimal Accuracy (posterior decoding, higher quality)", + typeid(int), (void *) &cmAlign, + "^[0-1]$", MMseqsParameter::COMMAND_ALIGN), + PARAM_CM_ALIGN_BANDED(PARAM_CM_ALIGN_BANDED_ID, "--cm-align-banded", "CM alignment banding", + "CM alignment banding:\n0: nonbanded (exact, fails >8GB on large models)\n1: HMM-banded", + typeid(int), (void *) &cmAlignBanded, + "^[0-1]$", MMseqsParameter::COMMAND_ALIGN), + PARAM_CM_LOCAL(PARAM_CM_LOCAL_ID, "--cm-local", "CM alignment mode", + "CM configuration:\n0: glocal (whole model must align; best for full-length rRNA)\n" + "1: local (partial model matches via local begins/ends; better for fragmentary/divergent hits)", + typeid(int), (void *) &cmLocal, + "^[0-1]$", MMseqsParameter::COMMAND_ALIGN), PARAM_DB_SIZE(PARAM_DB_SIZE_ID, "--db-size", "Database size", "Effective database size (0: use actual size)", typeid(size_t), (void *) &dbSize, "^[0-9]+$", MMseqsParameter::COMMAND_ALIGN), - PARAM_CALIBRATE_CM(PARAM_CALIBRATE_CM_ID, "--calibrate-cm", "Calibrate CM", - "Run Infernal cmcalibrate after cmbuild to fit E-value exp-tail parameters.\n" - "Slow (~1-2s per query). Off by default; CmScan uses a native E-value fallback.", - typeid(bool), (void *) &calibrateCm, - "", MMseqsParameter::COMMAND_MISC), PARAM_CMLITE_MSA_EVAL(PARAM_CMLITE_MSA_EVAL_ID, "--cmlite-msa-eval", "CmLite MSA E-value", "Include only hits with <= this E-value when building the cmbuild seed CM.\n" "All hits from resultDB are still searched afterward; this only filters the CM-building subset.", typeid(double), (void *) &cmliteMsaEvalThr, - "^([0-9eE+.-]+|[iI][nN][fF])$", MMseqsParameter::COMMAND_ALIGN) + "^([0-9eE+.-]+|[iI][nN][fF])$", MMseqsParameter::COMMAND_ALIGN), + PARAM_CMBUILD_ERE(PARAM_CMBUILD_ERE_ID, "--cmbuild-ere", "cmbuild target relative entropy", + "cmbuild: target mean match-state relative entropy in bits.\n" + "Lower: more permissive/sensitive\nhigher: more specific\n<0: Infernal default (0.59 struct / 0.38 no-ss).", + typeid(double), (void *) &cmbuildEre, + "^-?[0-9]*(\\.[0-9]+)?$", MMseqsParameter::COMMAND_PROFILE), + PARAM_CMBUILD_SYMFRAC(PARAM_CMBUILD_SYMFRAC_ID, "--cmbuild-symfrac", "cmbuild match-column fraction", + "cmbuild: fraction of non-gap residues for an MSA column to become a consensus/match column.\n<0: keep ALL columns (query-centric)\n[0,1]: occupancy-based.", + typeid(double), (void *) &cmbuildSymfrac, + "^-?[0-9]*(\\.[0-9]+)?$", MMseqsParameter::COMMAND_PROFILE), + PARAM_CMBUILD_NOSS(PARAM_CMBUILD_NOSS_ID, "--cmbuild-noss", "cmbuild without secondary structure", + "cmbuild:\n0: use SS_cons base pairs\n1: build a sequence-only CM (ignore/require-no structure).", + typeid(int), (void *) &cmbuildNoss, + "^[0-1]$", MMseqsParameter::COMMAND_PROFILE) { - cmRegionFlanking = 0.0f; - cmMode = 0; + cmRegionFlanking = 1.0f; // 1.0 => pad by full W (prior hardcoded behavior) + cmMode = 1; + cmAlign = 0; + cmAlignBanded = 1; + cmLocal = 0; // config: glocal (current shipped default) dbSize = 0; - calibrateCm = false; cmliteMsaEvalThr = DBL_MAX; + cmbuildEre = -1.0; // <0 => Infernal default target rel-entropy + cmbuildSymfrac = -1.0; // <0 => keep all columns (current default) + cmbuildNoss = 0; // use SS_cons (current default) // Register dinuc.out as compiled-in matrix and set as default scoringMatrixFile = MultiParam>(NuclAA("dinuc.out", "dinuc.out")); @@ -43,9 +70,13 @@ LocalParameters::LocalParameters() : Parameters(), alphabetSize = MultiParam>(NuclAA(25, 5)); maskMode = 0; - align.push_back(&PARAM_CM_REGION); + cmscan = align; + cmscan.push_back(&PARAM_CM_REGION); + cmscan.push_back(&PARAM_CM_MODE); + cmscan.push_back(&PARAM_CM_ALIGN); + cmscan.push_back(&PARAM_CM_ALIGN_BANDED); + cmscan.push_back(&PARAM_CM_LOCAL); - // rnaalign inherits the standard align parameters plus --db-size rnaalign = align; rnaalign.push_back(&PARAM_DB_SIZE); @@ -54,7 +85,6 @@ LocalParameters::LocalParameters() : Parameters(), splitstrand.push_back(&PARAM_COMPRESSED); splitstrand.push_back(&PARAM_V); - cmbuild.push_back(&PARAM_CALIBRATE_CM); // hhfilter/rMSA-style row filter on the cmbuild input MSA (off by default) cmbuild.push_back(&PARAM_FILTER_MSA); cmbuild.push_back(&PARAM_FILTER_MAX_SEQ_ID); @@ -64,6 +94,9 @@ LocalParameters::LocalParameters() : Parameters(), cmbuild.push_back(&PARAM_FILTER_NDIFF); cmbuild.push_back(&PARAM_FILTER_MIN_ENABLE); cmbuild.push_back(&PARAM_CMLITE_MSA_EVAL); + cmbuild.push_back(&PARAM_CMBUILD_ERE); + cmbuild.push_back(&PARAM_CMBUILD_SYMFRAC); + cmbuild.push_back(&PARAM_CMBUILD_NOSS); cmbuild.push_back(&PARAM_THREADS); cmbuild.push_back(&PARAM_COMPRESSED); cmbuild.push_back(&PARAM_V); diff --git a/src/LocalParameters.h b/src/LocalParameters.h index ab83f70..82668a1 100644 --- a/src/LocalParameters.h +++ b/src/LocalParameters.h @@ -24,19 +24,30 @@ class LocalParameters : public Parameters { float cmRegionFlanking; int cmMode; + int cmAlign; + int cmAlignBanded; + int cmLocal; size_t dbSize; - bool calibrateCm; double cmliteMsaEvalThr; + double cmbuildEre; + double cmbuildSymfrac; + int cmbuildNoss; PARAMETER(PARAM_CM_REGION) PARAMETER(PARAM_CM_MODE) + PARAMETER(PARAM_CM_ALIGN) + PARAMETER(PARAM_CM_ALIGN_BANDED) + PARAMETER(PARAM_CM_LOCAL) PARAMETER(PARAM_DB_SIZE) - PARAMETER(PARAM_CALIBRATE_CM) PARAMETER(PARAM_CMLITE_MSA_EVAL) + PARAMETER(PARAM_CMBUILD_ERE) + PARAMETER(PARAM_CMBUILD_SYMFRAC) + PARAMETER(PARAM_CMBUILD_NOSS) std::vector splitstrand; std::vector rnaalign; std::vector cmbuild; + std::vector cmscan; }; #endif diff --git a/src/RiboseekBase.cpp b/src/RiboseekBase.cpp index 4d601a6..c6991df 100644 --- a/src/RiboseekBase.cpp +++ b/src/RiboseekBase.cpp @@ -40,7 +40,7 @@ std::vector riboseekCommands = { {"targetDB", DbType::ACCESS_MODE_INPUT, DbType::NEED_DATA, &DbValidator::sequenceDb }, {"resultDB", DbType::ACCESS_MODE_OUTPUT, DbType::NEED_DATA, &DbValidator::alignmentDb }, {"tmpDir", DbType::ACCESS_MODE_OUTPUT, DbType::NEED_DATA, &DbValidator::directory }}}, - {"cmsearch", cmsearch, &localPar.align, COMMAND_ALIGNMENT, + {"cmsearch", cmsearch, &localPar.cmscan, COMMAND_ALIGNMENT, "CM search with in-tree CYK/Inside dynamic programming", "riboseek cmsearch queryCMDB targetDB resultDB alignmentDB\n", "Martin Steinegger ", @@ -49,7 +49,7 @@ std::vector riboseekCommands = { {"targetDB", DbType::ACCESS_MODE_INPUT, DbType::NEED_DATA, &DbValidator::sequenceDb }, {"resultDB", DbType::ACCESS_MODE_INPUT, DbType::NEED_DATA, &DbValidator::resultDb }, {"alignmentDB", DbType::ACCESS_MODE_OUTPUT, DbType::NEED_DATA, &DbValidator::alignmentDb }}}, - {"cmscan", cmscan, &localPar.align, COMMAND_HIDDEN, + {"cmscan", cmscan, &localPar.cmscan, COMMAND_HIDDEN, "Compatibility alias for cmsearch", "riboseek cmscan queryCMDB targetDB resultDB alignmentDB\n", "Martin Steinegger ", diff --git a/src/commons/CMakeLists.txt b/src/commons/CMakeLists.txt index aa0b693..e7b001d 100644 --- a/src/commons/CMakeLists.txt +++ b/src/commons/CMakeLists.txt @@ -1,5 +1,5 @@ set(commons_source_files - commons/RNAFoldBridge.h + commons/RnaFoldingBridge.h commons/DinucleotideMapping.h commons/DinucleotideMapping.cpp PARENT_SCOPE) diff --git a/src/commons/RNAFoldBridge.h b/src/commons/RNAFoldBridge.h deleted file mode 100644 index 94e01f0..0000000 --- a/src/commons/RNAFoldBridge.h +++ /dev/null @@ -1,118 +0,0 @@ -#ifndef MMSEQS_RNAFOLD_BRIDGE_H -#define MMSEQS_RNAFOLD_BRIDGE_H - -#include -#include - -#ifdef HAVE_RNAFOLD_PREDICT - -#include -#include -#include -#include - -extern "C" { -#include -#include -} - -static inline bool rnaFoldPredictDotBracket(const std::string &rna, - std::string &dotBracket, - double *scoreOut) { - dotBracket.clear(); - if (scoreOut != nullptr) { - *scoreOut = 0.0; - } - if (rna.empty()) { - return false; - } - - // Prepare sequence: uppercase, T->U - std::string seq(rna); - for (size_t i = 0; i < seq.size(); ++i) { - seq[i] = static_cast(std::toupper(static_cast(seq[i]))); - if (seq[i] == 'T') { - seq[i] = 'U'; - } - } - - // Allocate structure string - char *structure = (char *)calloc(seq.size() + 1, sizeof(char)); - if (structure == nullptr) { - return false; - } - - // Run MFE prediction - float mfe = vrna_fold(seq.c_str(), structure); - - dotBracket.assign(structure, seq.size()); - if (scoreOut != nullptr) { - *scoreOut = static_cast(mfe); - } - - free(structure); - return true; -} - -// Consensus dot-bracket via single-seq vrna_fold on the QUERY sequence -// (rows[0], ungapped), with the predicted structure mapped back into -// alignment-column space (gap columns receive '.'). Mirrors the reference -// workflow: reformat MSA → RNAfold(query) → splice as #=GC SS_cons → -// cmbuild --hand. vrna_alifold is intentionally not used. -static inline bool rnaFoldAlifoldDotBracket(const std::vector &rows, - std::string &dotBracket, - double *scoreOut) { - dotBracket.clear(); - if (scoreOut != nullptr) *scoreOut = 0.0; - if (rows.empty()) return false; - - const size_t alnLen = rows[0].size(); - if (alnLen == 0) return false; - - std::string ungapped; - std::vector keep(alnLen, false); - ungapped.reserve(alnLen); - for (size_t col = 0; col < alnLen; ++col) { - char c = static_cast(std::toupper(static_cast(rows[0][col]))); - if (c == '-' || c == '.') continue; - if (c == 'T') c = 'U'; - keep[col] = true; - ungapped.push_back(c); - } - if (ungapped.empty()) return false; - - std::string compactSs; - if (!rnaFoldPredictDotBracket(ungapped, compactSs, scoreOut)) return false; - if (compactSs.size() != ungapped.size()) return false; - - dotBracket.assign(alnLen, '.'); - size_t j = 0; - for (size_t col = 0; col < alnLen; ++col) { - if (keep[col]) { - dotBracket[col] = compactSs[j++]; - } - } - return true; -} - -#else - -static inline bool rnaFoldPredictDotBracket(const std::string & /*rna*/, - std::string &dotBracket, - double *scoreOut) { - dotBracket.clear(); - if (scoreOut != nullptr) *scoreOut = 0.0; - return false; -} - -static inline bool rnaFoldAlifoldDotBracket(const std::vector & /*rows*/, - std::string &dotBracket, - double *scoreOut) { - dotBracket.clear(); - if (scoreOut != nullptr) *scoreOut = 0.0; - return false; -} - -#endif - -#endif diff --git a/src/commons/RnaFoldingBridge.h b/src/commons/RnaFoldingBridge.h new file mode 100644 index 0000000..707d5f7 --- /dev/null +++ b/src/commons/RnaFoldingBridge.h @@ -0,0 +1,73 @@ +#ifndef MMSEQS_RNA_FOLDING_BRIDGE_H +#define MMSEQS_RNA_FOLDING_BRIDGE_H + +#include +#include + +#include +#include + +static inline bool rnaFoldPredictDotBracket(const std::string &rna, + std::string &dotBracket, + double *scoreOut) { + dotBracket.clear(); + if (scoreOut != nullptr) { + *scoreOut = 0.0; + } + if (rna.empty()) { + return false; + } + + tornadofold::TornadoFold folder; + int mfe = folder.fold(rna); + dotBracket = folder.traceback(mfe); + if (dotBracket.size() != rna.size()) { + dotBracket.clear(); + return false; + } + if (scoreOut != nullptr) { + *scoreOut = static_cast(mfe) / 100.0; + } + return true; +} + +// Consensus dot-bracket via single-seq folding of the QUERY sequence (rows[0], +// ungapped), with the predicted structure mapped back into alignment-column +// space (gap columns receive '.') +static inline bool rnaFoldAlifoldDotBracket(const std::vector &rows, + std::string &dotBracket, + double *scoreOut) { + dotBracket.clear(); + if (scoreOut != nullptr) *scoreOut = 0.0; + if (rows.empty()) return false; + + const size_t alnLen = rows[0].size(); + if (alnLen == 0) return false; + + std::string ungapped; + std::vector keep(alnLen, false); + ungapped.reserve(alnLen); + for (size_t col = 0; col < alnLen; ++col) { + char c = static_cast(std::toupper(static_cast(rows[0][col]))); + if (c == '-' || c == '.') continue; + if (c == 'T') c = 'U'; + keep[col] = true; + ungapped.push_back(c); + } + if (ungapped.empty()) return false; + + std::string compactSs; + if (!rnaFoldPredictDotBracket(ungapped, compactSs, scoreOut)) return false; + if (compactSs.size() != ungapped.size()) return false; + + dotBracket.assign(alnLen, '.'); + size_t j = 0; + for (size_t col = 0; col < alnLen; ++col) { + if (keep[col]) { + dotBracket[col] = compactSs[j++]; + } + } + return true; +} + +#endif diff --git a/src/rnautils/CMakeLists.txt b/src/rnautils/CMakeLists.txt index b1784e6..35d9571 100644 --- a/src/rnautils/CMakeLists.txt +++ b/src/rnautils/CMakeLists.txt @@ -1,8 +1,9 @@ set(rnautils_source_files rnautils/CmScan.cpp + rnautils/CmBuild.cpp rnautils/GenerateCm.cpp rnautils/splitstrand.cpp rnautils/RnaAlign.cpp rnautils/RnaMatcher.cpp - rnautils/RnaSmithWaterman.cpp - PARENT_SCOPE) + rnautils/RnaSmithWaterman.cpp) +set(rnautils_source_files ${rnautils_source_files} PARENT_SCOPE) diff --git a/src/rnautils/CmBuild.cpp b/src/rnautils/CmBuild.cpp new file mode 100644 index 0000000..aaf2ae8 --- /dev/null +++ b/src/rnautils/CmBuild.cpp @@ -0,0 +1,357 @@ +extern "C" { +#include "infernal.h" +#include "esl_msa.h" +#include "esl_msaweight.h" +#include "esl_vectorops.h" +#include "esl_bitfield.h" +} + +#include +#include +#include +#include +#include +#include +#include + +// One-time global init of logsum tables +void cmInfernalGlobalInit() { + static std::once_flag once; + std::call_once(once, []() { + // SIMD dispatch (static inline; harmless to call) + impl_Init(); + // integer Inside logsum table + init_ilogsum(); + // float logsum table + FLogsumInit(); + // hmmer p7 float table + p7_FLogsumInit(); + }); +} + +void flattenInsertEmissions(CM_t *cm) { + esl_vec_FNorm(cm->null, cm->abc->K); + for (int v = 0; v < cm->M; v++) { + if (cm->sttype[v] == IL_st || cm->sttype[v] == IR_st) { + esl_vec_FSet(cm->e[v], cm->abc->K * cm->abc->K, 0.0f); + esl_vec_FCopy(cm->null, cm->abc->K, cm->e[v]); + } + } +} + +double targetRelent(int clen, int nbps) { + const double esigma = 45.0; + const double reTarget = (nbps > 0) ? 0.59 : 0.38; + double etarget = (esigma - eslCONST_LOG2R * + std::log(2.0 / ((double) clen * (double) (clen + 1)))) / (double) clen; + return (etarget > reTarget) ? etarget : reTarget; +} + +// Round a probability the same way as ASCII CM +static inline float asciiRoundProb(float p, float nullLog, float nullMul) { + if (p == 0.0f) return 0.0f; + double sc = std::log((double) p / (double) nullLog) * 1.44269504; + double r = std::round(sc * 1000.0) / 1000.0; + return (float) (std::exp(r / 1.44269504) * (double) nullMul); +} + +// Quantize to the ASCII CM-file precision +static void quantizeCM(CM_t *cm) { + const int K = cm->abc->K; + std::vector nullOrig(cm->null, cm->null + K); + for (int x = 0; x < K; x++) { + cm->null[x] = asciiRoundProb(cm->null[x], 1.0f / (float) K, 1.0f / (float) K); + } + for (int v = 0; v < cm->M; v++) { + if (cm->sttype[v] != B_st) + for (int x = 0; x < cm->cnum[v]; x++) { + cm->t[v][x] = asciiRoundProb(cm->t[v][x], 1.0f, 1.0f); + } + if (cm->sttype[v] == MP_st) { + for (int x = 0; x < K; x++) { + for (int y = 0; y < K; y++) { + cm->e[v][x*K+y] = asciiRoundProb(cm->e[v][x*K+y], nullOrig[x]*nullOrig[y], cm->null[x]*cm->null[y]); + } + } + } else if (cm->sttype[v]==ML_st || cm->sttype[v]==MR_st || cm->sttype[v]==IL_st || cm->sttype[v]==IR_st) { + for (int x = 0; x < K; x++) { + cm->e[v][x] = asciiRoundProb(cm->e[v][x], nullOrig[x], cm->null[x]); + } + } + } +} + +bool cmbuildFromAlignment(const std::string &name, + const std::vector &sqnames, + const std::vector &aseqs, + const std::string &ssCons, + std::string &cmText, std::string &err, + double ereOverride, double symfracOpt, bool noss, + bool forScan, bool useInside, bool useLocal, + CM_t **ret_cm) { + if (aseqs.empty()) { err = "no sequences"; return false; } + if (sqnames.size() != aseqs.size()) { err = "name/row count mismatch"; return false; } + const int nseq = (int) aseqs.size(); + const int alen = (int) aseqs[0].size(); + if (alen == 0) { err = "empty alignment rows"; return false; } + for (const std::string &r : aseqs) + if ((int) r.size() != alen) { err = "ragged alignment rows"; return false; } + if (!noss) { + if (ssCons.empty()) { err = "no SS_cons annotation"; return false; } + if ((int) ssCons.size() != alen) { err = "SS_cons length mismatch"; return false; } + } + + cmInfernalGlobalInit(); + + char errbuf[eslERRBUFSIZE]; + errbuf[0] = '\0'; + + ESL_ALPHABET *abc = esl_alphabet_Create(eslRNA); + ESL_MSA *msa = esl_msa_CreateDigital(abc, nseq, alen); + for (int i = 0; i < nseq; i++) { + esl_abc_Digitize(abc, aseqs[i].c_str(), msa->ax[i]); + msa->wgt[i] = 1.0; + esl_strdup(sqnames[i].c_str(), -1, &msa->sqname[i]); + } + + // --hand: all-'x' RF => every alignment column is a consensus/match column, + // so clen == alen == query length and cmscan traces map 1:1 to query coords. + msa->rf = (char *) malloc((size_t) alen + 1); + memset(msa->rf, 'x', (size_t) alen); + msa->rf[alen] = '\0'; + msa->ss_cons = (char *) malloc((size_t) alen + 1); + // no base pairs -> sequence-only CM + if (noss) { + memset(msa->ss_cons, '.', (size_t) alen); + } else { + memcpy(msa->ss_cons, ssCons.c_str(), (size_t) alen); + } + msa->ss_cons[alen] = '\0'; + esl_msa_SetName(msa, name.empty() ? "query" : name.c_str(), -1); + + uint32_t checksum = 0; + esl_msa_Checksum(msa, &checksum); + + // PB (Henikoff position-based) relative weights over the RF consensus cols + ESL_MSAWEIGHT_CFG *wcfg = esl_msaweight_cfg_Create(); + wcfg->ignore_rf = FALSE; // use the RF line we set + esl_msaweight_PB_adv(wcfg, msa, NULL); + esl_msaweight_cfg_Destroy(wcfg); + + // overwrite each fragment's terminal gaps with the missing symbol for parsetree truncation + ESL_BITFIELD *fragassign = NULL; + esl_msa_MarkFragments(msa, 0.5f, &fragassign); + const ESL_DSQ missing = (ESL_DSQ) esl_abc_XGetMissing(abc); + for (int i = 0; i < nseq; i++) { + if (!esl_bitfield_IsSet(fragassign, i)) continue; + for (int apos = 1; apos <= alen; apos++) { + if (esl_abc_XIsResidue(abc, msa->ax[i][apos])) break; + msa->ax[i][apos] = missing; + } + for (int apos = alen; apos >= 1; apos--) { + if (esl_abc_XIsResidue(abc, msa->ax[i][apos])) break; + msa->ax[i][apos] = missing; + } + } + esl_bitfield_Destroy(fragassign); + + // guide tree + expanded CM + CM_t *cm = NULL; + Parsetree_t *mtr = NULL; + const int useRf = (symfracOpt < 0.0) ? TRUE : FALSE; // <0: all columns via RF; else occupancy + const float symfr = (symfracOpt < 0.0) ? 0.5f : (float) symfracOpt; + if (HandModelmaker(msa, errbuf, useRf, FALSE, FALSE, symfr, &cm, &mtr) != eslOK) { + err = std::string("HandModelmaker: ") + errbuf; + esl_msa_Destroy(msa); esl_alphabet_Destroy(abc); + return false; + } + + float *null = NULL; + DefaultNullModel(abc, &null); + CMSetNullModel(cm, null); + + // rebalance + CM_t *balanced = NULL; + if (CMRebalance(cm, errbuf, &balanced) != eslOK) { + err = std::string("CMRebalance: ") + errbuf; + FreeParsetree(mtr); FreeCM(cm); free(null); + esl_msa_Destroy(msa); esl_alphabet_Destroy(abc); + return false; + } + FreeCM(cm); + cm = balanced; + + int nbps = 0; + for (int nd = 0; nd < cm->nodes; nd++) { + if (cm->ndtype[nd] == MATP_nd) nbps++; + } + Prior_t *pri = Prior_Default(nbps == 0 ? TRUE : FALSE); + + // count transitions and joint emissions + int *used_el = (int *) malloc(((size_t) alen + 1) * sizeof(int)); + esl_vec_ISet(used_el, alen + 1, FALSE); + std::vector tr(nseq, NULL); + for (int i = 0; i < nseq; i++) { + if (Transmogrify(cm, errbuf, mtr, msa->ax[i], used_el, alen, &tr[i]) != eslOK) { + err = std::string("Transmogrify: ") + errbuf; + for (int k = 0; k < i; k++) FreeParsetree(tr[k]); + free(used_el); Prior_Destroy(pri); FreeParsetree(mtr); FreeCM(cm); free(null); + esl_msa_Destroy(msa); esl_alphabet_Destroy(abc); + return false; + } + ParsetreeCountExceptTruncatedMPs(cm, tr[i], msa->ax[i], msa->wgt[i]); + } + + // truncated-MP marginals against a stable double copy of the MP counts + std::vector dbl_e(cm->M, NULL); + const int K = cm->abc->K; + for (int v = 0; v < cm->M; v++) { + if (cm->sttype[v] == MP_st) { + dbl_e[v] = (double *) malloc((size_t) K * K * sizeof(double)); + for (int a = 0; a < K * K; a++) dbl_e[v][a] = (double) cm->e[v][a]; + } + } + for (int i = 0; i < nseq; i++) { + ParsetreeCountOnlyTruncatedMPs(cm, tr[i], msa->ax[i], msa->wgt[i], dbl_e.data(), pri); + } + for (int v = 0; v < cm->M; v++) free(dbl_e[v]); + + // flanking ROOT_IL/IR inserts are not learned from the alignment. + cm_zero_flanking_insert_counts(cm, errbuf); + + // detach-check dual inserts + cm_find_and_detach_dual_inserts(cm, TRUE, FALSE); + + // 1emit map, EL self-transition score, sequence counts + cm->emap = CreateEmitMap(cm); + cm->el_selfsc = sreLOG2(0.94); + cm->nseq = nseq; + cm->eff_nseq = (float) nseq; + + // null2/null3 omega + cm->null2_omega = DEFAULT_NULL2_OMEGA; + cm->null3_omega = DEFAULT_NULL3_OMEGA; + + // name. + cm_SetName(cm, msa->name); + + // entropy weighting + { + const int clen = cm->clen; + const double etarget = (ereOverride >= 0.0) ? ereOverride : targetRelent(clen, nbps); + double hmm_re = 0.0, neff = 0.0; + cm_EntropyWeight(cm, pri, etarget, 0.1, (double) cm->nseq, FALSE, &hmm_re, &neff); + cm->eff_nseq = (float) neff; + cm_Rescale(cm, (float) (neff / (double) nseq)); + } + + // priors, detach, flatten inserts, renormalize + PriorifyCM(cm, pri); + cm_find_and_detach_dual_inserts(cm, FALSE, TRUE); + flattenInsertEmissions(cm); + CMRenormalize(cm); + if (forScan) { + // round to CM-file precision + renorm, exactly as cm_file_Read + quantizeCM(cm); + CMRenormalize(cm); + } + + // QDB bands + W + logodds + consensus + cm->beta_W = 1e-7; + cm->qdbinfo->beta1 = 1e-7; + cm->qdbinfo->beta2 = 1e-15; + // global mode: no CM_CONFIG_LOCAL + cm->config_opts |= CM_CONFIG_QDB; + if (forScan) { + cm->config_opts |= CM_CONFIG_SCANMX | CM_CONFIG_NONBANDEDMX; + if (useLocal) { + cm->config_opts |= CM_CONFIG_LOCAL | CM_CONFIG_HMMLOCAL | CM_CONFIG_HMMEL; + } + if (useInside) { + cm->search_opts |= CM_SEARCH_INSIDE; + } + } + if (cm_Configure(cm, errbuf, -1) != eslOK) { + err = std::string("cm_Configure: ") + errbuf; + for (int i = 0; i < nseq; i++) FreeParsetree(tr[i]); + free(used_el); Prior_Destroy(pri); FreeParsetree(mtr); FreeCM(cm); free(null); + esl_msa_Destroy(msa); esl_alphabet_Destroy(abc); + return false; + } + + // consensus for CM_ALIDISPLAY + cm_SetConsensus(cm, cm->cmcons, NULL); + + if (forScan) { + // caller owns cm; abc stays alive via cm->abc + *ret_cm = cm; + for (int i = 0; i < nseq; i++) { + FreeParsetree(tr[i]); + } + free(used_el); + Prior_Destroy(pri); + FreeParsetree(mtr); + free(null); + esl_msa_Destroy(msa); + return true; + } + + // Disable unused p7 HMM filter with very few iteration + if (cm->mlp7 != NULL) { + // not thread safe? + static std::mutex p7Mutex; + std::lock_guard lock(p7Mutex); + double gfmu = 0.0, gflambda = 0.0; + if (cm_p7_Calibrate(cm->mlp7, errbuf, 200, 200, 100, ESL_MAX(100, 2 * cm->clen), + 10, 10, 10, 10, 0.055, 0.065, &gfmu, &gflambda) == eslOK) { + cm_p7_hmm_SetConsensus(cm->mlp7); + cm_SetFilterHMM(cm, cm->mlp7, gfmu, gflambda); + } + } + + // checksum, validate, write + cm->checksum = checksum; + cm->flags |= CMH_CHKSUM; + if (cm_Validate(cm, 1e-4, errbuf) != eslOK) { + err = std::string("cm_Validate: ") + errbuf; + for (int i = 0; i < nseq; i++) FreeParsetree(tr[i]); + free(used_el); Prior_Destroy(pri); FreeParsetree(mtr); FreeCM(cm); free(null); + esl_msa_Destroy(msa); esl_alphabet_Destroy(abc); + return false; + } + + // serialize to an in-memory buffer + bool ok = true; + { + char *buf = NULL; + size_t sz = 0; + FILE *fp = open_memstream(&buf, &sz); + if (fp == NULL) { + err = "open_memstream failed"; + ok = false; + } else { + if (cm_file_WriteASCII(fp, -1, cm) != eslOK) { + err = "cm_file_WriteASCII failed"; + ok = false; + } + fclose(fp); + if (ok) { + cmText.assign(buf, sz); + } + free(buf); + } + } + + // Cleanup + for (int i = 0; i < nseq; i++) { + FreeParsetree(tr[i]); + } + free(used_el); + Prior_Destroy(pri); + FreeParsetree(mtr); + FreeCM(cm); + free(null); + esl_msa_Destroy(msa); + esl_alphabet_Destroy(abc); + return ok; +} diff --git a/src/rnautils/CmBuildScan.h b/src/rnautils/CmBuildScan.h new file mode 100644 index 0000000..1b65b4b --- /dev/null +++ b/src/rnautils/CmBuildScan.h @@ -0,0 +1,39 @@ +#ifndef RIBOSEEK_CMBUILDSCAN_H +#define RIBOSEEK_CMBUILDSCAN_H + +#include "DBReader.h" +#include "SubstitutionMatrix.h" +#include "MsaFilter.h" +#include "Sequence.h" +#include +#include + +class LocalParameters; +struct cm_s; +typedef struct cm_s CM_t; + +// per-thread build context +struct CmBuildCtx { + DBReader* qDbr = nullptr; + DBReader* tDbr = nullptr; + DBReader* resultReader = nullptr; + SubstitutionMatrix* subMat = nullptr; + MsaFilter* msaFilter = nullptr; + Sequence* targetMapper = nullptr; + std::vector qid_vec; + float alifoldMinCov = 0.70f; + float minColCoverage = 0.30f; + bool doMsaFilter = false; + bool decodeTargetDinuc = false; + bool targetGpuDb = false; + int targetSeqType = 0; +}; + +extern bool buildQueryCmText( + LocalParameters &par, CmBuildCtx &ctx, size_t id, + unsigned int thread_idx, std::string &cmText, std::string &err, + bool forScan = false, bool useInside = true, bool useLocal = false, + CM_t **ret_cm = nullptr +); + +#endif diff --git a/src/rnautils/CmScan.cpp b/src/rnautils/CmScan.cpp index 0f3eb77..63bd907 100644 --- a/src/rnautils/CmScan.cpp +++ b/src/rnautils/CmScan.cpp @@ -1,66 +1,29 @@ -#include "LocalCommandDeclarations.h" #include "LocalParameters.h" #include "Debug.h" -#include "MMseqsMPI.h" -#include "MathUtil.h" #include "DBWriter.h" #include "DBReader.h" #include "FileUtil.h" #include "Matcher.h" #include "Util.h" #include "NucleotideMatrix.h" +#include "CmBuildScan.h" +#include "Util.h" #include "Sequence.h" -#include "simd.h" - -#ifdef OPENMP -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#if defined(__AVX2__) -#include -#define CM_HAS_SSE2 1 -#elif defined(__SSE2__) -#include -#define CM_HAS_SSE2 1 -#else -#include -#define CM_HAS_SSE2 1 + +#ifdef OPENMP +#include #endif -namespace { +extern "C" { +#include "infernal.h" +#include "esl_sq.h" +#include "esl_stopwatch.h" +} -static const double NEG_INF = -std::numeric_limits::infinity(); -static const double LN2 = 0.69314718055994530942; -static const double LOG2E = 1.4426950408889634074; -typedef uint16_t StateId; -#if defined(__GNUC__) || defined(__clang__) -#define CM_ALWAYS_INLINE inline __attribute__((always_inline)) -#else -#define CM_ALWAYS_INLINE inline -#endif +extern void cmInfernalGlobalInit(); struct FastaSeq { unsigned int key = 0; @@ -68,327 +31,90 @@ struct FastaSeq { std::string seq; }; -enum CmStateType { - CM_ST_MP, - CM_ST_ML, - CM_ST_MR, - CM_ST_IL, - CM_ST_IR, - CM_ST_D, - CM_ST_S, - CM_ST_B, - CM_ST_E, - CM_ST_UNKNOWN -}; - -// Node-type encoding for cm_localize. Matches Infernal cm.h ndtype except we -// only carry the types relevant to begin/end eligibility decisions. -enum CmNodeType { - CM_ND_UNKNOWN = 0, - CM_ND_ROOT, - CM_ND_MATP, - CM_ND_MATL, - CM_ND_MATR, - CM_ND_BEGL, - CM_ND_BEGR, - CM_ND_BIF, - CM_ND_END, -}; - -struct CmState { - CmStateType type; - int idx; - int cfirst; - int cnum; - std::vector trans; // for !B: size cnum - std::vector emit; // 4 for singlet emitters, 16 for MP, empty otherwise - std::vector emitF; // float parallel for hot-path use - int mapLeft; // consensus/map coordinate, -1 if none - int mapRight; // consensus/map coordinate, -1 if none - int dmin1; - int dmax1; - // Local-mode (cm_localize) metadata. - int nodeIdx = -1; - CmNodeType nodeType = CM_ND_UNKNOWN; - bool isFirstOfNode = false; - double beginSc = NEG_INF; // log2(begin prob); finite only at begin-eligible heads - double endSc = NEG_INF; // log2(end prob); finite only at end-eligible heads - // Truncation DP: marginal emission scores for L/R modes (Infernal cm_CalcMargLikScores). - // For MATP_MP only, size 4 per nucleotide; empty otherwise. - std::vector lmesc; // L-mode: keep left, marginalize right partner - std::vector rmesc; // R-mode: keep right, marginalize left partner -}; - -struct ExactStateExec { - CmStateType type; - int niShift; - int dConsume; - bool consumesLeft; - bool consumesRight; - int bLeft; - int bRight; - StateId trDst4[4]; - float trScF4[4]; - double trSc4[4]; - size_t trOff; - int trCount; - const float *emitPtr; - int emitSize; - int splitKMin; - int splitKMax; - int splitRMin; - int splitRMax; - int dmin; - int dmax; - double null2Agg[4]; -}; - -struct InfernalExactModel { - struct ExpTail { - bool valid = false; - double lambda = 0.0; - double muExtrap = 0.0; - double dbsize = 0.0; - int nrandhits = 0; - }; - - int clen; - int w; - bool hasNull = false; - double nullProb[4] = {0.25, 0.25, 0.25, 0.25}; // A,C,G,U - double null2Omega = 1.0 / 65536.0; - double null3Omega = 1.0 / 65536.0; - // Local-mode parameters (Infernal cm_localize). Currently parsed only for - // diagnostic use; DP wiring is the next step in the 2YGH_1 fix. - double pBegin = 0.0; // local-begin probability (0 = glocal) - double pEnd = 0.0; // local-end probability (0 = glocal) - double elSelf = 0.0; // EL self-transition score (nats per residue, ≤0) - bool hasLocalCfg = false; - ExpTail expLC; - ExpTail expLI; - ExpTail expGC; - ExpTail expGI; - std::vector states; - int rootState; - // Truncation DP support (gated on MMSEQS_CMSCAN_TRUNC_DP=1; otherwise empty). - // emap mirrors Infernal CMEmitMap_t (display.c:CreateEmitMap): per-node - // consensus position bounds + EL-anchor position. - int nNodes = 0; - std::vector nodeFirstState; // size nNodes; first state index of each node - std::vector nodeType; - std::vector emapLpos; // node -> consensus 0..clen+1 (inclusive low bound) - std::vector emapRpos; // node -> consensus 0..clen+1 (inclusive high bound) - std::vector emapEpos; // node -> consensus position EL insertion follows - // Expected per-state occupancy psi[v] (Infernal cm_ExpectedPositionOccupancy). - // Linear-space probability of state v being entered in a sample. Size states.size(). - std::vector psi; - // Truncation penalties (Infernal cm_tr_penalties_Create). - // [type][stateIdx]; type 0=5'+3', 1=5'only, 2=3'only. Indexed by global state id. - // Two parallel sets: trpG = global mode (no local), trpL = local mode. - // Stored as log2 probabilities (NEG_INF if not eligible for that truncation type). - std::vector> trpG; - std::vector> trpL; - bool hasTruncDp = false; -}; - -template -struct RawBuffer { - T *ptr; - size_t cap; - - RawBuffer() : ptr(NULL), cap(0) {} - ~RawBuffer() { std::free(ptr); } - - RawBuffer(const RawBuffer &) = delete; - RawBuffer &operator=(const RawBuffer &) = delete; - - bool ensure(size_t need) { - if (need <= cap) { - return true; - } - size_t newCap = (cap == 0) ? 1024 : cap; - while (newCap < need) { - newCap <<= 1; - } - void *np = std::realloc(ptr, newCap * sizeof(T)); - if (np == NULL) { - return false; - } - ptr = static_cast(np); - cap = newCap; - return true; - } - - void release() { - std::free(ptr); - ptr = NULL; - cap = 0; - } - - T *data() { return ptr; } - const T *data() const { return ptr; } -}; - -struct ExactScanWorkspace { - std::vector seqCode; - std::vector prefA; - std::vector prefC; - std::vector prefG; - std::vector prefU; - std::vector log2Int; - RawBuffer vit; - std::vector in; - RawBuffer stateBase; - RawBuffer bSplitBegByVD; - RawBuffer bSplitEndByVD; - RawBuffer bSplitTmp; - RawBuffer negRow; - std::vector trScF; - RawBuffer insD; // deck-based inside: layout [d * M * (localN+1) + v * (localN+1) + li] - std::vector memoVitDense; - std::vector memoInDense; - std::vector memoVitSeen; - std::vector memoInSeen; - uint32_t memoVitGen = 1; - uint32_t memoInGen = 1; - std::unordered_map memoVitMap; - std::unordered_map memoInMap; - // Per-state ragged (v, i, d) chart for Infernal-compat exactVitRec. - // Each state v gets bandSize[v] * (N+1) cells, allocated contiguously - // and indexed via cumulative chartOffset[v]. Total cells = chartOffset[M]. - // Bands derive from cmbuild's QDB output (CmState::dmin1/dmax1) and shrink - // memory ~30x vs the previous flat M*N*maxSpan layout. - std::vector exactChart; - std::vector exactChartSeen; - uint32_t exactChartGen = 1; - std::vector exactChartOffset; // M+1 entries, exactChartOffset[M] = total cells - std::vector exactChartBandDmin; // M entries - std::vector exactChartBandSize; // M entries (= dmax-dmin+1, or 0 if empty) +inline std::string trim(const std::string &s) { + size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b])) != 0) ++b; + size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1])) != 0) --e; + return s.substr(b, e - b); +} - void releaseFastDeckBuffers() { - vit.release(); - stateBase.release(); - bSplitBegByVD.release(); - bSplitEndByVD.release(); - bSplitTmp.release(); - negRow.release(); - } -}; +inline char normalizeBase(char c) { + c = static_cast(std::toupper(static_cast(c))); + if (c == 'T') return 'U'; + return c; +} -static inline size_t rawBufferCapacityFor(size_t need) { - size_t cap = 1024; - while (cap < need) { - cap <<= 1; +char complementBase(char c) { + switch (normalizeBase(c)) { + case 'A': return 'U'; + case 'C': return 'G'; + case 'G': return 'C'; + case 'U': return 'A'; + default: return 'N'; } - return cap; } -static inline size_t rawBufferBytesFor(size_t need, size_t elemSize) { - if (need == 0) { - return 0; +std::string reverseComplement(const std::string &seq) { + std::string rc(seq.size(), 'N'); + for (size_t i = 0; i < seq.size(); ++i) { + rc[seq.size() - 1 - i] = complementBase(seq[i]); } - return rawBufferCapacityFor(need) * elemSize; + return rc; } -static size_t cmFastDeckBudgetBytes() { - static const size_t budget = []() -> size_t { - const char *env = std::getenv("MMSEQS_CMSCAN_EXACT_FASTDECK_BUDGET_MB"); - if (env != NULL && *env != '\0') { - const long mb = std::atol(env); - if (mb <= 0) { - return std::numeric_limits::max(); - } - return static_cast(mb) * 1024ULL * 1024ULL; - } - return std::numeric_limits::max(); - }(); - return budget; +// cigar encoding +std::string rleTraceOps(const std::string &ops) { + if (ops.empty()) return "NA"; + std::string out; + out.reserve(ops.size() * 2); + char cur = ops[0]; + int run = 1; + for (size_t i = 1; i < ops.size(); ++i) { + if (ops[i] == cur) { ++run; } + else { out += std::to_string(run); out.push_back(cur); cur = ops[i]; run = 1; } + } + out += std::to_string(run); + out.push_back(cur); + return out; } -class FastDeckMemoryGate { -public: - explicit FastDeckMemoryGate(size_t bytes) : bytes_(bytes), active_(false) { - const size_t budget = cmFastDeckBudgetBytes(); - if (budget == std::numeric_limits::max() || bytes_ == 0) { - return; - } - std::unique_lock lock(mutex()); - condition().wait(lock, [&]() { - const size_t inUse = bytesInUse(); - if (bytes_ > budget) { - return inUse == 0; - } - return inUse + bytes_ <= budget; - }); - bytesInUse() += bytes_; - active_ = true; - } - ~FastDeckMemoryGate() { - if (!active_) { - return; - } - { - std::lock_guard lock(mutex()); - size_t &inUse = bytesInUse(); - inUse = (inUse >= bytes_) ? (inUse - bytes_) : 0; +// cigar decoding +std::string reverseRleTraceOps(const std::string &rle) { + struct Run { int n; char op; }; + std::vector runs; + runs.reserve(rle.size() / 2 + 1); + size_t pos = 0; + while (pos < rle.size()) { + if (!std::isdigit(static_cast(rle[pos]))) return rle; + int n = 0; + while (pos < rle.size() && std::isdigit(static_cast(rle[pos]))) { + n = n * 10 + (rle[pos] - '0'); ++pos; } - condition().notify_all(); - } - bool active() const { return active_; } - FastDeckMemoryGate(const FastDeckMemoryGate &) = delete; - FastDeckMemoryGate &operator=(const FastDeckMemoryGate &) = delete; -private: - static std::mutex &mutex() { - static std::mutex m; - return m; - } - static std::condition_variable &condition() { - static std::condition_variable cv; - return cv; - } - static size_t &bytesInUse() { - static size_t value = 0; - return value; + if (pos >= rle.size() || n <= 0) return rle; + runs.push_back(Run{n, rle[pos++]}); } - size_t bytes_; - bool active_; -}; - -struct FastDeckBufferRelease { - FastDeckBufferRelease(ExactScanWorkspace &workspace, bool enabled) : ws(workspace), enabled(enabled) {} - ~FastDeckBufferRelease() { - if (enabled) { - ws.releaseFastDeckBuffers(); + std::string out; + out.reserve(rle.size()); + for (std::vector::const_reverse_iterator it = runs.rbegin(); it != runs.rend(); ++it) { + if (!out.empty() && out.back() == it->op) { + size_t digitEnd = out.size() - 1; + size_t digitStart = digitEnd; + while (digitStart > 0 && std::isdigit(static_cast(out[digitStart - 1]))) --digitStart; + int prev = std::atoi(out.c_str() + digitStart); + out.erase(digitStart); + out += std::to_string(prev + it->n); + out.push_back(it->op); + } else { + out += std::to_string(it->n); + out.push_back(it->op); } } - ExactScanWorkspace &ws; - bool enabled; -}; - -static inline std::string trim(const std::string &s) { - size_t b = 0; - while (b < s.size() && std::isspace(static_cast(s[b])) != 0) { - ++b; - } - size_t e = s.size(); - while (e > b && std::isspace(static_cast(s[e - 1])) != 0) { - --e; - } - return s.substr(b, e - b); -} - -static inline char normalizeBase(char c) { - c = static_cast(std::toupper(static_cast(c))); - if (c == 'T') { - return 'U'; - } - return c; + return out; } -static int effectiveDecodeSeqType(int dbtype, bool useDinucMapping) { - if (!useDinucMapping) { - return dbtype; - } +int effectiveDecodeSeqType(int dbtype, bool useDinucMapping) { + if (!useDinucMapping) return dbtype; unsigned int ext = DBReader::getExtendedDbtype(dbtype); ext |= Parameters::DBTYPE_EXTENDED_DINUCLEOTIDE; if (Parameters::isEqualDbtype(dbtype, Parameters::DBTYPE_HMM_PROFILE)) { @@ -397,6516 +123,371 @@ static int effectiveDecodeSeqType(int dbtype, bool useDinucMapping) { return DBReader::setExtendedDbtype(Parameters::DBTYPE_AMINO_ACIDS, ext); } -static std::string decodeMappedSequenceToRna(const Sequence &seq, - const BaseMatrix &subMat, - const unsigned char *num2outputnum) { +std::string decodeMappedSequenceToRna(const Sequence &seq, const BaseMatrix &subMat, + const unsigned char *num2outputnum) { std::string out; out.reserve(static_cast(seq.L)); for (int i = 0; i < seq.L; ++i) { unsigned char code = seq.numSequence[i]; - if (num2outputnum != NULL) { - code = num2outputnum[code]; - } + if (num2outputnum != NULL) code = num2outputnum[code]; char c = normalizeBase(subMat.num2aa[code]); - if (c != 'A' && c != 'C' && c != 'G' && c != 'U') { - c = 'N'; - } + if (c != 'A' && c != 'C' && c != 'G' && c != 'U') c = 'N'; out.push_back(c); } - return out; -} - -static inline double logSumExp(double a, double b) { - if (a == NEG_INF) { - return b; - } - if (b == NEG_INF) { - return a; - } - if (a < b) { - std::swap(a, b); - } - const double d = b - a; // <= 0 - if (d < -50.0) { - return a; - } - return a + std::log1p(std::exp(d)); -} - -static bool cmFastMathEnabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_FASTMATH"); - cached = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - return cached == 1; -} - -static bool cmFastLogsumEnabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_FASTLOGSUM"); - cached = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - return cached == 1; -} - -static bool cmDebugExtEnabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_DEBUG_EXT"); - cached = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - return cached == 1; -} - -static bool cmExactFastDeckEnabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_EXACT_FASTDECK"); - // Keep fast-deck enabled by default; allow explicit opt-out. - cached = (env != NULL && std::string(env) == "0") ? 0 : 1; - } - return cached == 1; -} - -static bool cmTruncModesEnabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_TRUNC"); - // Infernal-like default: truncation modes enabled unless explicitly disabled. - cached = (env != NULL && std::string(env) == "0") ? 0 : 1; - } - return cached == 1; -} - -static bool cmNull3Enabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_NULL3"); - cached = (env != NULL && std::string(env) == "0") ? 0 : 1; - } - return cached == 1; -} - -// Optimal-accuracy alignment opt-in. When set, runInfernalExactScan switches -// from CYK (max over paths) to Inside+Outside+OA (posterior-coverage trace), -// reusing the vit buffer across phases: Inside in vit, then alpha overwritten -// in-place by OA scores during the OA fill, then vit reinterpreted as a uint8 -// shadow matrix for traceback. One extra float buffer (beta) is allocated. -static bool cmOAEnabled() { - static int cached = -1; - if (cached == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_OA"); - cached = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - return cached == 1; -} - -static inline double fastLog1pExp(double x) { - // Stable approximation for log(1 + exp(x)); tuned for x <= 0 path used by logSumExp. - if (x <= -20.0) { - return 0.0; - } - if (x >= 20.0) { - return x; - } - const float e2 = static_cast(x * LOG2E); - const float ex = static_cast(MathUtil::fpow2(e2)); - const float onePlus = 1.0f + ex; - return static_cast(MathUtil::flog2(onePlus)) * LN2; -} - -static inline double logSumExpMaybeFast(double a, double b) { - if (!cmFastMathEnabled()) { - return logSumExp(a, b); - } - if (a == NEG_INF) { - return b; - } - if (b == NEG_INF) { - return a; - } - if (a < b) { - std::swap(a, b); - } - const double d = b - a; // <= 0 - if (d < -50.0) { - return a; - } - return a + fastLog1pExp(d); -} - -static inline float bSplitBestFull(const float *left, const float *rightStart, int rightStep, int d, float negInf) { - float best = negInf; - int k = 0; -#if defined(__AVX2__) - auto hmax256 = [](__m256 v) -> float { - const __m128 lo = _mm256_castps256_ps128(v); - const __m128 hi = _mm256_extractf128_ps(v, 1); - __m128 m = _mm_max_ps(lo, hi); - m = _mm_max_ps(m, _mm_movehl_ps(m, m)); - m = _mm_max_ps(m, _mm_shuffle_ps(m, m, _MM_SHUFFLE(1, 1, 1, 1))); - return _mm_cvtss_f32(m); - }; - const __m256 negV = _mm256_set1_ps(negInf); - const __m256i offs = _mm256_setr_epi32(0, rightStep, 2 * rightStep, 3 * rightStep, - 4 * rightStep, 5 * rightStep, 6 * rightStep, 7 * rightStep); - __m256 bestV = negV; - for (; k + 7 <= d; k += 8) { - const __m256 l = _mm256_loadu_ps(left + k); - const __m256i base = _mm256_set1_epi32(k * rightStep); - const __m256i idx = _mm256_add_epi32(base, offs); - const __m256 r = _mm256_i32gather_ps(rightStart, idx, 4); - const __m256 valid = _mm256_and_ps(_mm256_cmp_ps(l, negV, _CMP_NEQ_OQ), - _mm256_cmp_ps(r, negV, _CMP_NEQ_OQ)); - const __m256 s = _mm256_add_ps(l, r); - const __m256 c = _mm256_blendv_ps(negV, s, valid); - bestV = _mm256_max_ps(bestV, c); - } - { - const float hv = hmax256(bestV); - if (hv > best) { - best = hv; - } - } -#endif - for (; k + 3 <= d; k += 4) { - const float l0 = left[k]; - const float r0 = rightStart[(k + 0) * rightStep]; - if (l0 != negInf && r0 != negInf) { - const float cand = l0 + r0; - if (cand > best) { - best = cand; - } - } - const float l1 = left[k + 1]; - const float r1 = rightStart[(k + 1) * rightStep]; - if (l1 != negInf && r1 != negInf) { - const float cand = l1 + r1; - if (cand > best) { - best = cand; - } - } - const float l2 = left[k + 2]; - const float r2 = rightStart[(k + 2) * rightStep]; - if (l2 != negInf && r2 != negInf) { - const float cand = l2 + r2; - if (cand > best) { - best = cand; - } - } - const float l3 = left[k + 3]; - const float r3 = rightStart[(k + 3) * rightStep]; - if (l3 != negInf && r3 != negInf) { - const float cand = l3 + r3; - if (cand > best) { - best = cand; - } - } - } - for (; k <= d; ++k) { - const float l = left[k]; - const float r = rightStart[k * rightStep]; - if (l != negInf && r != negInf) { - const float cand = l + r; - if (cand > best) { - best = cand; - } - } - } - return best; -} - -static inline float bSplitBestPacked(const float *left, const float *rightPacked, int len, float negInf) { - float best = negInf; - int k = 0; -#if defined(__AVX2__) - auto hmax256 = [](__m256 v) -> float { - const __m128 lo = _mm256_castps256_ps128(v); - const __m128 hi = _mm256_extractf128_ps(v, 1); - __m128 m = _mm_max_ps(lo, hi); - m = _mm_max_ps(m, _mm_movehl_ps(m, m)); - m = _mm_max_ps(m, _mm_shuffle_ps(m, m, _MM_SHUFFLE(1, 1, 1, 1))); - return _mm_cvtss_f32(m); - }; - const __m256 negV = _mm256_set1_ps(negInf); - __m256 bestV = negV; - for (; k + 7 < len; k += 8) { - const __m256 l = _mm256_loadu_ps(left + k); - const __m256 r = _mm256_loadu_ps(rightPacked + k); - const __m256 valid = _mm256_and_ps(_mm256_cmp_ps(l, negV, _CMP_NEQ_OQ), - _mm256_cmp_ps(r, negV, _CMP_NEQ_OQ)); - const __m256 s = _mm256_add_ps(l, r); - const __m256 c = _mm256_blendv_ps(negV, s, valid); - bestV = _mm256_max_ps(bestV, c); - } - { - const float hv = hmax256(bestV); - if (hv > best) { - best = hv; - } - } -#endif - for (; k < len; ++k) { - const float l = left[k]; - const float r = rightPacked[k]; - if (l != negInf && r != negInf) { - const float cand = l + r; - if (cand > best) { - best = cand; - } - } - } - return best; -} - -static inline double expMaybeFast(double x) { - if (cmFastMathEnabled()) { - return static_cast(MathUtil::fpow2(static_cast(x * LOG2E))); - } - return std::exp(x); -} - -static inline double logMaybeFast(double x) { - if (cmFastMathEnabled()) { - return static_cast(MathUtil::flog2(static_cast(x))) * LN2; - } - return std::log(x); -} - -static inline double exp2MaybeFast(double x) { - if (cmFastMathEnabled()) { - return static_cast(MathUtil::fpow2(static_cast(x))); - } - return std::exp2(x); -} - -static inline double log2MaybeFast(double x) { - if (cmFastMathEnabled()) { - return static_cast(MathUtil::flog2(static_cast(x))); - } - return std::log2(x); -} - -struct LogSumAcc { - double maxVal; - double scaledSum; - bool has; - - LogSumAcc() : maxVal(NEG_INF), scaledSum(0.0), has(false) {} - - inline void add(double x) { - if (!has) { - maxVal = x; - scaledSum = 1.0; - has = true; - return; - } - if (x > maxVal) { - const double d = maxVal - x; - scaledSum = ((d < -50.0) ? 0.0 : scaledSum * expMaybeFast(d)) + 1.0; - maxVal = x; - } else { - const double d = x - maxVal; - if (d >= -50.0) { - scaledSum += expMaybeFast(d); - } - } - } - - inline double value() const { - if (!has) { - return NEG_INF; - } - return maxVal + logMaybeFast(scaledSum); - } -}; - -static inline double log2SumExp2(double a, double b) { - if (a == NEG_INF) { - return b; - } - if (b == NEG_INF) { - return a; - } - if (a < b) { - std::swap(a, b); - } - const double d = b - a; - if (d < -80.0) { - return a; - } - return a + std::log2(1.0 + std::exp2(d)); -} - -struct FastLog2OnePlusPow2NegLut { - static const int TABLE_SCALE = 64; - static const int TABLE_MAX_X = 32; - static const int TABLE_N = TABLE_MAX_X * TABLE_SCALE + 1; - float v[TABLE_N]; - FastLog2OnePlusPow2NegLut() { - for (int i = 0; i < TABLE_N; ++i) { - const double xv = static_cast(i) / static_cast(TABLE_SCALE); - v[i] = static_cast(std::log2(1.0 + std::exp2(-xv))); - } - } -}; - -static CM_ALWAYS_INLINE float log2OnePlusPow2NegApproxF(float x) { - if (x <= 0.0f) { - return 1.0f; - } - if (x >= static_cast(FastLog2OnePlusPow2NegLut::TABLE_MAX_X)) { - return 0.0f; - } - static const FastLog2OnePlusPow2NegLut lut; - int idx = static_cast(x * static_cast(FastLog2OnePlusPow2NegLut::TABLE_SCALE) + 0.5f); - if (idx < 0) idx = 0; - if (idx >= FastLog2OnePlusPow2NegLut::TABLE_N) idx = FastLog2OnePlusPow2NegLut::TABLE_N - 1; - return lut.v[idx]; -} - -static CM_ALWAYS_INLINE float log2SumExp2ApproxF(float a, float b) { - static const float NEG_INF_F = -std::numeric_limits::infinity(); - if (a == NEG_INF_F) { - return b; - } - if (b == NEG_INF_F) { - return a; - } - if (a < b) { - const float tmp = a; - a = b; - b = tmp; - } - const float d = a - b; // >= 0 - if (d >= static_cast(FastLog2OnePlusPow2NegLut::TABLE_MAX_X)) { - return a; - } - return a + log2OnePlusPow2NegApproxF(d); -} - -static CM_ALWAYS_INLINE void log2AccFastAddF(float x, bool &has, float &acc) { - if (!has) { - acc = x; - has = true; - return; - } - acc = log2SumExp2ApproxF(acc, x); -} - -#if defined(CM_HAS_SSE2) -// 4-lane SIMD log-sum-exp in log2 space, using the scalar LUT for the -// log2(1 + 2^(-d)) correction. Drop-in replacement for `_mm_max_ps` in the -// fast-deck fill when running Inside (sum over paths) instead of CYK (max). -// -// Lane semantics: for each i, returns log2(2^a[i] + 2^b[i]). -// NEG_INF handling: if max(a,b) == -inf the result is -inf; if only one is -// -inf the other is returned. This matches the scalar fall-through path. -static CM_ALWAYS_INLINE __m128 log2SumExp2ApproxFSse(__m128 a, __m128 b) { - static const float NEG_INF_F_LSEX = -std::numeric_limits::infinity(); - const __m128 mx = _mm_max_ps(a, b); - const __m128 mn = _mm_min_ps(a, b); - // d = mx - mn >= 0 when both finite. When mn == -inf, the diff is +inf - // (or NaN if mx is also -inf); the scalar LUT clamps to TABLE_MAX_X so - // non-NaN large d returns 0. We mask the both-NEG_INF lane separately. - const __m128 niV = _mm_set1_ps(NEG_INF_F_LSEX); - const __m128 isMxNI = _mm_cmpeq_ps(mx, niV); - // Replace mn with mx in the both-NEG_INF lanes so d collapses to 0 and - // the LUT lookup is well-defined; we'll clobber the result with NEG_INF - // afterwards via the mask. - const __m128 mnSafe = _mm_or_ps(_mm_and_ps(isMxNI, mx), _mm_andnot_ps(isMxNI, mn)); - const __m128 d = _mm_sub_ps(mx, mnSafe); - alignas(16) float dArr[4]; - _mm_store_ps(dArr, d); - alignas(16) float adjArr[4]; - adjArr[0] = log2OnePlusPow2NegApproxF(dArr[0]); - adjArr[1] = log2OnePlusPow2NegApproxF(dArr[1]); - adjArr[2] = log2OnePlusPow2NegApproxF(dArr[2]); - adjArr[3] = log2OnePlusPow2NegApproxF(dArr[3]); - const __m128 sum = _mm_add_ps(mx, _mm_load_ps(adjArr)); - // Both-NEG_INF lanes -> NEG_INF; otherwise sum. - return _mm_or_ps(_mm_and_ps(isMxNI, niV), _mm_andnot_ps(isMxNI, sum)); -} -#endif - -static inline void log2AccExactAdd(double x, bool &has, double &maxVal, double &scaledSum) { - if (!has) { - maxVal = x; - scaledSum = 1.0; - has = true; - return; - } - if (x > maxVal) { - const double d = maxVal - x; - scaledSum = ((d < -80.0) ? 0.0 : scaledSum * exp2MaybeFast(d)) + 1.0; - maxVal = x; - } else { - const double d = x - maxVal; - if (d >= -80.0) { - scaledSum += exp2MaybeFast(d); - } - } -} - -static inline double log2AccExactValue(bool has, double maxVal, double scaledSum) { - return has ? (maxVal + log2MaybeFast(scaledSum)) : NEG_INF; -} - -template -struct LogSumAccBase2T; - -template <> -struct LogSumAccBase2T { - double maxVal; - double scaledSum; - bool has; - - LogSumAccBase2T() : maxVal(NEG_INF), scaledSum(0.0), has(false) {} - - inline void add(double x) { - if (!has) { - maxVal = x; - scaledSum = 1.0; - has = true; - return; - } - if (x > maxVal) { - const double d = maxVal - x; - scaledSum = ((d < -80.0) ? 0.0 : scaledSum * exp2MaybeFast(d)) + 1.0; - maxVal = x; - } else { - const double d = x - maxVal; - if (d >= -80.0) { - scaledSum += exp2MaybeFast(d); - } - } - } - - inline double value() const { - if (!has) { - return NEG_INF; - } - return maxVal + log2MaybeFast(scaledSum); - } -}; - -template <> -struct LogSumAccBase2T { - float maxVal; - bool has; - - LogSumAccBase2T() : maxVal(-std::numeric_limits::infinity()), has(false) {} - - inline void add(double x) { - const float xf = static_cast(x); - if (!has) { - maxVal = xf; - has = true; - return; - } - maxVal = log2SumExp2ApproxF(maxVal, xf); - } - - inline double value() const { - return has ? static_cast(maxVal) : NEG_INF; - } -}; - -typedef LogSumAccBase2T LogSumAccBase2Exact; -typedef LogSumAccBase2T LogSumAccBase2Fast; - -static double emitBitToProbSingle(double bitSc, const InfernalExactModel &model, int a) { - if (!std::isfinite(bitSc) || a < 0 || a >= 4 || model.nullProb[a] <= 0.0) { - return 0.0; - } - return std::exp2(bitSc) * model.nullProb[a]; -} - -static double emitBitToProbPair(double bitSc, const InfernalExactModel &model, int a, int b) { - if (!std::isfinite(bitSc) || a < 0 || a >= 4 || b < 0 || b >= 4 || - model.nullProb[a] <= 0.0 || model.nullProb[b] <= 0.0) { - return 0.0; - } - return std::exp2(bitSc) * model.nullProb[a] * model.nullProb[b]; -} - -static double scoreCorrectionNull2BitsFromTrace(const InfernalExactModel &model, - const double modelAggRaw[4], - const int obsCount[4]) { - if (!model.hasNull || model.null2Omega <= 0.0) { - return 0.0; - } - double total = 0.0; - for (int a = 0; a < 4; ++a) { - total += modelAggRaw[a]; - } - if (total <= 0.0) { - return 0.0; - } - double score = 0.0; - for (int a = 0; a < 4; ++a) { - if (obsCount[a] <= 0 || model.nullProb[a] <= 0.0) { - continue; - } - const double p = std::max(1e-12, modelAggRaw[a] / total); - score += static_cast(obsCount[a]) * std::log2(p / model.nullProb[a]); - } - score += std::log2(model.null2Omega); - return log2SumExp2(0.0, score); -} - -static bool infernalExactScoreToEvalue(const InfernalExactModel &model, - bool insideMode, - char hitMode, - double scoreBits, - double targetDbResidues, - double &outEvalue) { - const bool truncLike = (hitMode == 'L' || hitMode == 'R' || hitMode == 'T'); - const InfernalExactModel::ExpTail &exp = insideMode - ? (truncLike ? model.expGI : model.expLI) - : (truncLike ? model.expGC : model.expLC); - if (!exp.valid || exp.lambda <= 0.0 || exp.dbsize <= 0.0 || exp.nrandhits <= 0 || targetDbResidues <= 0.0) { - return false; - } - const double curEffDbSize = (targetDbResidues / exp.dbsize) * static_cast(exp.nrandhits); - const double p = std::exp(-exp.lambda * (scoreBits - exp.muExtrap)); - outEvalue = p * curEffDbSize; - return std::isfinite(outEvalue); -} - -static bool looksLikeInfernalCm(const std::string &path) { - std::ifstream in(path.c_str()); - if (!in.good()) { - return false; - } - std::string first; - std::getline(in, first); - first = trim(first); - // Handle optional UTF-8 BOM. - if (first.size() >= 3 && - static_cast(first[0]) == 0xEF && - static_cast(first[1]) == 0xBB && - static_cast(first[2]) == 0xBF) { - first = first.substr(3); - } - return first.find("INFERNAL1/") != std::string::npos; -} - -static int parseMaybeInt(const std::string &tok) { - if (tok.empty() || tok == "-") { - return -1; - } - char *end = NULL; - long v = std::strtol(tok.c_str(), &end, 10); - if (end == tok.c_str() || *end != '\0') { - return -1; - } - return static_cast(v); -} - -static bool parseMaybeDouble(const std::string &tok, double &out) { - if (tok == "*") { - out = NEG_INF; - return true; - } - char *end = NULL; - out = std::strtod(tok.c_str(), &end); - return end != tok.c_str() && *end == '\0'; -} - -static CmStateType parseCmStateType(const std::string &tok) { - if (tok == "MP") { - return CM_ST_MP; - } - if (tok == "ML") { - return CM_ST_ML; - } - if (tok == "MR") { - return CM_ST_MR; - } - if (tok == "IL") { - return CM_ST_IL; - } - if (tok == "IR") { - return CM_ST_IR; - } - if (tok == "D") { - return CM_ST_D; - } - if (tok == "S") { - return CM_ST_S; - } - if (tok == "B") { - return CM_ST_B; - } - if (tok == "E") { - return CM_ST_E; - } - return CM_ST_UNKNOWN; -} - -static int baseToIdx(char c) { - c = normalizeBase(c); - if (c == 'A') { - return 0; - } - if (c == 'C') { - return 1; - } - if (c == 'G') { - return 2; - } - if (c == 'U') { - return 3; - } - return -1; -} - -static char complementBase(char c) { - switch (normalizeBase(c)) { - case 'A': return 'U'; - case 'C': return 'G'; - case 'G': return 'C'; - case 'U': return 'A'; - default: return 'N'; - } -} - -static std::string reverseComplement(const std::string &seq) { - std::string rc(seq.size(), 'N'); - for (size_t i = 0; i < seq.size(); ++i) { - rc[seq.size() - 1 - i] = complementBase(seq[i]); - } - return rc; -} - -static double parseInfernalAsciiScore(const std::string &tok) { - if (tok == "*") { - return NEG_INF; - } - double v = 0.0; - if (!parseMaybeDouble(tok, v)) { - return NEG_INF; - } - return v; -} - -static double parseInfernalAsciiProb(const std::string &tok, double nullProb) { - if (tok == "*") { - return 0.0; - } - double bits = 0.0; - if (!parseMaybeDouble(tok, bits)) { - return nullProb; - } - return std::exp2(bits) * nullProb; -} - -static CmNodeType parseCmNodeType(const std::string &tok) { - if (tok == "ROOT") return CM_ND_ROOT; - if (tok == "MATP") return CM_ND_MATP; - if (tok == "MATL") return CM_ND_MATL; - if (tok == "MATR") return CM_ND_MATR; - if (tok == "BEGL") return CM_ND_BEGL; - if (tok == "BEGR") return CM_ND_BEGR; - if (tok == "BIF") return CM_ND_BIF; - if (tok == "END") return CM_ND_END; - return CM_ND_UNKNOWN; -} - -// ===================================================================== -// Stage 6: native port of Infernal cm_TrCYKInsideAlign (cm_dpalign_trunc.c) -// Fills J/L/R/T DP and shadow matrices for full-sequence trCYK alignment. -// Not yet wired into the alignment path; integration is Stage 8. -// ===================================================================== -struct TrCykResult { - double score = -1e30; - char mode = 'J'; - int b = 0; - std::vector>> Jyshadow, Lyshadow, Ryshadow; - std::vector>> Jkshadow, Lkshadow, Rkshadow, Tkshadow; - std::vector>> Lkmode, Rkmode; - std::vector>> Jalpha, Lalpha, Ralpha, Talpha; -}; - -// TRMODE_* offset constants — match Infernal cm_alphabet.h. -// Match Infernal infernal.h: TRMODE_T=0, TRMODE_R=1, TRMODE_L=2, TRMODE_J=3, -// TRMODE_UNKNOWN=4. TRMODE_*_OFFSET picks DP source for shadow decoding. -static const int8_t TR_TRMODE_T = 0; -static const int8_t TR_TRMODE_R = 1; -static const int8_t TR_TRMODE_L = 2; -static const int8_t TR_TRMODE_J = 3; -static const int8_t TR_TRMODE_UNKNOWN = 4; -static const int TR_TRMODE_J_OFFSET = 0; -static const int TR_TRMODE_L_OFFSET = 10; -static const int TR_TRMODE_R_OFFSET = 20; -static const int8_t TR_USED_LOCAL_BEGIN = 101; -static const int8_t TR_USED_EL = 102; -static const int8_t TR_USED_TRUNC_BEGIN = 103; -static const int8_t TR_USED_TRUNC_END = 104; - -static inline bool trNotImpossible(double x) { - // Infernal NOT_IMPOSSIBLE: x > -1e8. Our endsc/trp use NEG_INF = -inf - // when ineligible, so any finite value qualifies. - return std::isfinite(x); -} - -// Mirrors esl_abc_FAvgScore: arithmetic average of esc[0..K-1] over degeneracy -// expansion class. For fully-degenerate X/N over {A,C,G,U}: avg of all four. -template -static inline float trEmitDegen1(const T *esc, int8_t b) { - if (b >= 0 && b <= 3) return (float)esc[(size_t)b]; - double s = 0.0; - for (int x = 0; x < 4; ++x) s += (double)esc[(size_t)x]; - return (float)(s * 0.25); -} - -// MP pair emission with degeneracy on either or both sides. Arithmetic avg -// over the cartesian product of degeneracy expansions for bi and bj. -static inline float trEmitDegenPair(const float *esc, int Kp, int8_t bi, int8_t bj) { - const bool degI = (bi < 0 || bi > 3); - const bool degJ = (bj < 0 || bj > 3); - if (!degI && !degJ) return esc[(size_t)((int)bi * Kp + (int)bj)]; - int xLo = degI ? 0 : (int)bi, xHi = degI ? 3 : (int)bi; - int yLo = degJ ? 0 : (int)bj, yHi = degJ ? 3 : (int)bj; - double s = 0.0; - int n = 0; - for (int x = xLo; x <= xHi; ++x) { - for (int y = yLo; y <= yHi; ++y) { - s += (double)esc[(size_t)(x * Kp + y)]; - ++n; - } - } - return (float)(s / (double)n); -} - -static const TrCykResult& runTrCYKInsideAlign(const InfernalExactModel &m, - const std::vector &dsq, - int L, - char preset_mode, - int pty_idx) { - const float TR_IMPOSSIBLE = -1e30f; - const int M = (int)m.states.size(); - - // fill_{L,R,T} based on preset_mode (mirrors cm_TrFillFromMode). - bool fill_L = false, fill_R = false, fill_T = false; - switch (preset_mode) { - case 'J': fill_L = false; fill_R = false; fill_T = false; break; - case 'L': fill_L = true; fill_R = false; fill_T = false; break; - case 'R': fill_L = false; fill_R = true; fill_T = false; break; - case 'T': fill_L = true; fill_R = true; fill_T = true; break; - default: fill_L = true; fill_R = true; fill_T = true; break; // unknown - } - - // Per-thread arena. Vectors grow monotonically across calls — never shrink — - // so the inner float/int storage is reused. Reset overhead is just std::fill - // over [0..LP1) per (v, j) row, no malloc/free per envelope. - thread_local TrCykResult r; - // Reset the score-summary fields (data arrays are overwritten by reset+DP below). - r.score = -1e30; - r.mode = 'J'; - r.b = 0; - - const int LP1 = L + 1; - auto resetAlpha = [&](std::vector>> &a) { - if ((int)a.size() < M + 1) a.resize((size_t)M + 1); - for (int v = 0; v <= M; ++v) { - auto &mv = a[(size_t)v]; - if ((int)mv.size() < LP1) mv.resize((size_t)LP1); - for (int j = 0; j < LP1; ++j) { - auto &jv = mv[(size_t)j]; - if ((int)jv.size() < LP1) jv.resize((size_t)LP1); - std::fill(jv.begin(), jv.begin() + LP1, TR_IMPOSSIBLE); - } - } - }; - auto resetByteSh = [&](std::vector>> &a, int8_t val) { - if ((int)a.size() < M + 1) a.resize((size_t)M + 1); - for (int v = 0; v <= M; ++v) { - auto &mv = a[(size_t)v]; - if ((int)mv.size() < LP1) mv.resize((size_t)LP1); - for (int j = 0; j < LP1; ++j) { - auto &jv = mv[(size_t)j]; - if ((int)jv.size() < LP1) jv.resize((size_t)LP1); - std::fill(jv.begin(), jv.begin() + LP1, val); - } - } - }; - auto resetIntSh = [&](std::vector>> &a) { - if ((int)a.size() < M + 1) a.resize((size_t)M + 1); - for (int v = 0; v <= M; ++v) { - auto &mv = a[(size_t)v]; - if ((int)mv.size() < LP1) mv.resize((size_t)LP1); - for (int j = 0; j < LP1; ++j) { - auto &jv = mv[(size_t)j]; - if ((int)jv.size() < LP1) jv.resize((size_t)LP1); - std::fill(jv.begin(), jv.begin() + LP1, 0); - } - } - }; - - resetAlpha(r.Jalpha); - if (fill_L) resetAlpha(r.Lalpha); - if (fill_R) resetAlpha(r.Ralpha); - if (fill_T) resetAlpha(r.Talpha); // Talpha only used at B states; allocate full for simplicity. - - resetByteSh(r.Jyshadow, (int8_t)TR_USED_EL); - if (fill_L) resetByteSh(r.Lyshadow, (int8_t)TR_USED_EL); - if (fill_R) resetByteSh(r.Ryshadow, (int8_t)TR_USED_EL); - resetIntSh(r.Jkshadow); - if (fill_L) resetIntSh(r.Lkshadow); - if (fill_R) resetIntSh(r.Rkshadow); - if (fill_T) resetIntSh(r.Tkshadow); - if (fill_L) resetByteSh(r.Lkmode, (int8_t)TR_TRMODE_J); - if (fill_R) resetByteSh(r.Rkmode, (int8_t)TR_TRMODE_J); - - // el_scA[d] = elSelf * d. elSelf is in our model already in log2-bits. - std::vector el_scA((size_t)LP1, 0.0f); - const double elSelf = m.elSelf; - for (int d = 0; d <= L; ++d) { - el_scA[(size_t)d] = (float)(elSelf * (double)d); - } - - const bool localOn = m.hasLocalCfg; - - // MMSEQS_CMSCAN_TRUNC_DISABLE_EL=1: gate End-Local pickup off in the DP. - // Mirrors --notrunc-style behavior where EL is unreachable, so the trace - // never picks an EL parsetree path. Justified because our renderer outputs - // fixed-CLEN columns and cannot represent EL inserts the way Infernal's - // Parsetrees2Alignment does (dynamic-width MSA with per-cpos EL blocks). - static const char *const elDisableEnv = std::getenv("MMSEQS_CMSCAN_TRUNC_DISABLE_EL"); - const bool disableEL = (elDisableEnv != nullptr && elDisableEnv[0] == '1'); - // MMSEQS_CMSCAN_TRUNC_NO_TRPENALTY=1: include v=0 in main recurrence and - // gate trunc-entry pickup off. Mirrors Infernal --notrunc behavior where - // alpha[0][L][L] is filled by the natural S→child recurrence at root, - // not via a USED_TRUNC_BEGIN pickup. Trace then walks normally from v=0. - static const char *const noTrPenEnv = std::getenv("MMSEQS_CMSCAN_TRUNC_NO_TRPENALTY"); - const bool noTrPenalty = (noTrPenEnv != nullptr && noTrPenEnv[0] == '1'); - - // If local ends are on (and EL not disabled), EL deck (v=M) holds el_scA[d] - // for j,d in [0,L]. With EL disabled, leave the deck at TR_IMPOSSIBLE. - if (localOn && !disableEL) { - for (int j = 0; j <= L; ++j) { - for (int d = 0; d <= j; ++d) { - r.Jalpha[(size_t)M][(size_t)j][(size_t)d] = el_scA[(size_t)d]; - if (fill_L) r.Lalpha[(size_t)M][(size_t)j][(size_t)d] = el_scA[(size_t)d]; - if (fill_R) r.Ralpha[(size_t)M][(size_t)j][(size_t)d] = el_scA[(size_t)d]; - } - } - } - - // Trackers for begin states across modes. - int Jb = 0, Lb = 0, Rb = 0, Tb = 0; - // For noTrPenalty (--notrunc-equivalent): track best local-begin pickup - // separately, commit only at end (mirrors Infernal cm_CYKInsideAlign). - float bsc_local = TR_IMPOSSIBLE; - int blb_local = -1; - - // Helper to read transition score for state v at child yoffset. - auto tscOf = [&](int v, int yoffset) -> float { - const CmState &s = m.states[(size_t)v]; - if (yoffset < 0 || (size_t)yoffset >= s.trans.size()) return TR_IMPOSSIBLE; - double t = s.trans[(size_t)yoffset]; - if (!std::isfinite(t)) return TR_IMPOSSIBLE; - return (float)t; - }; - - // Main recursion: v = M-1 down to 1. v=0 (ROOT_S) handled via trpenalty hand-off below. - // When noTrPenalty, also include v=0 so alpha[0] is filled by the natural - // S-state recurrence (no truncation entry needed). - const int loopStop = noTrPenalty ? -1 : 0; - for (int v = M - 1; v > loopStop; --v) { - const CmState &s = m.states[(size_t)v]; - const CmStateType st = s.type; - const int cfirst = s.cfirst; - const int cnum = s.cnum; - - int sd = 0, sdl = 0, sdr = 0; - switch (st) { - case CM_ST_MP: sd = 2; sdl = 1; sdr = 1; break; - case CM_ST_ML: case CM_ST_IL: sd = 1; sdl = 1; sdr = 0; break; - case CM_ST_MR: case CM_ST_IR: sd = 1; sdl = 0; sdr = 1; break; - default: sd = 0; sdl = 0; sdr = 0; break; - } - - // Re-init this state's J/L/R deck if a local end is reachable from v. - // With disableEL, skip this entirely so EL is never the picked path. - const double endsc_v = s.endSc; - if (localOn && !disableEL && trNotImpossible(endsc_v)) { - const float endF = (float)endsc_v; - const simd_float vEndF = simdf32_set(endF); - const int VS = VECSIZE_FLOAT; - // SIMD over d for each j: Jalpha[v][j][d] = el_scA[d - sd] + endF - for (int j = 0; j <= L; ++j) { - float *Jrow = r.Jalpha[(size_t)v][(size_t)j].data(); - int d = sd; - for (; d + VS - 1 <= j; d += VS) { - simd_float vEl = simdf32_loadu(&el_scA[(size_t)(d - sd)]); - simdf32_storeu(Jrow + d, simdf32_add(vEl, vEndF)); - } - for (; d <= j; ++d) { - Jrow[d] = el_scA[(size_t)(d - sd)] + endF; - } - } - if (fill_L) { - for (int j = 0; j <= L; ++j) { - float *Lrow = r.Lalpha[(size_t)v][(size_t)j].data(); - int d = sdl; - for (; d + VS - 1 <= j; d += VS) { - simd_float vEl = simdf32_loadu(&el_scA[(size_t)(d - sdl)]); - simdf32_storeu(Lrow + d, simdf32_add(vEl, vEndF)); - } - for (; d <= j; ++d) { - Lrow[d] = el_scA[(size_t)(d - sdl)] + endF; - } - } - } - if (fill_R) { - for (int j = 0; j <= L; ++j) { - float *Rrow = r.Ralpha[(size_t)v][(size_t)j].data(); - int d = sdr; - for (; d + VS - 1 <= j; d += VS) { - simd_float vEl = simdf32_loadu(&el_scA[(size_t)(d - sdr)]); - simdf32_storeu(Rrow + d, simdf32_add(vEl, vEndF)); - } - for (; d <= j; ++d) { - Rrow[d] = el_scA[(size_t)(d - sdr)] + endF; - } - } - } - } - - // Per-state recursion by type. - if (st == CM_ST_E) { - for (int j = 0; j <= L; ++j) { - r.Jalpha[(size_t)v][(size_t)j][0] = 0.0f; - if (fill_L) r.Lalpha[(size_t)v][(size_t)j][0] = 0.0f; - if (fill_R) r.Ralpha[(size_t)v][(size_t)j][0] = 0.0f; - } - } - else if (st == CM_ST_IL || st == CM_ST_ML) { - const float *esc = s.emitF.data(); - const int Ryoffset0 = (st == CM_ST_IL) ? 1 : 0; - for (int j = sdr; j <= L; ++j) { - const int j_sdr = j - sdr; - for (int d = sd; d <= j; ++d) { - const int d_sd = d - sd; - int i = j - d + 1; - for (int yoff = 0; yoff < cnum; ++yoff) { - const int y = cfirst + yoff; - const float tsc = tscOf(v, yoff); - float sc = r.Jalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (sc > r.Jalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = sc; - r.Jyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - if (fill_L) { - float scL = r.Lalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (scL > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scL; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_L_OFFSET); - } - } - } - // Single-residue emit on left position i. - const int8_t bi = (i >= 1 && i <= L) ? dsq[(size_t)i] : (int8_t)4; - const float emitL = (i >= 1 && i <= L) ? trEmitDegen1(esc, bi) : TR_IMPOSSIBLE; - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] += emitL; - if (r.Jalpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - if (fill_L) { - if (d >= 2) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] += emitL; - } else { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = emitL; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = TR_USED_TRUNC_END; - } - if (r.Lalpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - } - // R deck handled separately (depends on Jalpha[y][j_sdr][d], not d_sd). - if (fill_R) { - for (int yoff = Ryoffset0; yoff < cnum; ++yoff) { - const int y = cfirst + yoff; - const float tsc = tscOf(v, yoff); - float scA = r.Jalpha[(size_t)y][(size_t)j_sdr][(size_t)d] + tsc; - if (scA > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scA; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - float scB = r.Ralpha[(size_t)y][(size_t)j_sdr][(size_t)d] + tsc; - if (scB > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scB; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_R_OFFSET); - } - } - if (r.Ralpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - } - } - } - } - else if (st == CM_ST_IR || st == CM_ST_MR) { - const float *esc = s.emitF.data(); - const int Lyoffset0 = (st == CM_ST_IR) ? 1 : 0; - for (int j = sdr; j <= L; ++j) { - const int j_sdr = j - sdr; - for (int d = sd; d <= j; ++d) { - const int d_sd = d - sd; - for (int yoff = 0; yoff < cnum; ++yoff) { - const int y = cfirst + yoff; - const float tsc = tscOf(v, yoff); - float sc = r.Jalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (sc > r.Jalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = sc; - r.Jyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - if (fill_R) { - float scR = r.Ralpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (scR > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scR; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_R_OFFSET); - } - } - } - // Single-residue emit on right position j. - const int8_t bj = (j >= 1 && j <= L) ? dsq[(size_t)j] : (int8_t)4; - const float emitR = (j >= 1 && j <= L) ? trEmitDegen1(esc, bj) : TR_IMPOSSIBLE; - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] += emitR; - if (r.Jalpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - if (fill_R) { - if (d >= 2) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] += emitR; - } else { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = emitR; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = TR_USED_TRUNC_END; - } - if (r.Ralpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - } - // L deck handled separately (uses j, d not j_sdr, d_sd). - if (fill_L) { - for (int yoff = Lyoffset0; yoff < cnum; ++yoff) { - const int y = cfirst + yoff; - const float tsc = tscOf(v, yoff); - float scA = r.Jalpha[(size_t)y][(size_t)j][(size_t)d] + tsc; - if (scA > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scA; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - float scB = r.Lalpha[(size_t)y][(size_t)j][(size_t)d] + tsc; - if (scB > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scB; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_L_OFFSET); - } - } - if (r.Lalpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - } - } - } - } - else if (st == CM_ST_MP) { - const float *esc = s.emitF.data(); - const int escSize = (int)s.emitF.size(); - const int Kp = 4; // our MP emit table is 4x4 (no degeneracies stored). - // Recurrence over children. - for (int yoff = 0; yoff < cnum; ++yoff) { - const int y = cfirst + yoff; - const float tsc = tscOf(v, yoff); - for (int j = sdr; j <= L; ++j) { - const int j_sdr = j - sdr; - for (int d = sd; d <= j; ++d) { - const int d_sd = d - sd; - float sc = r.Jalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (sc > r.Jalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = sc; - r.Jyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - } - if (fill_L) { - for (int d = sdl; d <= j; ++d) { - const int d_sdl = d - sdl; - float scA = r.Jalpha[(size_t)y][(size_t)j][(size_t)d_sdl] + tsc; - if (scA > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scA; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - float scB = r.Lalpha[(size_t)y][(size_t)j][(size_t)d_sdl] + tsc; - if (scB > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scB; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_L_OFFSET); - } - } - } - if (fill_R) { - for (int d = sdr; d <= j; ++d) { - const int d_sdr = d - sdr; - float scA = r.Jalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sdr] + tsc; - if (scA > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scA; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - float scB = r.Ralpha[(size_t)y][(size_t)j_sdr][(size_t)d_sdr] + tsc; - if (scB > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scB; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_R_OFFSET); - } - } - } - } - } - // Add emission scores; lmesc/rmesc for marginal modes. - const float *lme = (s.lmesc.size() >= 4) ? nullptr : nullptr; (void)lme; - for (int j = 0; j <= L; ++j) { - int i = j; - if (j >= 1) { - r.Jalpha[(size_t)v][(size_t)j][1] = TR_IMPOSSIBLE; - if (fill_L) { - const int8_t bi = (i >= 1 && i <= L) ? dsq[(size_t)i] : (int8_t)4; - float em = ((int)s.lmesc.size() >= 4 && i >= 1 && i <= L) - ? trEmitDegen1(s.lmesc.data(), bi) : TR_IMPOSSIBLE; - r.Lalpha[(size_t)v][(size_t)j][1] = em; - r.Lyshadow[(size_t)v][(size_t)j][1] = TR_USED_TRUNC_END; - } - if (fill_R) { - const int8_t bj = (j >= 1 && j <= L) ? dsq[(size_t)j] : (int8_t)4; - float em = ((int)s.rmesc.size() >= 4 && j >= 1 && j <= L) - ? trEmitDegen1(s.rmesc.data(), bj) : TR_IMPOSSIBLE; - r.Ralpha[(size_t)v][(size_t)j][1] = em; - r.Ryshadow[(size_t)v][(size_t)j][1] = TR_USED_TRUNC_END; - } - } - i--; - for (int d = 2; d <= j; ++d) { - const int8_t bi = (i >= 1 && i <= L) ? dsq[(size_t)i] : (int8_t)4; - const int8_t bj = (j >= 1 && j <= L) ? dsq[(size_t)j] : (int8_t)4; - float pairEm = (escSize >= Kp * Kp && i >= 1 && i <= L && j >= 1 && j <= L) - ? trEmitDegenPair(esc, Kp, bi, bj) : TR_IMPOSSIBLE; - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] += pairEm; - if (fill_L) { - float em = ((int)s.lmesc.size() >= 4 && i >= 1 && i <= L) - ? trEmitDegen1(s.lmesc.data(), bi) : TR_IMPOSSIBLE; - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] += em; - } - if (fill_R) { - float em = ((int)s.rmesc.size() >= 4 && j >= 1 && j <= L) - ? trEmitDegen1(s.rmesc.data(), bj) : TR_IMPOSSIBLE; - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] += em; - } - i--; - } - } - // Clamp to IMPOSSIBLE for d>=1. - for (int j = 0; j <= L; ++j) { - for (int d = 1; d <= j; ++d) { - if (r.Jalpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - if (fill_L && r.Lalpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - if (fill_R && r.Ralpha[(size_t)v][(size_t)j][(size_t)d] < TR_IMPOSSIBLE) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = TR_IMPOSSIBLE; - } - } - } - } - else if (st != CM_ST_B) { - // D, S — non-self, non-emitting. - for (int yoff = 0; yoff < cnum; ++yoff) { - const int y = cfirst + yoff; - const float tsc = tscOf(v, yoff); - for (int j = sdr; j <= L; ++j) { - const int j_sdr = j - sdr; - for (int d = sd; d <= j; ++d) { - const int d_sd = d - sd; - float sc = r.Jalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (sc > r.Jalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = sc; - r.Jyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_J_OFFSET); - } - if (fill_L) { - float scL = r.Lalpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (scL > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scL; - r.Lyshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_L_OFFSET); - } - } - if (fill_R) { - float scR = r.Ralpha[(size_t)y][(size_t)j_sdr][(size_t)d_sd] + tsc; - if (scR > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scR; - r.Ryshadow[(size_t)v][(size_t)j][(size_t)d] = (int8_t)(yoff + TR_TRMODE_R_OFFSET); - } - } - } - if (fill_L) r.Lalpha[(size_t)v][(size_t)j][0] = TR_IMPOSSIBLE; - if (fill_R) r.Ralpha[(size_t)v][(size_t)j][0] = TR_IMPOSSIBLE; - if (st == CM_ST_S) { - if (fill_L) r.Lyshadow[(size_t)v][(size_t)j][0] = TR_USED_TRUNC_END; - if (fill_R) r.Ryshadow[(size_t)v][(size_t)j][0] = TR_USED_TRUNC_END; - } - } - } - } - else { - // B state: BIF. - const int y = cfirst; // left subtree - const int z = cnum; // right subtree (encoded in cnum field) - for (int j = 0; j <= L; ++j) { - for (int d = 0; d <= j; ++d) { - for (int k = 0; k <= d; ++k) { - float scJ = r.Jalpha[(size_t)y][(size_t)(j - k)][(size_t)(d - k)] - + r.Jalpha[(size_t)z][(size_t)j][(size_t)k]; - if (scJ > r.Jalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Jalpha[(size_t)v][(size_t)j][(size_t)d] = scJ; - r.Jkshadow[(size_t)v][(size_t)j][(size_t)d] = k; - } - if (fill_L) { - float scL = r.Jalpha[(size_t)y][(size_t)(j - k)][(size_t)(d - k)] - + r.Lalpha[(size_t)z][(size_t)j][(size_t)k]; - if (scL > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scL; - r.Lkshadow[(size_t)v][(size_t)j][(size_t)d] = k; - r.Lkmode[(size_t)v][(size_t)j][(size_t)d] = TR_TRMODE_J; - } - } - if (fill_R) { - float scR = r.Ralpha[(size_t)y][(size_t)(j - k)][(size_t)(d - k)] - + r.Jalpha[(size_t)z][(size_t)j][(size_t)k]; - if (scR > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scR; - r.Rkshadow[(size_t)v][(size_t)j][(size_t)d] = k; - r.Rkmode[(size_t)v][(size_t)j][(size_t)d] = TR_TRMODE_J; - } - } - } - if (fill_T) { - for (int k = 1; k < d; ++k) { - float scT = r.Ralpha[(size_t)y][(size_t)(j - k)][(size_t)(d - k)] - + r.Lalpha[(size_t)z][(size_t)j][(size_t)k]; - if (scT > r.Talpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Talpha[(size_t)v][(size_t)j][(size_t)d] = scT; - r.Tkshadow[(size_t)v][(size_t)j][(size_t)d] = k; - } - } - } - // Special case 1: full sequence aligns to BEGL_S left child (k=0). - if (fill_L) { - float scA = r.Jalpha[(size_t)y][(size_t)j][(size_t)d]; - if (scA > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scA; - r.Lkshadow[(size_t)v][(size_t)j][(size_t)d] = 0; - r.Lkmode[(size_t)v][(size_t)j][(size_t)d] = TR_TRMODE_J; - } - float scB = r.Lalpha[(size_t)y][(size_t)j][(size_t)d]; - if (scB > r.Lalpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Lalpha[(size_t)v][(size_t)j][(size_t)d] = scB; - r.Lkshadow[(size_t)v][(size_t)j][(size_t)d] = 0; - r.Lkmode[(size_t)v][(size_t)j][(size_t)d] = TR_TRMODE_L; - } - } - // Special case 2: full sequence aligns to BEGR_S right child (k=d). - if (fill_R) { - float scA = r.Jalpha[(size_t)z][(size_t)j][(size_t)d]; - if (scA > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scA; - r.Rkshadow[(size_t)v][(size_t)j][(size_t)d] = d; - r.Rkmode[(size_t)v][(size_t)j][(size_t)d] = TR_TRMODE_J; - } - float scB = r.Ralpha[(size_t)z][(size_t)j][(size_t)d]; - if (scB > r.Ralpha[(size_t)v][(size_t)j][(size_t)d]) { - r.Ralpha[(size_t)v][(size_t)j][(size_t)d] = scB; - r.Rkshadow[(size_t)v][(size_t)j][(size_t)d] = d; - r.Rkmode[(size_t)v][(size_t)j][(size_t)d] = TR_TRMODE_R; - } - } - } - } - } - - // Truncated begin into v: only update [0][L][L] cells. - double trpenalty = NEG_INF; - if (pty_idx >= 0 && pty_idx < (int)m.trpL.size() && (int)m.trpL[(size_t)pty_idx].size() == M - && (int)m.trpG.size() > pty_idx && (int)m.trpG[(size_t)pty_idx].size() == M) { - trpenalty = localOn ? m.trpL[(size_t)pty_idx][(size_t)v] - : m.trpG[(size_t)pty_idx][(size_t)v]; - } - if (noTrPenalty) trpenalty = NEG_INF; // disable trunc entry - if (trNotImpossible(trpenalty)) { - const float trF = (float)trpenalty; - float scJ = r.Jalpha[(size_t)v][(size_t)L][(size_t)L] + trF; - if (scJ > r.Jalpha[0][(size_t)L][(size_t)L]) { - r.Jalpha[0][(size_t)L][(size_t)L] = scJ; - Jb = v; - } - if (fill_L) { - float scL = r.Lalpha[(size_t)v][(size_t)L][(size_t)L] + trF; - if (scL > r.Lalpha[0][(size_t)L][(size_t)L]) { - r.Lalpha[0][(size_t)L][(size_t)L] = scL; - Lb = v; - } - } - if (fill_R) { - float scR = r.Ralpha[(size_t)v][(size_t)L][(size_t)L] + trF; - if (scR > r.Ralpha[0][(size_t)L][(size_t)L]) { - r.Ralpha[0][(size_t)L][(size_t)L] = scR; - Rb = v; - } - } - if (fill_T && st == CM_ST_B) { - float scT = r.Talpha[(size_t)v][(size_t)L][(size_t)L] + trF; - if (scT > r.Talpha[0][(size_t)L][(size_t)L]) { - r.Talpha[0][(size_t)L][(size_t)L] = scT; - Tb = v; - } - } - } - - // Local-begin pickup (Infernal cm_CYKInsideAlign:1028-1033). Only - // active when noTrPenalty (mirrors --notrunc). beginSc[v] is finite - // for begin-eligible heads (MATP/MATL/MATR/BIF firsts) with values - // from cm_CalculateLocalBeginProbs. Track separately, commit at end. - if (noTrPenalty && localOn && v > 0 && trNotImpossible(s.beginSc)) { - float cand = r.Jalpha[(size_t)v][(size_t)L][(size_t)L] + (float)s.beginSc; - if (cand > bsc_local) { - bsc_local = cand; - blb_local = v; - } - } - } /* end loop v = M-1..1 */ - - // Commit local-begin pickup to root (Infernal cm_CYKInsideAlign:1040-1043). - if (noTrPenalty && bsc_local > r.Jalpha[0][(size_t)L][(size_t)L]) { - r.Jalpha[0][(size_t)L][(size_t)L] = bsc_local; - // Use TR_USED_TRUNC_BEGIN sentinel since trace already handles it - // (jumps to b without consuming residues, then continues normally). - r.Jyshadow[0][(size_t)L][(size_t)L] = TR_USED_TRUNC_BEGIN; - Jb = blb_local; - } - // DEBUG: dump alpha[v][j][d] for specific (v,j,d) tuples. - // Set MMSEQS_CMSCAN_DUMP_CELLS="v:j:d,v:j:d,..." to enable. - if (const char *cellEnv = std::getenv("MMSEQS_CMSCAN_DUMP_CELLS")) { - std::string spec = cellEnv; - size_t pos = 0; - while (pos < spec.size()) { - size_t comma = spec.find(',', pos); - if (comma == std::string::npos) comma = spec.size(); - std::string tok = spec.substr(pos, comma - pos); - int vv = -1, jj = -1, dd = -1; - std::sscanf(tok.c_str(), "%d:%d:%d", &vv, &jj, &dd); - if (vv >= 0 && vv < M && jj >= 0 && jj <= L && dd >= 0 && dd <= jj) { - float aJ = r.Jalpha[(size_t)vv][(size_t)jj][(size_t)dd]; - std::fprintf(stderr, "[CELL_DBG] v=%d j=%d d=%d type=%d J=%.4f\n", - vv, jj, dd, (int)m.states[(size_t)vv].type, aJ); - } - pos = comma + 1; - } - } - // DEBUG: dump alpha[v][j][d] for matching Infernal CM_DUMP_CYK_J output. - // Set MMSEQS_CMSCAN_DUMP_CYK_J=",,..." to enable. - if (const char *jenv = std::getenv("MMSEQS_CMSCAN_DUMP_CYK_J")) { - const char *p = jenv; - while (*p) { - char *endp = nullptr; - long jt = std::strtol(p, &endp, 10); - if (endp == p) break; - if (jt > 0 && jt <= L) { - for (int dd = 0; dd <= (int)jt; ++dd) { - for (int vv = 0; vv < M; ++vv) { - float a = r.Jalpha[(size_t)vv][(size_t)jt][(size_t)dd]; - if (a <= -1e29f) continue; - std::fprintf(stderr, "DUMP_CYK_RIBO j=%ld d=%d v=%d type=%d alpha=%.6f\n", - jt, dd, vv, (int)m.states[(size_t)vv].type, a); - } - } - } - p = endp; - if (*p == ',') p++; - else if (*p) break; - } - } - // DEBUG: dump root-level decision values when MMSEQS_CMSCAN_DUMP_ROOT=1. - if (std::getenv("MMSEQS_CMSCAN_DUMP_ROOT") != nullptr) { - const CmState &s0 = m.states[0]; - std::fprintf(stderr, "[ROOT_DBG] L=%d cnum=%d cfirst=%d alpha[0][L][L]=%.4f Jb=%d\n", - L, s0.cnum, s0.cfirst, (double)r.Jalpha[0][(size_t)L][(size_t)L], Jb); - for (int yoff = 0; yoff < s0.cnum; ++yoff) { - int y = s0.cfirst + yoff; - float tsc = (yoff < (int)s0.trans.size()) ? (float)s0.trans[yoff] : -1e30f; - float ay = (y >= 0 && y < M) ? r.Jalpha[(size_t)y][(size_t)L][(size_t)L] : -1e30f; - float bgs = (y >= 0 && y < M) ? (float)m.states[(size_t)y].beginSc : -1e30f; - std::fprintf(stderr, "[ROOT_DBG] yoff=%d y=%d type=%d tsc=%.4f alpha[y]=%.4f beginSc=%.4f sum=%.4f\n", - yoff, y, (int)m.states[(size_t)y].type, tsc, ay, bgs, ay+tsc); - } - // Also dump state 3 (MP at node 1) transitions for cell-diff vs Infernal - if (M > 12) { - const CmState &s3 = m.states[3]; - std::fprintf(stderr, "[ROOT_DBG] state 3 (MP) cnum=%d cfirst=%d alpha[3][L][L]=%.4f endSc=%.4f\n", - s3.cnum, s3.cfirst, (double)r.Jalpha[3][(size_t)L][(size_t)L], s3.endSc); - for (int yoff = 0; yoff < s3.cnum && yoff < (int)s3.trans.size(); ++yoff) { - int y = s3.cfirst + yoff; - float tsc = (float)s3.trans[yoff]; - float ay = (y < M) ? r.Jalpha[(size_t)y][(size_t)(L-1)][(size_t)(L-2)] : -1e30f; - std::fprintf(stderr, "[ROOT_DBG] 3->yoff=%d y=%d type=%d tsc=%.4f alpha[y][L-1][L-2]=%.4f\n", - yoff, y, (int)m.states[(size_t)y].type, tsc, ay); - } - } - } - - // All valid alignments use a truncated begin: stamp the root shadow. - // With noTrPenalty, leave the regular yshadow set by the v=0 recurrence so - // the trace walks normally (no TRUNC_BEGIN jump). - if (!noTrPenalty) { - r.Jyshadow[0][(size_t)L][(size_t)L] = TR_USED_TRUNC_BEGIN; - if (fill_L) r.Lyshadow[0][(size_t)L][(size_t)L] = TR_USED_TRUNC_BEGIN; - if (fill_R) r.Ryshadow[0][(size_t)L][(size_t)L] = TR_USED_TRUNC_BEGIN; - } - - // Choose the optimal mode/score/b. - double sc; - char mode; - int b; - if (preset_mode == 'J') { - sc = r.Jalpha[0][(size_t)L][(size_t)L]; mode = 'J'; b = Jb; - } else if (preset_mode == 'L') { - sc = r.Lalpha[0][(size_t)L][(size_t)L]; mode = 'L'; b = Lb; - } else if (preset_mode == 'R') { - sc = r.Ralpha[0][(size_t)L][(size_t)L]; mode = 'R'; b = Rb; - } else if (preset_mode == 'T') { - sc = r.Talpha[0][(size_t)L][(size_t)L]; mode = 'T'; b = Tb; - } else { - sc = r.Jalpha[0][(size_t)L][(size_t)L]; mode = 'J'; b = Jb; - if (fill_L && r.Lalpha[0][(size_t)L][(size_t)L] > sc) { - sc = r.Lalpha[0][(size_t)L][(size_t)L]; mode = 'L'; b = Lb; - } - if (fill_R && r.Ralpha[0][(size_t)L][(size_t)L] > sc) { - sc = r.Ralpha[0][(size_t)L][(size_t)L]; mode = 'R'; b = Rb; - } - if (fill_T && r.Talpha[0][(size_t)L][(size_t)L] > sc) { - sc = r.Talpha[0][(size_t)L][(size_t)L]; mode = 'T'; b = Tb; - } - } - r.score = sc; - r.mode = mode; - r.b = b; - return r; -} - -static void dumpTrCykAlphaFor(const TrCykResult &r, int M, int L, int v_focus) { - const char *flag = std::getenv("MMSEQS_CMSCAN_DUMP_TRDP"); - if (flag == nullptr || std::string(flag) != "1") return; - (void)M; - auto cellOr = [&](const std::vector>> &a, int v, int j, int d) -> double { - if ((int)a.size() <= v) return -1e30; - if ((int)a[(size_t)v].size() <= j) return -1e30; - if ((int)a[(size_t)v][(size_t)j].size() <= d) return -1e30; - return (double)a[(size_t)v][(size_t)j][(size_t)d]; - }; - std::fprintf(stderr, - "[TRDP_DUMP] v=%d L=%d J[L][L]=%.4f L[L][L]=%.4f R[L][L]=%.4f T[L][L]=%.4f\n", - v_focus, L, - cellOr(r.Jalpha, v_focus, L, L), - cellOr(r.Lalpha, v_focus, L, L), - cellOr(r.Ralpha, v_focus, L, L), - cellOr(r.Talpha, v_focus, L, L)); - for (int dj = 0; dj <= 3 && dj <= L; ++dj) { - const int j = L - dj; - for (int dd = 0; dd <= 3 && dd <= j; ++dd) { - const int d = j - dd; - std::fprintf(stderr, - "[TRDP_DUMP] v=%d j=%d d=%d J=%.4f L=%.4f R=%.4f T=%.4f\n", - v_focus, j, d, - cellOr(r.Jalpha, v_focus, j, d), - cellOr(r.Lalpha, v_focus, j, d), - cellOr(r.Ralpha, v_focus, j, d), - cellOr(r.Talpha, v_focus, j, d)); - } - } - std::fprintf(stderr, "[TRDP_DUMP] result: score=%.4f mode=%c b=%d\n", r.score, r.mode, r.b); -} - -// ===================================================================== -// Stage 7: native port of Infernal cm_tr_alignT (cm_dpalign_trunc.c:178). -// Walks J/L/R/T shadow matrices produced by Stage 6 to recover the trCYK -// parsetree (state sequence with mode tags). Mirrors Infernal exactly: -// TRMODE_*_OFFSET decoding, USED_TRUNC_BEGIN/END/EL sentinels, BIF stack. -// ===================================================================== -struct TrParseNode { - int v; // state index in CM (or M for EL) - int8_t mode; // TR_TRMODE_J/L/R/T - int emitl; // i-coordinate (1-based) - int emitr; // j-coordinate (1-based) - int parent; // index in trace, -1 for root - int nxtl; // left child trace index, -1 if none - int nxtr; // right child trace index, -1 if none - bool is_right_child; // attach side relative to parent -}; - -struct TrParsetree { - std::vector nodes; - bool ok = false; - char err[256] = {0}; - char optimal_mode = 'J'; - int b = 0; // local-entry state - double trpenalty = 0.0; - double score = 0.0; -}; - -// Append a node to the parsetree, attaching it under `parent` on the -// indicated side. Returns the new node's index. Mirrors InsertTraceNodewithMode. -static int trInsertTraceNode(TrParsetree &tr, int parent, bool is_right_child, - int i, int j, int v, int8_t mode) { - TrParseNode n; - n.v = v; - n.mode = mode; - n.emitl = i; - n.emitr = j; - n.parent = parent; - n.nxtl = -1; - n.nxtr = -1; - n.is_right_child = is_right_child; - int idx = (int)tr.nodes.size(); - tr.nodes.push_back(n); - if (parent >= 0) { - if (is_right_child) tr.nodes[(size_t)parent].nxtr = idx; - else tr.nodes[(size_t)parent].nxtl = idx; - } - return idx; -} - -static TrParsetree runTrCYKAlignT(const InfernalExactModel &m, - const TrCykResult &r, - int L, - int pty_idx) { - TrParsetree tr; - const int M = (int)m.states.size(); - char mode = TR_TRMODE_J; - switch (r.mode) { - case 'J': mode = TR_TRMODE_J; break; - case 'L': mode = TR_TRMODE_L; break; - case 'R': mode = TR_TRMODE_R; break; - case 'T': mode = TR_TRMODE_T; break; - default: mode = TR_TRMODE_J; break; - } - tr.optimal_mode = r.mode; - tr.b = r.b; - tr.score = r.score; - - // Trpenalty for the chosen entry state b (local-mode mirrors - // cm_tr_alignT line 233; we always use local pty since CMH_LOCAL_BEGIN - // is implied by truncation DP being enabled). - if (pty_idx >= 0 && r.b >= 0 && r.b < M - && pty_idx < (int)m.trpL.size() - && r.b < (int)m.trpL[(size_t)pty_idx].size()) { - tr.trpenalty = m.trpL[(size_t)pty_idx][(size_t)r.b]; - } - - // Root S: attach state 0 spanning [1..L] in the optimal mode. - int rootIdx = trInsertTraceNode(tr, -1, false, 1, L, 0, mode); - (void)rootIdx; - - // Stacks for bifurcation right-child resumption. - struct PdaEntry { int j; int k; int bifparent; int8_t mode; }; - std::vector pda; - - int v = 0; - int i = 1; - int j = L; - int d = L; - - auto getJyshadow = [&](int v, int j, int d) -> int8_t { - return r.Jyshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getLyshadow = [&](int v, int j, int d) -> int8_t { - return r.Lyshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getRyshadow = [&](int v, int j, int d) -> int8_t { - return r.Ryshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getJkshadow = [&](int v, int j, int d) -> int { - return r.Jkshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getLkshadow = [&](int v, int j, int d) -> int { - return r.Lkshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getRkshadow = [&](int v, int j, int d) -> int { - return r.Rkshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getTkshadow = [&](int v, int j, int d) -> int { - return r.Tkshadow[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getLkmode = [&](int v, int j, int d) -> int8_t { - return r.Lkmode[(size_t)v][(size_t)j][(size_t)d]; - }; - auto getRkmode = [&](int v, int j, int d) -> int8_t { - return r.Rkmode[(size_t)v][(size_t)j][(size_t)d]; - }; - - const int MAX_STEPS = 8 * (L + 1) * (M + 1) + 1000; - int steps = 0; - - while (true) { - if (++steps > MAX_STEPS) { - std::snprintf(tr.err, sizeof(tr.err), - "trace step limit exceeded at v=%d j=%d d=%d", v, j, d); - return tr; - } - if (v < 0 || v > M) { - std::snprintf(tr.err, sizeof(tr.err), - "trace v=%d out of [0..M=%d]", v, M); - return tr; - } - - const CmStateType st = (v == M) ? CM_ST_UNKNOWN : m.states[(size_t)v].type; - const bool is_E_or_EL = (v == M) || (st == CM_ST_E); - - if (v != M && st == CM_ST_B) { - // Bifurcation: pick k from kshadow per mode. - int k = 0; - if (mode == TR_TRMODE_J) k = getJkshadow(v, j, d); - else if (mode == TR_TRMODE_L) k = getLkshadow(v, j, d); - else if (mode == TR_TRMODE_R) k = getRkshadow(v, j, d); - else if (mode == TR_TRMODE_T) k = getTkshadow(v, j, d); - else { std::snprintf(tr.err, sizeof(tr.err), "bogus mode at B v=%d", v); return tr; } - - int8_t prvmode = mode; - int8_t rightmode; - if (prvmode == TR_TRMODE_J) rightmode = TR_TRMODE_J; - else if (prvmode == TR_TRMODE_L) rightmode = TR_TRMODE_L; - else if (prvmode == TR_TRMODE_R) rightmode = getRkmode(v, j, d); - else rightmode = TR_TRMODE_L; // TRMODE_T - - // Stash right-child state. - PdaEntry e; - e.j = j; - e.k = k; - e.bifparent = (int)tr.nodes.size() - 1; - e.mode = rightmode; - pda.push_back(e); - - // Determine left-child mode. - int8_t leftmode; - if (prvmode == TR_TRMODE_J) leftmode = TR_TRMODE_J; - else if (prvmode == TR_TRMODE_L) leftmode = getLkmode(v, j, d); - else if (prvmode == TR_TRMODE_R) leftmode = TR_TRMODE_R; - else leftmode = TR_TRMODE_R; // TRMODE_T - - // Descend to BEGL_S (left child). - j = j - k; - d = d - k; - i = j - d + 1; - int y = m.states[(size_t)v].cfirst; - mode = leftmode; - trInsertTraceNode(tr, (int)tr.nodes.size() - 1, false, i, j, y, mode); - v = y; - continue; - } - - if (is_E_or_EL) { - // Pop a right-start off the stack, attach as right child. - if (pda.empty()) break; // done - PdaEntry e = pda.back(); pda.pop_back(); - int bifparent = e.bifparent; - int kk = e.k; - int jj = e.j; - mode = e.mode; - int parent_v = tr.nodes[(size_t)bifparent].v; - // Right child y = cnum of bif state (cnum holds index of right child S). - int y = m.states[(size_t)parent_v].cnum; - d = kk; - j = jj; - i = j - d + 1; - trInsertTraceNode(tr, bifparent, true, i, j, y, mode); - v = y; - continue; - } - - // Standard state: read yshadow per mode. - int yoffset_raw = 0; - if (mode == TR_TRMODE_J) yoffset_raw = (int)getJyshadow(v, j, d); - else if (mode == TR_TRMODE_L) yoffset_raw = (int)getLyshadow(v, j, d); - else if (mode == TR_TRMODE_R) yoffset_raw = (int)getRyshadow(v, j, d); - else if (mode == TR_TRMODE_T) { - if (v == 0) yoffset_raw = TR_USED_TRUNC_BEGIN; - else { std::snprintf(tr.err, sizeof(tr.err), - "TRMODE_T at non-root non-B v=%d", v); return tr; } - } - else { std::snprintf(tr.err, sizeof(tr.err), "bogus mode %d", (int)mode); return tr; } - - int yoffset = yoffset_raw; - int8_t nxtmode = mode; - if (yoffset == TR_USED_TRUNC_BEGIN) { nxtmode = mode; } - else if (yoffset == TR_USED_TRUNC_END) { /* irrelevant */ } - else if (yoffset == TR_USED_EL) { /* irrelevant */ } - else if (yoffset >= TR_TRMODE_R_OFFSET) { nxtmode = TR_TRMODE_R; yoffset -= TR_TRMODE_R_OFFSET; } - else if (yoffset >= TR_TRMODE_L_OFFSET) { nxtmode = TR_TRMODE_L; yoffset -= TR_TRMODE_L_OFFSET; } - else if (yoffset >= TR_TRMODE_J_OFFSET) { nxtmode = TR_TRMODE_J; yoffset -= TR_TRMODE_J_OFFSET; } - else { std::snprintf(tr.err, sizeof(tr.err), - "yoffset=%d out of bounds at v=%d j=%d d=%d", yoffset_raw, v, j, d); return tr; } - - // Advance i/j based on state-type emission and current mode. - switch (st) { - case CM_ST_D: case CM_ST_S: case CM_ST_B: case CM_ST_E: - break; - case CM_ST_MP: - if (mode == TR_TRMODE_J) i++; - if (mode == TR_TRMODE_L && d > 0) i++; - if (mode == TR_TRMODE_J) j--; - if (mode == TR_TRMODE_R && d > 0) j--; - break; - case CM_ST_ML: case CM_ST_IL: - if (mode == TR_TRMODE_J) i++; - if (mode == TR_TRMODE_L && d > 0) i++; - break; - case CM_ST_MR: case CM_ST_IR: - if (mode == TR_TRMODE_J) j--; - if (mode == TR_TRMODE_R && d > 0) j--; - break; - default: - std::snprintf(tr.err, sizeof(tr.err), "bogus state-type %d at v=%d", (int)st, v); return tr; - } - d = j - i + 1; - - if (yoffset == TR_USED_EL || yoffset == TR_USED_TRUNC_END) { - if (yoffset == TR_USED_EL) { - trInsertTraceNode(tr, (int)tr.nodes.size() - 1, false, i, j, M, mode); - } - v = M; // EL or pseudo-EL - continue; - } - if (yoffset == TR_USED_TRUNC_BEGIN) { - trInsertTraceNode(tr, (int)tr.nodes.size() - 1, false, i, j, r.b, mode); - v = r.b; - continue; - } - - // Standard descent. - mode = nxtmode; - int y = m.states[(size_t)v].cfirst + yoffset; - trInsertTraceNode(tr, (int)tr.nodes.size() - 1, false, i, j, y, mode); - v = y; - } - - tr.ok = true; - return tr; -} - -static void dumpTrParsetree(const TrParsetree &tr, const InfernalExactModel &m) { - const char *flag = std::getenv("MMSEQS_CMSCAN_DUMP_TRPT"); - if (flag == nullptr || std::string(flag) != "1") return; - std::fprintf(stderr, "[TRPT_DUMP] mode=%c b=%d score=%.4f trpenalty=%.4f ok=%d %s\n", - tr.optimal_mode, tr.b, tr.score, tr.trpenalty, tr.ok ? 1 : 0, - tr.ok ? "" : tr.err); - std::fprintf(stderr, "[TRPT_DUMP] %4s %4s %4s %4s %4s %4s %4s\n", - "idx", "v", "mode", "emitl", "emitr", "parent", "type"); - auto modeChar = [](int8_t md) -> char { - if (md == TR_TRMODE_J) return 'J'; - if (md == TR_TRMODE_L) return 'L'; - if (md == TR_TRMODE_R) return 'R'; - if (md == TR_TRMODE_T) return 'T'; - return '?'; - }; - auto stChar = [&](int v) -> const char* { - if (v == (int)m.states.size()) return "EL"; - switch (m.states[(size_t)v].type) { - case CM_ST_MP: return "MP"; - case CM_ST_ML: return "ML"; - case CM_ST_MR: return "MR"; - case CM_ST_IL: return "IL"; - case CM_ST_IR: return "IR"; - case CM_ST_D: return "D"; - case CM_ST_S: return "S"; - case CM_ST_B: return "B"; - case CM_ST_E: return "E"; - default: return "?"; - } - }; - for (size_t k = 0; k < tr.nodes.size(); ++k) { - const TrParseNode &n = tr.nodes[k]; - std::fprintf(stderr, "[TRPT_DUMP] %4zu %4d:%-3s %3c %4d %4d %4d %4s\n", - k, n.v, stChar(n.v), modeChar(n.mode), - n.emitl, n.emitr, n.parent, - (n.parent < 0) ? "ROOT" : (n.is_right_child ? "RGT" : "LFT")); - } -} - -// Flatten a TrParsetree into the same trace-state list that the J-only CYK -// emits (DFS pre-order; nodes were already inserted in that order by -// runTrCYKAlignT). EL pseudo-states (v == M) are kept; downstream consumers -// (modelTraceCigarQueryCoord) skip them via "sid >= states.size()". -static std::vector trParsetreeStates(const TrParsetree &tr) { - std::vector out; - out.reserve(tr.nodes.size()); - for (const TrParseNode &n : tr.nodes) { - out.push_back(n.v); - } - return out; -} - -static InfernalExactModel parseInfernalCmExactModelFromStream(std::istream &in, const std::string &srcLabel) { - int clen = -1; - int statesN = -1; - int w = -1; - bool inModel = false; - bool done = false; - int curMapL = -1; - int curMapR = -1; - int curNodeIdx = -1; - CmNodeType curNodeType = CM_ND_UNKNOWN; - bool nodeChanged = false; - InfernalExactModel m; - - std::vector states; - std::string line; - while (std::getline(in, line)) { - const std::string t = trim(line); - if (t.empty()) { - continue; - } - if (!inModel && t == "CM") { - inModel = true; - continue; - } - if (inModel && t == "//") { - done = true; - break; - } - if (!inModel) { - std::stringstream hs(t); - std::string key; - hs >> key; - if (key == "CLEN") { - hs >> clen; - } else if (key == "STATES") { - hs >> statesN; - } else if (key == "W") { - hs >> w; - } else if (key == "N3OMEGA") { - hs >> m.null3Omega; - } else if (key == "N2OMEGA") { - hs >> m.null2Omega; - } else if (key == "PBEGIN") { - hs >> m.pBegin; - m.hasLocalCfg = true; - } else if (key == "PEND") { - hs >> m.pEnd; - m.hasLocalCfg = true; - } else if (key == "ELSELF") { - hs >> m.elSelf; - m.hasLocalCfg = true; - } else if (key == "NULL") { - const double nullBase = 0.25; - std::string tok0, tok1, tok2, tok3; - hs >> tok0 >> tok1 >> tok2 >> tok3; - if (!tok0.empty() && !tok1.empty() && !tok2.empty() && !tok3.empty()) { - m.nullProb[0] = parseInfernalAsciiProb(tok0, nullBase); - m.nullProb[1] = parseInfernalAsciiProb(tok1, nullBase); - m.nullProb[2] = parseInfernalAsciiProb(tok2, nullBase); - m.nullProb[3] = parseInfernalAsciiProb(tok3, nullBase); - m.hasNull = true; - } - } else if (key == "ECMLC" || key == "ECMLI" || key == "ECMGC" || key == "ECMGI") { - double lambda = 0.0; - double muExtrap = 0.0; - double muOrig = 0.0; - double dbsize = 0.0; - int nrandhits = 0; - double tailp = 0.0; - hs >> lambda >> muExtrap >> muOrig >> dbsize >> nrandhits >> tailp; - (void) muOrig; - (void) tailp; - InfernalExactModel::ExpTail exp; - exp.valid = true; - exp.lambda = lambda; - exp.muExtrap = muExtrap; - exp.dbsize = dbsize; - exp.nrandhits = nrandhits; - if (key == "ECMLC") { - m.expLC = exp; - } else if (key == "ECMLI") { - m.expLI = exp; - } else if (key == "ECMGC") { - m.expGC = exp; - } else { - m.expGI = exp; - } - } - continue; - } - - if (line.find('[') != std::string::npos && line.find(']') != std::string::npos) { - const size_t open = line.find('['); - const size_t close = line.find(']'); - std::stringstream ns(line.substr(open + 1, close - open - 1)); - std::string ntype; - int nidx = -1; - ns >> ntype >> nidx; - curNodeType = parseCmNodeType(ntype); - curNodeIdx = nidx; - nodeChanged = true; - std::stringstream aft(line.substr(close + 1)); - std::vector toks; - std::string tok; - while (aft >> tok) { - toks.push_back(tok); - } - if (toks.size() >= 2) { - curMapL = parseMaybeInt(toks[0]); - curMapR = parseMaybeInt(toks[1]); - } else { - curMapL = -1; - curMapR = -1; - } - continue; - } - - std::stringstream ss(line); - std::vector toks; - std::string tok; - while (ss >> tok) { - toks.push_back(tok); - } - if (toks.size() < 10) { - continue; - } - - const CmStateType st = parseCmStateType(toks[0]); - if (st == CM_ST_UNKNOWN) { - continue; - } - const int v = parseMaybeInt(toks[1]); - const int cfirst = parseMaybeInt(toks[4]); - const int cnum = parseMaybeInt(toks[5]); - const int dmin1 = parseMaybeInt(toks[7]); - const int dmax1 = parseMaybeInt(toks[8]); - if (v < 0) { - continue; - } - - CmState s; - s.type = st; - s.idx = v; - s.cfirst = cfirst; - s.cnum = cnum; - s.mapLeft = curMapL; - s.mapRight = curMapR; - s.nodeIdx = curNodeIdx; - s.nodeType = curNodeType; - s.isFirstOfNode = nodeChanged; - nodeChanged = false; - - int off = 10; - if (s.type != CM_ST_B && cnum > 0) { - for (int x = 0; x < cnum && (off + x) < static_cast(toks.size()); ++x) { - s.trans.push_back(parseInfernalAsciiScore(toks[static_cast(off + x)])); - } - off += cnum; - } - - int emitN = 0; - if (s.type == CM_ST_MP) { - emitN = 16; - } else if (s.type == CM_ST_ML || s.type == CM_ST_MR || s.type == CM_ST_IL || s.type == CM_ST_IR) { - emitN = 4; - } - for (int x = 0; x < emitN && (off + x) < static_cast(toks.size()); ++x) { - s.emit.push_back(parseInfernalAsciiScore(toks[static_cast(off + x)])); - } - s.emitF.resize(s.emit.size()); - for (size_t k = 0; k < s.emit.size(); ++k) { s.emitF[k] = static_cast(s.emit[k]); } - s.dmin1 = dmin1; - s.dmax1 = dmax1; - states.push_back(s); - } - if (!done || clen <= 0 || states.empty()) { - Debug(Debug::ERROR) << "Failed parsing Infernal CM state graph from " << srcLabel << "\n"; - EXIT(EXIT_FAILURE); - } - - std::sort(states.begin(), states.end(), [](const CmState &a, const CmState &b) { return a.idx < b.idx; }); - for (size_t i = 0; i < states.size(); ++i) { - if (states[i].idx != static_cast(i)) { - Debug(Debug::WARNING) << "Non-contiguous state indices in CM; expected " << i - << " found " << states[i].idx << "\n"; - } - } - if (statesN > 0 && static_cast(states.size()) != statesN) { - Debug(Debug::WARNING) << "CM STATES header=" << statesN - << " parsed states=" << states.size() << "\n"; - } - - m.clen = clen; - m.w = w; - m.states = states; - m.rootState = 0; - - // cm_localize: with PBEGIN > 0, identify begin-eligible heads (first state of - // MATP/MATL/MATR/BIF nodes); with PEND > 0, identify end-eligible heads (first - // state of MATP/MATL/MATR/BEGL/BEGR nodes whose successor node is not END). - // Rescale the head's outgoing transitions in linear space by 1/(1+end_lin) and - // populate beginSc/endSc as log2 probabilities. - // Gated on MMSEQS_CMSCAN_LOCAL_MODE=1 (default off). Although Infernal cmsearch - // is local by default, our local pickup is currently a post-fill pass (EL-state - // contributions don't propagate up via the parent CYK recurrence). Until that's - // interleaved into the per-state fill loop, enabling local-by-default produces - // worse alignments than pure glocal (mean -0.075 AUC on 9-query bench, 9-May). - { - const char *env = std::getenv("MMSEQS_CMSCAN_LOCAL_MODE"); - const bool enableLocalMode = (env != NULL && std::string(env) == "1"); - if (!enableLocalMode) { - m.hasLocalCfg = false; - } - } - if (m.hasLocalCfg) { - // Map from nodeIdx -> {firstStateIdx, nodeType, nextNodeType} - int maxNode = -1; - for (const CmState &s : m.states) { - if (s.nodeIdx > maxNode) maxNode = s.nodeIdx; - } - std::vector firstOfNode(static_cast(maxNode + 2), -1); - std::vector nodeTypes(static_cast(maxNode + 2), CM_ND_UNKNOWN); - for (const CmState &s : m.states) { - if (s.nodeIdx >= 0 && s.isFirstOfNode) { - firstOfNode[static_cast(s.nodeIdx)] = s.idx; - nodeTypes[static_cast(s.nodeIdx)] = s.nodeType; - } - } - - // Begin-eligible: first state of MATP/MATL/MATR/BIF nodes (excluding node 0). - // End-eligible: first state of MATP/MATL/MATR/BEGL/BEGR nodes whose - // successor node is not END (node index >0 and < maxNode). - std::vector beginHeads; - std::vector endHeads; - for (int n = 1; n <= maxNode; ++n) { - const CmNodeType nt = nodeTypes[static_cast(n)]; - const int head = firstOfNode[static_cast(n)]; - if (head < 0) continue; - if (nt == CM_ND_MATP || nt == CM_ND_MATL || nt == CM_ND_MATR || nt == CM_ND_BIF) { - beginHeads.push_back(head); - } - if (nt == CM_ND_MATP || nt == CM_ND_MATL || nt == CM_ND_MATR || - nt == CM_ND_BEGL || nt == CM_ND_BEGR) { - CmNodeType nextNt = (n + 1 <= maxNode) ? nodeTypes[static_cast(n + 1)] - : CM_ND_END; - if (nextNt != CM_ND_END) { - endHeads.push_back(head); - } - } - } - - const double LOG2 = std::log(2.0); - if (m.pBegin > 0.0 && !beginHeads.empty()) { - // Mirror Infernal cm_CalculateLocalBeginProbs (cm_modelconfig.c:397): - // node 1's first state (the natural first child of ROOT_S) gets - // log2(1 - pBegin). Other internal heads (nd >= 2) share pBegin - // across nstartsRest = nbegin - 1. Treating all heads uniformly - // costs ~10 bits at v=0 vs Infernal (verified on 2YGH_1 harness). - const int node1Head = (1 < static_cast(firstOfNode.size())) ? firstOfNode[1] : -1; - int nstartsRest = 0; - for (int h : beginHeads) { - if (h != node1Head) nstartsRest++; - } - const double begLogFirst = std::log(1.0 - m.pBegin) / LOG2; - const double begLogRest = (nstartsRest > 0) - ? std::log(m.pBegin / static_cast(nstartsRest)) / LOG2 - : NEG_INF; - for (int h : beginHeads) { - m.states[static_cast(h)].beginSc = - (h == node1Head) ? begLogFirst : begLogRest; - } - } - if (m.pEnd > 0.0 && !endHeads.empty()) { - const double endLin = m.pEnd / static_cast(endHeads.size()); - // After cm_localize: t[v]_new[i] = t[v]_old[i] / (1 + endLin), - // end[v] = endLin / (1 + endLin). - // In log2 space that is a uniform offset of -log2(1 + endLin) added to - // existing transition log probabilities, and endSc = log2(endLin/(1+endLin)). - const double scaleLog = -std::log(1.0 + endLin) / LOG2; - const double endLog = std::log(endLin / (1.0 + endLin)) / LOG2; - for (int h : endHeads) { - CmState &s = m.states[static_cast(h)]; - for (size_t k = 0; k < s.trans.size(); ++k) { - if (s.trans[k] != NEG_INF) { - s.trans[k] += scaleLog; - } - } - s.endSc = endLog; - } - } - // Mirror Infernal cm_modelconfig.c:491 — in local mode, ROOT_S has no - // standard outgoing transitions; the only way out of node 0 is via the - // local-begin pickup. Zero ROOT_S transitions to avoid double-counting. - const char *envZeroRoot = std::getenv("MMSEQS_CMSCAN_ZERO_ROOT_TRANS"); - const bool zeroRootTrans = (envZeroRoot != NULL && envZeroRoot[0] == '1'); - if (zeroRootTrans && m.pBegin > 0.0 && !m.states.empty()) { - CmState &rootS = m.states[0]; - for (size_t k = 0; k < rootS.trans.size(); ++k) { - rootS.trans[k] = NEG_INF; - } - } - Debug(Debug::INFO) << "Infernal CM local cfg: PBEGIN=" << m.pBegin - << " PEND=" << m.pEnd - << " ELSELF=" << m.elSelf - << " beginHeads=" << beginHeads.size() - << " endHeads=" << endHeads.size() << "\n"; - } - - // Truncation DP support tables (gated on MMSEQS_CMSCAN_TRUNC_DP=1). - // Mirrors Infernal display.c:CreateEmitMap and cm_trunc.c:cm_tr_penalties_Create. - { - const char *envTrunc = std::getenv("MMSEQS_CMSCAN_TRUNC_DP"); - const bool enableTruncDp = (envTrunc != NULL && envTrunc[0] == '1'); - if (enableTruncDp) { - // Build node table: count nodes, record first-state and type per node. - int maxNode = -1; - for (const CmState &s : m.states) { - if (s.nodeIdx > maxNode) maxNode = s.nodeIdx; - } - const int nNodes = maxNode + 1; - m.nNodes = nNodes; - m.nodeFirstState.assign(static_cast(nNodes), -1); - m.nodeType.assign(static_cast(nNodes), CM_ND_UNKNOWN); - for (const CmState &s : m.states) { - if (s.nodeIdx >= 0 && s.isFirstOfNode) { - m.nodeFirstState[static_cast(s.nodeIdx)] = s.idx; - m.nodeType[static_cast(s.nodeIdx)] = s.nodeType; - } - } - // For each BIF node, derive left/right child node indices from its - // first-state's cfirst (-> BEGL state) and cnum (-> BEGR state). - std::vector bifLeftChild(static_cast(nNodes), -1); - std::vector bifRightChild(static_cast(nNodes), -1); - for (int n = 0; n < nNodes; ++n) { - if (m.nodeType[static_cast(n)] != CM_ND_BIF) continue; - const int firstSt = m.nodeFirstState[static_cast(n)]; - if (firstSt < 0) continue; - const CmState &bs = m.states[static_cast(firstSt)]; - if (bs.cfirst >= 0 && bs.cfirst < (int)m.states.size()) { - bifLeftChild[static_cast(n)] = m.states[static_cast(bs.cfirst)].nodeIdx; - } - if (bs.cnum >= 0 && bs.cnum < (int)m.states.size()) { - bifRightChild[static_cast(n)] = m.states[static_cast(bs.cnum)].nodeIdx; - } - } - - // CreateEmitMap: stack-based DFS in left/right pass order. - m.emapLpos.assign(static_cast(nNodes), -1); - m.emapRpos.assign(static_cast(nNodes), -1); - m.emapEpos.assign(static_cast(nNodes), -1); - // Stack entries: pairs of (on_right, nodeIdx). Mirror esl_stack push order. - std::vector> pda; - int cpos = 0; - pda.emplace_back(0, 0); // start: left side of node 0 - while (!pda.empty()) { - const int on_right = pda.back().first; - const int nd = pda.back().second; - pda.pop_back(); - const CmNodeType nt = (nd >= 0 && nd < nNodes) - ? m.nodeType[static_cast(nd)] - : CM_ND_UNKNOWN; - if (on_right) { - m.emapRpos[static_cast(nd)] = cpos + 1; - if (nt == CM_ND_MATP || nt == CM_ND_MATR) cpos++; - } else { - if (nt == CM_ND_MATP || nt == CM_ND_MATL) cpos++; - m.emapLpos[static_cast(nd)] = cpos; - if (nt == CM_ND_BIF) { - // Push BIF for right side, then right child, then left child. - pda.emplace_back(1, nd); - pda.emplace_back(0, bifRightChild[static_cast(nd)]); - pda.emplace_back(0, bifLeftChild[static_cast(nd)]); - } else { - pda.emplace_back(1, nd); - if (nt != CM_ND_END) { - pda.emplace_back(0, nd + 1); - } - } - } - } - // epos pass: from end of model back, propagate consensus position - // an EL insertion follows. - int eposCur = 0; - for (int nd = nNodes - 1; nd >= 0; --nd) { - const CmNodeType nt = m.nodeType[static_cast(nd)]; - if (nt == CM_ND_END) { - eposCur = m.emapLpos[static_cast(nd)]; - } else if (nt == CM_ND_BIF) { - const int rc = bifRightChild[static_cast(nd)]; - if (rc >= 0) eposCur = m.emapEpos[static_cast(rc)]; - } - m.emapEpos[static_cast(nd)] = eposCur; - } - // Sanity: rpos[0] - 1 must equal clen. - if (nNodes > 0 && m.emapRpos[0] - 1 != m.clen) { - Debug(Debug::WARNING) << "TRUNC_DP emap: rpos[0]-1=" << (m.emapRpos[0]-1) - << " differs from clen=" << m.clen << "\n"; - } - for (int nd = 0; nd < nNodes; ++nd) { - if (m.emapLpos[static_cast(nd)] < 0 - || m.emapRpos[static_cast(nd)] < 0 - || m.emapEpos[static_cast(nd)] < 0) { - Debug(Debug::WARNING) << "TRUNC_DP emap: node " << nd << " unfilled\n"; - } - } - Debug(Debug::INFO) << "TRUNC_DP emap built: nNodes=" << nNodes - << " clen=" << m.clen - << " (rpos[0]-1=" << (nNodes ? m.emapRpos[0]-1 : -1) << ")\n"; - - // Stage 3 + 4: compute psi[v] (cm_ExpectedStateOccupancy) and trp tables - // (cm_tr_penalties_Create). Mirrors cm.c:cm_ExpectedStateOccupancy and - // cm_trunc.c:cm_tr_penalties_Create. - const int M = (int)m.states.size(); - - // Linearize transitions; renormalize for end-eligible heads (Infernal - // CMH_LOCAL_END branch in cm_ExpectedStateOccupancy renormalizes - // t_copy[v] to discount the local-end probability mass). - std::vector> transLin((size_t)M); - for (int v = 0; v < M; ++v) { - const CmState &vs = m.states[(size_t)v]; - transLin[(size_t)v].resize(vs.trans.size(), 0.0); - double sum = 0.0; - for (size_t k = 0; k < vs.trans.size(); ++k) { - transLin[(size_t)v][k] = (vs.trans[k] == NEG_INF) - ? 0.0 : std::pow(2.0, vs.trans[k]); - sum += transLin[(size_t)v][k]; - } - if (vs.endSc != NEG_INF && sum > 1e-12 && std::fabs(sum - 1.0) > 1e-9) { - for (size_t k = 0; k < transLin[(size_t)v].size(); ++k) { - transLin[(size_t)v][k] /= sum; - } - } - } - // ROOT_S: under ZRT=1 all trans are NEG_INF (sum=0). Warn — psi will - // collapse to 0 for non-root states. Bit-exact requires ZRT=0. - if (M > 0 && transLin[0].size() > 0) { - double rootSum = 0.0; - for (double t : transLin[0]) rootSum += t; - if (rootSum < 1e-12) { - Debug(Debug::WARNING) << "TRUNC_DP: ROOT_S trans zeroed (ZRT=1?); " - << "psi will be degenerate. Disable ZRT for bit-exact.\n"; - } - } - - // psi[v] = expected number of times state v is entered (linear). - // Topological order: states are in CM order (parents always have smaller - // idx than children for non-S parents). Enumerate parents from cfirst/cnum. - // For S_st: psi=1.0 directly. For non-S: sum psi[parent]*t[parent->v]. - // For IL/IR: add geometric self-loop term t/(1-t). - std::vector psi((size_t)M, 0.0); - std::vector>> parents((size_t)M); - for (int x = 0; x < M; ++x) { - const CmState &xs = m.states[(size_t)x]; - if (xs.type == CM_ST_B || xs.type == CM_ST_E) continue; - if (xs.cnum <= 0 || xs.cfirst < 0) continue; - for (int k = 0; k < xs.cnum; ++k) { - int v = xs.cfirst + k; - if (v < 0 || v >= M) continue; - parents[(size_t)v].push_back({x, k}); - } - } - for (int v = 0; v < M; ++v) { - const CmState &vs = m.states[(size_t)v]; - if (vs.type == CM_ST_S) { psi[(size_t)v] = 1.0; continue; } - const bool is_insert = (vs.type == CM_ST_IL || vs.type == CM_ST_IR); - for (const auto &pp : parents[(size_t)v]) { - int x = pp.first; - int kIdx = pp.second; - if (is_insert && x == v) continue; // skip self-loop - if (kIdx < 0 || kIdx >= (int)transLin[(size_t)x].size()) continue; - psi[(size_t)v] += psi[(size_t)x] * transLin[(size_t)x][(size_t)kIdx]; - } - if (is_insert && !transLin[(size_t)v].empty()) { - double t0 = transLin[(size_t)v][0]; - if (t0 < 1.0) { - psi[(size_t)v] += psi[(size_t)v] * (t0 / (1.0 - t0)); - } - } - } - m.psi = psi; - - // begin[v] = local-begin probability per state (linear). Mirror - // cm_modelconfig.c:cm_CalculateLocalBeginProbs. - std::vector begin((size_t)M, 0.0); - int nstartsBegin = 0; - for (int nd = 2; nd < nNodes; ++nd) { - const CmNodeType nt = m.nodeType[(size_t)nd]; - if (nt == CM_ND_MATP || nt == CM_ND_MATL || - nt == CM_ND_MATR || nt == CM_ND_BIF) nstartsBegin++; - } - const double pBeginEff = (m.pBegin > 0.0) ? m.pBegin : 0.05; - if (nNodes >= 2) { - int v1 = m.nodeFirstState[1]; - if (v1 >= 0) begin[(size_t)v1] = 1.0 - pBeginEff; - } - if (nstartsBegin > 0) { - double pp = pBeginEff / (double)nstartsBegin; - for (int nd = 2; nd < nNodes; ++nd) { - const CmNodeType nt = m.nodeType[(size_t)nd]; - if (nt == CM_ND_MATP || nt == CM_ND_MATL || - nt == CM_ND_MATR || nt == CM_ND_BIF) { - int v = m.nodeFirstState[(size_t)nd]; - if (v >= 0) begin[(size_t)v] = pp; - } - } - } - - // trp tables: per-state truncation penalty for {5'+3', 5' only, 3' only}. - // Storage: trpG[type][v] global, trpL[type][v] local (both log2). - // Linear-space computation, convert to log2 at the end. - std::vector> trpGlin(3, std::vector((size_t)M, 0.0)); - std::vector> trpLlin(3, std::vector((size_t)M, 0.0)); - - const double g_5and3 = 2.0 / ((double)m.clen * (double)(m.clen + 1)); - const double g_5or3 = 1.0 / (double)m.clen; - double prv5 = 0.0, prv3 = 0.0, prv53 = 0.0; - - for (int nd = 0; nd < nNodes; ++nd) { - const CmNodeType nt = m.nodeType[(size_t)nd]; - int lpos = m.emapLpos[(size_t)nd]; - int rpos = m.emapRpos[(size_t)nd]; - if (!(nt == CM_ND_MATP || nt == CM_ND_MATL)) lpos += 1; - if (!(nt == CM_ND_MATP || nt == CM_ND_MATR)) rpos -= 1; - - if (nt == CM_ND_END) { - prv5 = prv3 = prv53 = 0.0; - } else if (nt == CM_ND_BEGL || nt == CM_ND_BEGR) { - int begS = m.nodeFirstState[(size_t)nd]; - int parentB = -1; - for (int x = 0; x < M; ++x) { - const CmState &xs = m.states[(size_t)x]; - if (xs.type != CM_ST_B) continue; - if (nt == CM_ND_BEGL && xs.cfirst == begS) { parentB = x; break; } - if (nt == CM_ND_BEGR && xs.cnum == begS) { parentB = x; break; } - } - if (parentB >= 0) { - prv5 = (nt == CM_ND_BEGL) ? 0.0 : trpLlin[1][(size_t)parentB]; - prv3 = (nt == CM_ND_BEGR) ? 0.0 : trpLlin[2][(size_t)parentB]; - prv53 = trpLlin[0][(size_t)parentB]; - } else { - prv5 = prv3 = prv53 = 0.0; - } - } else if (nt == CM_ND_MATP || nt == CM_ND_MATL || - nt == CM_ND_MATR || nt == CM_ND_BIF) { - int mState = m.nodeFirstState[(size_t)nd]; - int i1 = -1, i2 = -1; - if (nd >= 1) { - const CmNodeType prevNt = m.nodeType[(size_t)(nd - 1)]; - int prevFirst = m.nodeFirstState[(size_t)(nd - 1)]; - if (prevFirst >= 0) { - switch (prevNt) { - case CM_ND_MATP: i1 = prevFirst + 4; i2 = prevFirst + 5; break; - case CM_ND_MATL: i1 = prevFirst + 2; break; - case CM_ND_MATR: i1 = prevFirst + 2; break; - case CM_ND_BEGR: i1 = prevFirst + 1; break; - case CM_ND_ROOT: i1 = prevFirst + 1; i2 = prevFirst + 2; break; - default: break; - } - } - } - - double m_psi = psi[(size_t)mState]; - if (nt == CM_ND_MATP) { - m_psi += psi[(size_t)(mState + 1)] + psi[(size_t)(mState + 2)]; - } - double i1_psi = (i1 == -1) ? 0.0 : psi[(size_t)i1]; - double i2_psi = (i2 == -1) ? 0.0 : psi[(size_t)i2]; - double summed_psi = m_psi + i1_psi + i2_psi; - if (summed_psi <= 1e-30) summed_psi = 1.0; - - trpGlin[0][(size_t)mState] = (m_psi / summed_psi) * g_5and3; - if (i1 != -1) trpGlin[0][(size_t)i1] = (i1_psi / summed_psi) * g_5and3; - if (i2 != -1) trpGlin[0][(size_t)i2] = (i2_psi / summed_psi) * g_5and3; - if (rpos == m.clen) { - trpGlin[1][(size_t)mState] = (m_psi / summed_psi) * g_5or3; - if (i1 != -1) trpGlin[1][(size_t)i1] = (i1_psi / summed_psi) * g_5or3; - if (i2 != -1) trpGlin[1][(size_t)i2] = (i2_psi / summed_psi) * g_5or3; - } - if (lpos == 1) { - trpGlin[2][(size_t)mState] = (m_psi / summed_psi) * g_5or3; - if (i1 != -1) trpGlin[2][(size_t)i1] = (i1_psi / summed_psi) * g_5or3; - if (i2 != -1) trpGlin[2][(size_t)i2] = (i2_psi / summed_psi) * g_5or3; - } - - int subtree_clen = rpos - lpos + 1; - int nfrag5 = subtree_clen; - int nfrag3 = subtree_clen; - int nfrag53 = (subtree_clen * (subtree_clen + 1)) / 2; - if (nfrag5 <= 0) nfrag5 = 1; - if (nfrag3 <= 0) nfrag3 = 1; - if (nfrag53 <= 0) nfrag53 = 1; - - double cur5 = begin[(size_t)mState] / (double)nfrag5 + prv5; - double cur3 = begin[(size_t)mState] / (double)nfrag3 + prv3; - double cur53 = begin[(size_t)mState] / (double)nfrag53 + prv53; - - trpLlin[0][(size_t)mState] = (m_psi / summed_psi) * cur53; - if (i1 != -1) trpLlin[0][(size_t)i1] = (i1_psi / summed_psi) * cur53; - if (i2 != -1) trpLlin[0][(size_t)i2] = (i2_psi / summed_psi) * cur53; - trpLlin[1][(size_t)mState] = (m_psi / summed_psi) * cur5; - if (i1 != -1) trpLlin[1][(size_t)i1] = (i1_psi / summed_psi) * cur5; - if (i2 != -1) trpLlin[1][(size_t)i2] = (i2_psi / summed_psi) * cur5; - trpLlin[2][(size_t)mState] = (m_psi / summed_psi) * cur3; - if (i1 != -1) trpLlin[2][(size_t)i1] = (i1_psi / summed_psi) * cur3; - if (i2 != -1) trpLlin[2][(size_t)i2] = (i2_psi / summed_psi) * cur3; - - prv5 = (nt == CM_ND_MATL) ? cur5 : 0.0; - prv3 = (nt == CM_ND_MATR) ? cur3 : 0.0; - prv53 = cur53; - } - } - - // Convert linear -> log2; non-set entries (still 0.0) become NEG_INF. - const double LN2 = std::log(2.0); - m.trpG.assign(3, std::vector((size_t)M, NEG_INF)); - m.trpL.assign(3, std::vector((size_t)M, NEG_INF)); - for (int v = 0; v < M; ++v) { - for (int t = 0; t < 3; ++t) { - if (trpGlin[t][(size_t)v] > 0.0) - m.trpG[t][(size_t)v] = std::log(trpGlin[t][(size_t)v]) / LN2; - if (trpLlin[t][(size_t)v] > 0.0) - m.trpL[t][(size_t)v] = std::log(trpLlin[t][(size_t)v]) / LN2; - } - } - - // cm_CalcMargLikScores: per-state lmesc/rmesc for L/R modes. - // MP: marginal sum over partner * null. ML/IL: lmesc=esc; rmesc=0. - // MR/IR: rmesc=esc; lmesc=0. Other states: leave empty. - for (size_t v = 0; v < m.states.size(); ++v) { - CmState &s = m.states[v]; - s.lmesc.clear(); - s.rmesc.clear(); - if (s.type == CM_ST_MP && s.emit.size() >= 16) { - s.lmesc.assign(4, NEG_INF); - s.rmesc.assign(4, NEG_INF); - for (int x = 0; x < 4; ++x) { - double sumL = 0.0; - double sumR = 0.0; - for (int y = 0; y < 4; ++y) { - // emit[a*4+b] = log2(e[a,b] / (null[a]*null[b])) - // lmesc[x] = log2(sum_y e[x,y] / null[x]) - // = log2(sum_y null[y] * 2^emit[x*4+y]) - const double lEsc = s.emit[(size_t)(x * 4 + y)]; - const double rEsc = s.emit[(size_t)(y * 4 + x)]; - sumL += m.nullProb[(size_t)y] * std::pow(2.0, lEsc); - sumR += m.nullProb[(size_t)y] * std::pow(2.0, rEsc); - } - s.lmesc[(size_t)x] = (sumL > 0.0) ? std::log(sumL) / std::log(2.0) : NEG_INF; - s.rmesc[(size_t)x] = (sumR > 0.0) ? std::log(sumR) / std::log(2.0) : NEG_INF; - } - } else if ((s.type == CM_ST_ML || s.type == CM_ST_IL) && s.emit.size() >= 4) { - s.lmesc.assign(4, 0.0); - s.rmesc.assign(4, 0.0); - for (int x = 0; x < 4; ++x) s.lmesc[(size_t)x] = s.emit[(size_t)x]; - } else if ((s.type == CM_ST_MR || s.type == CM_ST_IR) && s.emit.size() >= 4) { - s.lmesc.assign(4, 0.0); - s.rmesc.assign(4, 0.0); - for (int x = 0; x < 4; ++x) s.rmesc[(size_t)x] = s.emit[(size_t)x]; - } - } - - m.hasTruncDp = true; - - const char *envDumpEmap = std::getenv("MMSEQS_CMSCAN_DUMP_EMAP"); - if (envDumpEmap != NULL && envDumpEmap[0] == '1') { - fprintf(stderr, "EMAP_DUMP nNodes=%d clen=%d\n", nNodes, m.clen); - fprintf(stderr, "%4s %7s %9s %4s %4s %4s\n", - "Node", "State1", "Type", "lpos", "rpos", "epos"); - static const char *ntStr[] = { - "?", "ROOT", "MATP", "MATL", "MATR", "BEGL", "BEGR", "BIF", "END" - }; - for (int nd = 0; nd < nNodes; ++nd) { - int t = (int)m.nodeType[(size_t)nd]; - if (t < 0 || t > 8) t = 0; - fprintf(stderr, "%4d %7d %9s %4d %4d %4d\n", - nd, m.nodeFirstState[(size_t)nd], ntStr[t], - m.emapLpos[(size_t)nd], m.emapRpos[(size_t)nd], - m.emapEpos[(size_t)nd]); - } - } - const char *envDumpMesc = std::getenv("MMSEQS_CMSCAN_DUMP_MESC"); - if (envDumpMesc != NULL && envDumpMesc[0] == '1') { - fprintf(stderr, "MESC_DUMP M=%d (MP-only rows shown for brevity)\n", M); - fprintf(stderr, "%4s %4s %10s %10s %10s %10s %10s %10s %10s %10s\n", - "v", "type", "lmA", "lmC", "lmG", "lmU", "rmA", "rmC", "rmG", "rmU"); - for (int v = 0; v < M; ++v) { - const CmState &s = m.states[(size_t)v]; - if (s.type != CM_ST_MP) continue; - if (s.lmesc.size() < 4 || s.rmesc.size() < 4) continue; - fprintf(stderr, "%4d %4d %10.6f %10.6f %10.6f %10.6f %10.6f %10.6f %10.6f %10.6f\n", - v, (int)s.type, - s.lmesc[0], s.lmesc[1], s.lmesc[2], s.lmesc[3], - s.rmesc[0], s.rmesc[1], s.rmesc[2], s.rmesc[3]); - } - } - const char *envDumpTrp = std::getenv("MMSEQS_CMSCAN_DUMP_TRP"); - if (envDumpTrp != NULL && envDumpTrp[0] == '1') { - fprintf(stderr, "TRP_DUMP M=%d clen=%d\n", M, m.clen); - fprintf(stderr, "%4s %4s %12s %12s %12s %12s %12s %12s\n", - "v", "type", "g_5+3", "g_5", "g_3", "l_5+3", "l_5", "l_3"); - for (int v = 0; v < M; ++v) { - if (m.trpG[0][(size_t)v] == NEG_INF - && m.trpL[0][(size_t)v] == NEG_INF) continue; - fprintf(stderr, "%4d %4d %12.6f %12.6f %12.6f %12.6f %12.6f %12.6f\n", - v, (int)m.states[(size_t)v].type, - m.trpG[0][(size_t)v], m.trpG[1][(size_t)v], m.trpG[2][(size_t)v], - m.trpL[0][(size_t)v], m.trpL[1][(size_t)v], m.trpL[2][(size_t)v]); - } - } - } - // Stage 6+7 verification: when MMSEQS_CMSCAN_TRUNC_TEST_FASTA points - // to a single-record FASTA, run the truncated CYK + trace dispatcher - // on it and dump the parsetree (gated by MMSEQS_CMSCAN_DUMP_TRPT=1). - // Decoupled from the production scan path; pure verification harness. - const char *envTestFasta = std::getenv("MMSEQS_CMSCAN_TRUNC_TEST_FASTA"); - const bool enableTruncDpHarness = (std::getenv("MMSEQS_CMSCAN_TRUNC_DP") != NULL - && std::getenv("MMSEQS_CMSCAN_TRUNC_DP")[0] == '1'); - if (enableTruncDpHarness && envTestFasta != NULL && envTestFasta[0] != '\0') { - std::ifstream fin(envTestFasta); - if (!fin.good()) { - std::fprintf(stderr, "[TRUNC_TEST] cannot open FASTA: %s\n", envTestFasta); - } else { - std::string fline, hdr, seq; - while (std::getline(fin, fline)) { - if (fline.empty()) continue; - if (fline[0] == '>') { - if (!hdr.empty()) break; - hdr = fline.substr(1); - } else { - for (char c : fline) { - if (!std::isspace(static_cast(c))) seq.push_back(c); - } - } - } - if (seq.empty()) { - std::fprintf(stderr, "[TRUNC_TEST] empty sequence in %s\n", envTestFasta); - } else { - const int L = (int)seq.size(); - std::vector dsq((size_t)(L + 2), -1); // 1-based, [0] sentinel - for (int p = 1; p <= L; ++p) { - char c = seq[(size_t)(p - 1)]; - if (c=='A'||c=='a') dsq[(size_t)p] = 0; - else if (c=='C'||c=='c') dsq[(size_t)p] = 1; - else if (c=='G'||c=='g') dsq[(size_t)p] = 2; - else if (c=='T'||c=='t'||c=='U'||c=='u') dsq[(size_t)p] = 3; - else dsq[(size_t)p] = -1; - } - const int pty_idx = 0; // TRPENALTY_5P_AND_3P (matches cmsearch glocal/local) - const char preset_mode = 'U'; // unknown — fill J/L/R/T - std::fprintf(stderr, "[TRUNC_TEST] hdr=%s L=%d pty_idx=%d preset=%c\n", - hdr.c_str(), L, pty_idx, preset_mode); - const TrCykResult &res = runTrCYKInsideAlign(m, dsq, L, preset_mode, pty_idx); - std::fprintf(stderr, "[TRUNC_TEST] DP done: score=%.4f mode=%c b=%d\n", - res.score, res.mode, res.b); - TrParsetree tr = runTrCYKAlignT(m, res, L, pty_idx); - std::fprintf(stderr, "[TRUNC_TEST] trace done: ok=%d nodes=%zu err=%s\n", - tr.ok ? 1 : 0, tr.nodes.size(), tr.err); - dumpTrParsetree(tr, m); - dumpTrCykAlphaFor(res, (int)m.states.size(), L, /*v_focus=*/0); - dumpTrCykAlphaFor(res, (int)m.states.size(), L, /*v_focus=*/84); - dumpTrCykAlphaFor(res, (int)m.states.size(), L, /*v_focus=*/9); - // Specifically: BEGL_S=85 left subtree of BIF v=84, - // BEGR_S=165 right subtree. Expect Ralpha[85][90][90]≈-9.13, - // Lalpha[165][103][13]≈-2.05 from Infernal trace breakdown. - if (std::getenv("MMSEQS_CMSCAN_DUMP_TRDP") != NULL - && std::getenv("MMSEQS_CMSCAN_DUMP_TRDP")[0] == '1') { - auto cellOr = [&](const std::vector>> &a, - int v, int j, int d) -> double { - if ((int)a.size() <= v) return -1e30; - if ((int)a[(size_t)v].size() <= j) return -1e30; - if ((int)a[(size_t)v][(size_t)j].size() <= d) return -1e30; - return (double)a[(size_t)v][(size_t)j][(size_t)d]; - }; - std::fprintf(stderr, "[TRDP_BIF] v=85 (BEGL_S, left of BIF v=84):\n"); - for (int dd : {88, 89, 90, 91, 92}) { - for (int jj : {90}) { - if (dd > jj) continue; - std::fprintf(stderr, "[TRDP_BIF] v=85 j=%d d=%d J=%.4f L=%.4f R=%.4f\n", - jj, dd, - cellOr(res.Jalpha, 85, jj, dd), - cellOr(res.Lalpha, 85, jj, dd), - cellOr(res.Ralpha, 85, jj, dd)); - } - } - // Walk down the broken left subtree to find where the gap appears. - // Chain (Infernal trace): v=86 MP → 92 MP → 98 MP → 104 ML → 107 ML → - // 110 ML → 113 MR → 116 MR → 119 MR → 122 MR → 125 MR → 299 EL - // Inside-score reaching v=86 from the leaf side should be ≈ -9 + tsc(85→86). - // Probe v=86, v=125 in J/L/R modes at relevant cells. - std::fprintf(stderr, "[TRDP_BIF] v=86 (first MP after BEGL_S):\n"); - for (int dd : {88, 89, 90}) { - for (int jj : {90}) { - if (dd > jj) continue; - std::fprintf(stderr, "[TRDP_BIF] v=86 j=%d d=%d J=%.4f L=%.4f R=%.4f\n", - jj, dd, - cellOr(res.Jalpha, 86, jj, dd), - cellOr(res.Lalpha, 86, jj, dd), - cellOr(res.Ralpha, 86, jj, dd)); - } - } - // v=125 is the MR that local-end-pops into EL absorbing 79 residues. - // Infernal trace: tsc=-17.50, then EL spans residues 4..82 = 79 res. - // Inside score at v=125 j=82 d=80 should be ≈ -17.50 + esc(MR@A) + elSelf*79. - std::fprintf(stderr, "[TRDP_BIF] v=125 (MR with local-end pop in J trace):\n"); - for (int dd : {78, 79, 80, 81}) { - for (int jj : {82, 83}) { - if (dd > jj) continue; - std::fprintf(stderr, "[TRDP_BIF] v=125 j=%d d=%d J=%.4f L=%.4f R=%.4f\n", - jj, dd, - cellOr(res.Jalpha, 125, jj, dd), - cellOr(res.Lalpha, 125, jj, dd), - cellOr(res.Ralpha, 125, jj, dd)); - } - } - // EL deck (state index = M). - std::fprintf(stderr, "[TRDP_BIF] v=M (EL deck):\n"); - for (int dd : {0, 1, 79, 80}) { - std::fprintf(stderr, "[TRDP_BIF] v=%d j=%d d=%d J=%.4f L=%.4f R=%.4f\n", - (int)m.states.size(), dd, dd, - cellOr(res.Jalpha, (int)m.states.size(), dd, dd), - cellOr(res.Lalpha, (int)m.states.size(), dd, dd), - cellOr(res.Ralpha, (int)m.states.size(), dd, dd)); - } - // Print key model parameters: elSelf, endsc[125], state types. - std::fprintf(stderr, "[TRDP_BIF] elSelf=%.4f localOn=%d pBegin=%.4f pEnd=%.4f\n", - m.elSelf, (int)m.hasLocalCfg, m.pBegin, m.pEnd); - if (125 < (int)m.states.size()) { - std::fprintf(stderr, "[TRDP_BIF] state[125] type=%d endSc=%.4f beginSc=%.4f cfirst=%d cnum=%d\n", - (int)m.states[125].type, m.states[125].endSc, m.states[125].beginSc, - m.states[125].cfirst, m.states[125].cnum); - } - std::fprintf(stderr, "[TRDP_BIF] v=165 (BEGR_S, right of BIF v=84):\n"); - for (int dd : {11, 12, 13, 14, 15}) { - for (int jj : {103}) { - if (dd > jj) continue; - std::fprintf(stderr, "[TRDP_BIF] v=165 j=%d d=%d J=%.4f L=%.4f R=%.4f\n", - jj, dd, - cellOr(res.Jalpha, 165, jj, dd), - cellOr(res.Lalpha, 165, jj, dd), - cellOr(res.Ralpha, 165, jj, dd)); - } - } - } - // Per-state argmax(J,L,R,T)+trpenalty, including all candidate b values. - if (std::getenv("MMSEQS_CMSCAN_DUMP_TRDP") != NULL - && std::getenv("MMSEQS_CMSCAN_DUMP_TRDP")[0] == '1') { - std::fprintf(stderr, "[TRDP_BCAND] v J+pty5+3 L+pty5 R+pty3 T+pty5+3\n"); - for (size_t v = 0; v < m.states.size(); ++v) { - const CmState &s = m.states[v]; - if (s.type != CM_ST_MP && s.type != CM_ST_ML - && s.type != CM_ST_MR && s.type != CM_ST_B - && s.type != CM_ST_S) continue; - if (pty_idx >= (int)m.trpL.size()) continue; - double pJ = (m.trpL[0].size() > v && m.trpL[0][v] > -1e29) ? m.trpL[0][v] : 0.0; - double pL = (m.trpL[1].size() > v && m.trpL[1][v] > -1e29) ? m.trpL[1][v] : 0.0; - double pR = (m.trpL[2].size() > v && m.trpL[2][v] > -1e29) ? m.trpL[2][v] : 0.0; - double j = (res.Jalpha.size() > v && res.Jalpha[v].size() > (size_t)L - && res.Jalpha[v][(size_t)L].size() > (size_t)L) - ? res.Jalpha[v][(size_t)L][(size_t)L] : -1e30; - double l = (res.Lalpha.size() > v && res.Lalpha[v].size() > (size_t)L - && res.Lalpha[v][(size_t)L].size() > (size_t)L) - ? res.Lalpha[v][(size_t)L][(size_t)L] : -1e30; - double rr = (res.Ralpha.size() > v && res.Ralpha[v].size() > (size_t)L - && res.Ralpha[v][(size_t)L].size() > (size_t)L) - ? res.Ralpha[v][(size_t)L][(size_t)L] : -1e30; - double t = (res.Talpha.size() > v && res.Talpha[v].size() > (size_t)L - && res.Talpha[v][(size_t)L].size() > (size_t)L) - ? res.Talpha[v][(size_t)L][(size_t)L] : -1e30; - std::fprintf(stderr, "[TRDP_BCAND] %3zu %10.4f %10.4f %10.4f %10.4f\n", - v, - (j > -1e29) ? j + pJ : -999.0, - (l > -1e29) ? l + pL : -999.0, - (rr > -1e29) ? rr + pR : -999.0, - (t > -1e29) ? t + pJ : -999.0); - } - } - } - } - } - } - - Debug(Debug::INFO) << "Infernal CM exact parser: CLEN=" << m.clen - << " W=" << m.w - << " states=" << m.states.size() << "\n"; - return m; -} - -static InfernalExactModel parseInfernalCmExactModel(const std::string &path) { - std::ifstream in(path.c_str()); - if (!in.good()) { - Debug(Debug::ERROR) << "Cannot open Infernal CM file: " << path << "\n"; - EXIT(EXIT_FAILURE); - } - return parseInfernalCmExactModelFromStream(in, path); -} - -static bool hasDbIndex(const std::string &path) { - return FileUtil::fileExists((path + ".index").c_str()); -} - -// Decode a single sequence from an open DBReader by internal index. -static FastaSeq decodeOneSequence(DBReader &dbr, - size_t id, - BaseMatrix &subMat, - Sequence &seqObj, - bool useDinucMapping, - bool isGpuDb, - unsigned int thread_idx) { - FastaSeq cur; - cur.key = dbr.getDbKey(id); - const bool hasLookup = (dbr.getLookupSize() > 0); - if (hasLookup) { - const size_t lid = dbr.getLookupIdByKey(cur.key); - cur.id = dbr.getLookupEntryName(lid); - } else { - cur.id = std::to_string(cur.key); - } - const size_t seqLen = dbr.getSeqLen(id); - if (isGpuDb) { - const unsigned char *data = - reinterpret_cast(dbr.getDataUncompressed(id)); - seqObj.mapSequence(id, cur.key, std::make_pair(data, seqLen)); - } else { - const char *data = dbr.getData(id, thread_idx); - seqObj.mapSequence(id, cur.key, data, seqLen); - } - const Sequence::SeqAuxInfo *auxInfo = Sequence::getAuxInfo(seqObj.getSeqType()); - const unsigned char *num2outputnum = (useDinucMapping && auxInfo != NULL) - ? auxInfo->num2outputnum - : NULL; - cur.seq = decodeMappedSequenceToRna(seqObj, subMat, num2outputnum); - return cur; -} - -struct Hit { - unsigned int dbKey = 0; - unsigned int dbLen = 0; - std::string seqId; - int start1 = 0; - int end1 = 0; - char mode = 'J'; // J/L/R/T - bool trunc = false; - double cyk = NEG_INF; - double inside = NEG_INF; - double bias = 0.0; - double evalue = NEG_INF; - bool hasEvalue = false; - float precomputedSeqId = -1.0f; // trace-based pid, <0 means fall back to score-per-col estimate - std::string cigar; - std::string traceStates; - // Query-coord alignment span derived from CIGAR (with --hand: consensus - // col c == query position c-1). qStart/qEnd are 0-indexed; cigarAlnLen is - // the total CIGAR op count (M+D+I) in MMseqs convention. Negative qStart - // means "not populated; fall back to legacy emit". - int qStart = -1; - int qEnd = -1; - unsigned int cigarAlnLen = 0; - int leadingInsertTargets = 0; - int trailingInsertTargets = 0; -}; - -static inline char traceOpForState(CmStateType t) { - if (t == CM_ST_MP) return 'M'; - if (t == CM_ST_ML || t == CM_ST_MR) return 'M'; - if (t == CM_ST_IL || t == CM_ST_IR) return 'I'; - return '\0'; -} - -static std::string rleTraceOps(const std::string &ops) { - if (ops.empty()) { - return "NA"; - } - std::string out; - out.reserve(ops.size() * 2); - char cur = ops[0]; - int run = 1; - for (size_t i = 1; i < ops.size(); ++i) { - if (ops[i] == cur) { - ++run; - } else { - out += std::to_string(run); - out.push_back(cur); - cur = ops[i]; - run = 1; - } - } - out += std::to_string(run); - out.push_back(cur); - return out; -} - -static std::string encodeTraceStates(const std::vector &states) { - if (states.empty()) { - return "NA"; - } - std::string out; - out.reserve(states.size() * 4); - for (size_t i = 0; i < states.size(); ++i) { - if (i > 0) { - out.push_back(','); - } - out += std::to_string(states[i]); - } - return out; -} - -static std::string buildConsensusFromExactModel(const InfernalExactModel &model) { - if (model.clen <= 0) { - return std::string(); - } - std::string cons(static_cast(model.clen), 'N'); - std::vector bestSc(static_cast(model.clen), NEG_INF); - static const char bases[4] = {'A', 'C', 'G', 'U'}; - for (size_t si = 0; si < model.states.size(); ++si) { - const CmState &s = model.states[si]; - if (s.type == CM_ST_MP && s.emit.size() >= 16) { - if (s.mapLeft > 0 && s.mapLeft <= model.clen) { - int bi = 0; - double bsc = NEG_INF; - for (int a = 0; a < 4; ++a) { - double row = NEG_INF; - for (int b = 0; b < 4; ++b) { - row = std::max(row, s.emit[static_cast(a * 4 + b)]); - } - if (row > bsc) { - bsc = row; - bi = a; - } - } - const size_t pos = static_cast(s.mapLeft - 1); - if (bsc > bestSc[pos]) { - bestSc[pos] = bsc; - cons[pos] = bases[bi]; - } - } - if (s.mapRight > 0 && s.mapRight <= model.clen) { - int bi = 0; - double bsc = NEG_INF; - for (int b = 0; b < 4; ++b) { - double col = NEG_INF; - for (int a = 0; a < 4; ++a) { - col = std::max(col, s.emit[static_cast(a * 4 + b)]); - } - if (col > bsc) { - bsc = col; - bi = b; - } - } - const size_t pos = static_cast(s.mapRight - 1); - if (bsc > bestSc[pos]) { - bestSc[pos] = bsc; - cons[pos] = bases[bi]; - } - } - } else if ((s.type == CM_ST_ML || s.type == CM_ST_MR) && s.emit.size() >= 4) { - int mapPos = (s.type == CM_ST_ML) ? s.mapLeft : s.mapRight; - if (mapPos > 0 && mapPos <= model.clen) { - int bi = 0; - double bsc = s.emit[0]; - for (int a = 1; a < 4; ++a) { - if (s.emit[static_cast(a)] > bsc) { - bsc = s.emit[static_cast(a)]; - bi = a; - } - } - const size_t pos = static_cast(mapPos - 1); - if (bsc > bestSc[pos]) { - bestSc[pos] = bsc; - cons[pos] = bases[bi]; - } - } - } - } - return cons; -} - -// Build a query-coord MMseqs CIGAR from the CYK trace. -// With cmbuild --hand + all-'x' RF, consensus column c maps 1:1 to query -// position c-1, so per-column ops are query-coord directly. MMseqs convention: -// M = match (consumes 1 query, 1 target) -// D = consumes 1 query, 0 target -> CM delete state at column -// I = consumes 0 query, 1 target -> CM insert state between columns -// We trim to firstMatchCol..lastMatchCol so leading/trailing delete-only -// columns (which are walked by truncated CYK but are not really "covered") -// don't inflate the alignment span. outQStart/outQEnd are 0-indexed query -// positions; outAlnLen is the total number of CIGAR ops (M+D+I). -static std::string modelTraceCigarQueryCoord(const InfernalExactModel &model, - const std::vector &traceStates, - int *outQStart, int *outQEnd, - int *outAlnLen, - int *outLeadingInsertTargets, - int *outTrailingInsertTargets, - const std::vector *traceModes = nullptr) { - // Mode-aware emit semantics (Infernal cm_dpalign_trunc.c ModeEmits): - // J: MP both, ML left, MR right - // L: MP left, ML left, MR silent - // R: MP right, ML silent, MR right - // T: MP/ML/MR all silent (T only legal at root/BIF) - auto emitsLeft = [](int8_t mode) { - return mode == TR_TRMODE_J || mode == TR_TRMODE_L; - }; - auto emitsRight = [](int8_t mode) { - return mode == TR_TRMODE_J || mode == TR_TRMODE_R; - }; - if (outQStart) *outQStart = -1; - if (outQEnd) *outQEnd = -1; - if (outAlnLen) *outAlnLen = 0; - if (outLeadingInsertTargets) *outLeadingInsertTargets = 0; - if (outTrailingInsertTargets) *outTrailingInsertTargets = 0; - if (traceStates.empty()) { - return "NA"; - } - int maxMap = 0; - int minMap = std::numeric_limits::max(); - for (size_t i = 0; i < traceStates.size(); ++i) { - const int sid = traceStates[i]; - if (sid < 0 || sid >= static_cast(model.states.size())) { - continue; - } - const CmState &s = model.states[static_cast(sid)]; - if (s.mapLeft > 0) { - maxMap = std::max(maxMap, s.mapLeft); - minMap = std::min(minMap, s.mapLeft); - } - if (s.mapRight > 0) { - maxMap = std::max(maxMap, s.mapRight); - minMap = std::min(minMap, s.mapRight); - } - } - if (maxMap <= 0 || minMap == std::numeric_limits::max()) { - return "NA"; - } - std::vector mop(static_cast(maxMap + 1), '\0'); - std::vector insAfter(static_cast(maxMap + 1), 0); - // ROOT_IL anchors before col 1 (mapLeft==0) and ROOT_IR anchors after col - // CLEN (mapRight==CLEN+1==maxMap+1). They consume target residues but - // don't fit in insAfter[1..maxMap]; bin them here so they get attributed - // to leading/trailing inserts below — otherwise dbEnd-dbStart+1 exceeds - // the CIGAR's M+I and downstream walkers (result2dnamsa) overshoot. - int prefixIns = 0; - int suffixIns = 0; - for (size_t i = 0; i < traceStates.size(); ++i) { - const int sid = traceStates[i]; - if (sid < 0 || sid >= static_cast(model.states.size())) { - continue; - } - const CmState &s = model.states[static_cast(sid)]; - // Mode-aware emit (truncation modes). Without modes, treat as J. - const int8_t md = (traceModes && i < traceModes->size()) - ? (*traceModes)[i] - : (int8_t)TR_TRMODE_J; - const bool emL = emitsLeft(md); - const bool emR = emitsRight(md); - if (s.type == CM_ST_MP) { - if (emL && s.mapLeft > 0 && s.mapLeft <= maxMap) { - mop[static_cast(s.mapLeft)] = 'M'; - } - if (emR && s.mapRight > 0 && s.mapRight <= maxMap) { - mop[static_cast(s.mapRight)] = 'M'; - } - } else if (s.type == CM_ST_ML) { - if (emL && s.mapLeft > 0 && s.mapLeft <= maxMap) { - mop[static_cast(s.mapLeft)] = 'M'; - } - } else if (s.type == CM_ST_MR) { - if (emR && s.mapRight > 0 && s.mapRight <= maxMap) { - mop[static_cast(s.mapRight)] = 'M'; - } - } else if (s.type == CM_ST_D) { - if (s.mapLeft > 0 && s.mapLeft <= maxMap && mop[static_cast(s.mapLeft)] == '\0') { - mop[static_cast(s.mapLeft)] = 'D'; - } - if (s.mapRight > 0 && s.mapRight <= maxMap && mop[static_cast(s.mapRight)] == '\0') { - mop[static_cast(s.mapRight)] = 'D'; - } - } else if (s.type == CM_ST_IL) { - // IL inserts AFTER its left consensus column. ROOT_IL has mapLeft==0 - // and conceptually inserts before col 1 → bin as prefix. - // Mode-aware: IL only emits in mode J or L (silent in R/T). - if (!emL) { /* silent */ } - else { - int anchor = s.mapLeft; - if (anchor <= 0) { - prefixIns += 1; - } else if (anchor > maxMap) { - suffixIns += 1; - } else { - insAfter[static_cast(anchor)] += 1; - } - } - } else if (s.type == CM_ST_IR) { - // IR inserts BEFORE its right consensus column → anchor at mapRight-1. - // ROOT_IR has mapRight==CLEN+1 (=maxMap+1) so anchor==maxMap (valid). - // Mode-aware: IR only emits in mode J or R (silent in L/T). - if (!emR) { /* silent */ } - else { - int anchor = (s.mapRight > 0) ? s.mapRight - 1 : 0; - if (anchor <= 0) { - prefixIns += 1; - } else if (anchor > maxMap) { - suffixIns += 1; - } else { - insAfter[static_cast(anchor)] += 1; - } - } - } - } - int firstM = 0; - int lastM = 0; - for (int p = 1; p <= maxMap; ++p) { - if (mop[static_cast(p)] == 'M') { - if (firstM == 0) firstM = p; - lastM = p; - } - } - if (firstM == 0) { - // No match-state coverage; fall back to whole-trace span so callers can - // still report something rather than failing silently. - firstM = std::max(1, minMap); - lastM = maxMap; - } - // Count target residues consumed by inserts that fall outside the - // [firstM, lastM] window, so the caller can shrink dbStart/dbEnd to align - // with the trimmed CIGAR. Insert states with anchor < firstM consumed - // target before the alignment region; anchor > lastM consumed after. - int leadingIns = prefixIns; - int trailingIns = suffixIns; - for (int p = 1; p < firstM && p <= maxMap; ++p) { - leadingIns += insAfter[static_cast(p)]; - } - for (int p = lastM + 1; p <= maxMap; ++p) { - trailingIns += insAfter[static_cast(p)]; - } - if (outLeadingInsertTargets) *outLeadingInsertTargets = leadingIns; - if (outTrailingInsertTargets) *outTrailingInsertTargets = trailingIns; - std::string ops; - ops.reserve(static_cast(lastM - firstM + 1) + 32); - for (int p = firstM; p <= lastM; ++p) { - char c = mop[static_cast(p)]; - if (c == '\0') c = 'D'; // unwalked column inside hit envelope = query gap - ops.push_back((c == 'M') ? 'M' : 'D'); - const int ins = insAfter[static_cast(p)]; - for (int k = 0; k < ins; ++k) { - ops.push_back('I'); - } - } - if (outQStart) *outQStart = firstM - 1; - if (outQEnd) *outQEnd = lastM - 1; - if (outAlnLen) *outAlnLen = static_cast(ops.size()); - return rleTraceOps(ops); -} - -static std::string modelTraceConsensusForCigar(const InfernalExactModel &model, - const std::vector &traceStates) { - if (traceStates.empty()) { - return std::string(); - } - int maxMap = 0; - int minMap = std::numeric_limits::max(); - for (size_t i = 0; i < traceStates.size(); ++i) { - const int sid = traceStates[i]; - if (sid < 0 || sid >= static_cast(model.states.size())) { - continue; - } - const CmState &s = model.states[static_cast(sid)]; - if (s.mapLeft > 0) { - maxMap = std::max(maxMap, s.mapLeft); - minMap = std::min(minMap, s.mapLeft); - } - if (s.mapRight > 0) { - maxMap = std::max(maxMap, s.mapRight); - minMap = std::min(minMap, s.mapRight); - } - } - if (maxMap <= 0 || minMap == std::numeric_limits::max()) { - return std::string(); - } - - std::vector mop(static_cast(maxMap + 1), '\0'); - for (size_t i = 0; i < traceStates.size(); ++i) { - const int sid = traceStates[i]; - if (sid < 0 || sid >= static_cast(model.states.size())) { - continue; - } - const CmState &s = model.states[static_cast(sid)]; - if (s.type == CM_ST_MP) { - if (s.mapLeft > 0 && s.mapLeft <= maxMap) { - mop[static_cast(s.mapLeft)] = 'M'; - } - if (s.mapRight > 0 && s.mapRight <= maxMap) { - mop[static_cast(s.mapRight)] = 'M'; - } - } else if (s.type == CM_ST_ML || s.type == CM_ST_MR) { - int p = (s.type == CM_ST_ML) ? s.mapLeft : s.mapRight; - if (p > 0 && p <= maxMap) { - mop[static_cast(p)] = 'M'; - } - } else if (s.type == CM_ST_D) { - if (s.mapLeft > 0 && s.mapLeft <= maxMap && mop[static_cast(s.mapLeft)] == '\0') { - mop[static_cast(s.mapLeft)] = 'D'; - } - if (s.mapRight > 0 && s.mapRight <= maxMap && mop[static_cast(s.mapRight)] == '\0') { - mop[static_cast(s.mapRight)] = 'D'; - } - } - } - - const std::string baseCons = buildConsensusFromExactModel(model); - const char bases[4] = {'A', 'C', 'G', 'U'}; - std::vector bestSc(static_cast(maxMap + 1), NEG_INF); - std::vector bestCh(static_cast(maxMap + 1), 'N'); - for (int p = minMap; p <= maxMap; ++p) { - if (p >= 1 && p <= static_cast(baseCons.size())) { - bestCh[static_cast(p)] = normalizeBase(baseCons[static_cast(p - 1)]); - } - } - for (size_t i = 0; i < traceStates.size(); ++i) { - const int sid = traceStates[i]; - if (sid < 0 || sid >= static_cast(model.states.size())) { - continue; - } - const CmState &s = model.states[static_cast(sid)]; - if (s.type == CM_ST_MP && s.emit.size() >= 16) { - if (s.mapLeft > 0 && s.mapLeft <= maxMap) { - int bi = 0; - double bsc = NEG_INF; - for (int a = 0; a < 4; ++a) { - double row = NEG_INF; - for (int b = 0; b < 4; ++b) { - row = std::max(row, s.emit[static_cast(a * 4 + b)]); - } - if (row > bsc) { - bsc = row; - bi = a; - } - } - const size_t pos = static_cast(s.mapLeft); - if (bsc > bestSc[pos]) { - bestSc[pos] = bsc; - bestCh[pos] = bases[bi]; - } - } - if (s.mapRight > 0 && s.mapRight <= maxMap) { - int bi = 0; - double bsc = NEG_INF; - for (int b = 0; b < 4; ++b) { - double col = NEG_INF; - for (int a = 0; a < 4; ++a) { - col = std::max(col, s.emit[static_cast(a * 4 + b)]); - } - if (col > bsc) { - bsc = col; - bi = b; - } - } - const size_t pos = static_cast(s.mapRight); - if (bsc > bestSc[pos]) { - bestSc[pos] = bsc; - bestCh[pos] = bases[bi]; - } - } - } else if ((s.type == CM_ST_ML || s.type == CM_ST_MR) && s.emit.size() >= 4) { - const int mapPos = (s.type == CM_ST_ML) ? s.mapLeft : s.mapRight; - if (mapPos > 0 && mapPos <= maxMap) { - int bi = 0; - double bsc = s.emit[0]; - for (int a = 1; a < 4; ++a) { - if (s.emit[static_cast(a)] > bsc) { - bsc = s.emit[static_cast(a)]; - bi = a; - } - } - const size_t pos = static_cast(mapPos); - if (bsc > bestSc[pos]) { - bestSc[pos] = bsc; - bestCh[pos] = bases[bi]; - } - } - } - } - - std::string out; - out.reserve(static_cast(maxMap - minMap + 1)); - for (int p = minMap; p <= maxMap; ++p) { - if (mop[static_cast(p)] == '\0') { - continue; - } - out.push_back(bestCh[static_cast(p)]); - } - return out; -} - -static std::string reverseRleTraceOps(const std::string &rle) { - struct Run { - int n; - char op; - }; - std::vector runs; - runs.reserve(rle.size() / 2 + 1); - size_t pos = 0; - while (pos < rle.size()) { - if (!std::isdigit(static_cast(rle[pos]))) { - return rle; - } - int n = 0; - while (pos < rle.size() && std::isdigit(static_cast(rle[pos]))) { - n = n * 10 + (rle[pos] - '0'); - ++pos; - } - if (pos >= rle.size() || n <= 0) { - return rle; - } - runs.push_back(Run{n, rle[pos++]}); - } - - std::string out; - out.reserve(rle.size()); - for (std::vector::const_reverse_iterator it = runs.rbegin(); it != runs.rend(); ++it) { - if (!out.empty() && out.back() == it->op) { - size_t digitEnd = out.size() - 1; - size_t digitStart = digitEnd; - while (digitStart > 0 && std::isdigit(static_cast(out[digitStart - 1]))) { - --digitStart; - } - int prev = std::atoi(out.c_str() + digitStart); - out.erase(digitStart); - out += std::to_string(prev + it->n); - out.push_back(it->op); - } else { - out += std::to_string(it->n); - out.push_back(it->op); - } - } - return out; -} - -static std::vector decodeTraceStates(const std::string &enc) { - std::vector out; - if (enc.empty() || enc == "NA") { - return out; - } - size_t i = 0; - while (i < enc.size()) { - size_t j = i; - while (j < enc.size() && enc[j] != ',') { - ++j; - } - if (j > i) { - out.push_back(std::atoi(enc.substr(i, j - i).c_str())); - } - i = (j < enc.size()) ? (j + 1) : j; - } - return out; -} - -static float seqIdFromCigarConsensus(const std::string &cigar, - const std::string &consensus, - const std::string &observed) { - if (cigar.empty() || cigar == "NA" || consensus.empty() || observed.empty()) { - return 0.0f; - } - size_t iObs = 0; - size_t iCon = 0; - size_t ident = 0; - size_t denom = 0; - size_t p = 0; - while (p < cigar.size()) { - size_t cnt = 0; - while (p < cigar.size() && std::isdigit(static_cast(cigar[p])) != 0) { - cnt = cnt * 10 + static_cast(cigar[p] - '0'); - ++p; - } - if (cnt == 0) { - cnt = 1; - } - if (p >= cigar.size()) { - break; - } - const char op = cigar[p++]; - if (op == 'M') { - for (size_t k = 0; k < cnt; ++k) { - if (iObs >= observed.size() || iCon >= consensus.size()) { - break; - } - const char a = normalizeBase(observed[iObs++]); - const char b = normalizeBase(consensus[iCon++]); - if (a != 'N' && b != 'N') { - denom += 1; - } - if (a == b && a != 'N') { - ident += 1; - } - } - } else if (op == 'I') { - // MMseqs convention: I = target-only (gap on query) → advance observed. - const size_t step = std::min(cnt, observed.size() - std::min(iObs, observed.size())); - iObs += step; - denom += step; - } else if (op == 'D') { - // MMseqs convention: D = query-only (gap on target) → advance consensus. - const size_t step = std::min(cnt, consensus.size() - std::min(iCon, consensus.size())); - iCon += step; - denom += step; - } - } - if (denom == 0) { - return 0.0f; - } - return static_cast(ident) / static_cast(denom); -} - -static inline double cmStateEmitScoreFast(const ExactStateExec &st, const std::vector &seqCode, int i, int j) { - if (st.type == CM_ST_MP) { - if (i > j || i < 1 || j >= static_cast(seqCode.size()) || st.emitSize < 16 || st.emitPtr == NULL) { - return NEG_INF; - } - const int li = seqCode[static_cast(i)]; - const int ri = seqCode[static_cast(j)]; - if (li < 0 || ri < 0) { - return -1.0; - } - return st.emitPtr[li * 4 + ri]; - } - if (st.type == CM_ST_ML || st.type == CM_ST_IL) { - if (i > j || i < 1 || j >= static_cast(seqCode.size()) || st.emitSize < 4 || st.emitPtr == NULL) { - return NEG_INF; - } - const int bi = seqCode[static_cast(i)]; - if (bi < 0) { - return -1.0; - } - return st.emitPtr[bi]; - } - if (st.type == CM_ST_MR || st.type == CM_ST_IR) { - if (i > j || i < 1 || j >= static_cast(seqCode.size()) || st.emitSize < 4 || st.emitPtr == NULL) { - return NEG_INF; - } - const int bi = seqCode[static_cast(j)]; - if (bi < 0) { - return -1.0; - } - return st.emitPtr[bi]; - } - return 0.0; -} - -struct ExactRecCtx { - int N; - int M; - const std::vector *exec; - const ExactStateExec *execData; - const StateId *trDst; - const double *trSc; - const std::vector *seqCode; - // Per-state ragged (v, i, d) chart — float score + uint32 generation counter. - // Each state v has band [bandDmin[v] .. bandDmin[v]+bandSize[v]-1] derived - // from QDB (CmState::dmin1/dmax1). Cells for v are laid out as - // bandSize[v] * (N+1) consecutive floats starting at chartOffset[v]. - float *chartScore; - uint32_t *chartSeen; - uint32_t chartGen; - const size_t *chartOffset; // M+1 entries; chartOffset[M] = total cells - const int *chartBandDmin; // M entries - const int *chartBandSize; // M entries (= dmax-dmin+1, 0 if empty) -}; - -static inline size_t exactChartIndex(const ExactRecCtx &ctx, int v, int i, int d) { - return ctx.chartOffset[v] - + static_cast(i) * static_cast(ctx.chartBandSize[v]) - + static_cast(d - ctx.chartBandDmin[v]); -} - -static double exactVitRec(ExactRecCtx &ctx, int v, int i, int j) { - if (v < 0 || v >= ctx.M || i < 1 || i > ctx.N + 1 || j < 0 || j > ctx.N || i > j + 1) { - return NEG_INF; - } - const int d = j - i + 1; - const int bandDmin = ctx.chartBandDmin[v]; - const int bandSize = ctx.chartBandSize[v]; - if (bandSize <= 0 || d < bandDmin || d >= bandDmin + bandSize) { - // Out-of-band — NEG_INF. Cheap to recompute, do not memoize. - return NEG_INF; - } - const size_t dk = exactChartIndex(ctx, v, i, d); - if (ctx.chartSeen[dk] == ctx.chartGen) { - return static_cast(ctx.chartScore[dk]); - } - - const ExactStateExec &st = ctx.execData[static_cast(v)]; - if (j < i - 1) { - return NEG_INF; - } - if (st.type == CM_ST_E) { - const double rv = (i == j + 1) ? 0.0 : NEG_INF; - ctx.chartSeen[dk] = ctx.chartGen; - ctx.chartScore[dk] = static_cast(rv); - return rv; - } - if (st.type == CM_ST_B) { - const int y = st.bLeft; - const int z = st.bRight; - double best = NEG_INF; - int kBeg = i - 1; - int kEnd = j; - if (st.splitKMax >= st.splitKMin) { - kBeg = std::max(kBeg, i + st.splitKMin - 1); - kEnd = std::min(kEnd, i + st.splitKMax - 1); - } - if (st.splitRMax >= st.splitRMin) { - kBeg = std::max(kBeg, i + (d - st.splitRMax) - 1); - kEnd = std::min(kEnd, i + (d - st.splitRMin) - 1); - } - for (int k = kBeg; k <= kEnd; ++k) { - const double left = exactVitRec(ctx, y, i, k); - const double right = exactVitRec(ctx, z, k + 1, j); - if (left == NEG_INF || right == NEG_INF) { - continue; - } - best = std::max(best, left + right); - } - ctx.chartSeen[dk] = ctx.chartGen; - ctx.chartScore[dk] = static_cast(best); - return best; - } - - const int ni = i + st.niShift; - const int nj = j - (st.consumesRight ? 1 : 0); - // Interior-bound and emit-failure NEG_INFs are cheap to recompute; skip - // memoization to keep the sparse map bounded by actually-scored cells. - if (ni > nj + 1) { - return NEG_INF; - } - const double e = cmStateEmitScoreFast(st, *ctx.seqCode, i, j); - if (e == NEG_INF) { - return NEG_INF; - } - - double best = NEG_INF; - if (st.trCount == 1) { - const double nxt = exactVitRec(ctx, static_cast(st.trDst4[0]), ni, nj); - if (nxt != NEG_INF) { - best = e + st.trSc4[0] + nxt; - } - } else if (st.trCount == 2) { - const double nxt0 = exactVitRec(ctx, static_cast(st.trDst4[0]), ni, nj); - if (nxt0 != NEG_INF) best = std::max(best, e + st.trSc4[0] + nxt0); - const double nxt1 = exactVitRec(ctx, static_cast(st.trDst4[1]), ni, nj); - if (nxt1 != NEG_INF) best = std::max(best, e + st.trSc4[1] + nxt1); - } else if (st.trCount == 4) { - const double nxt0 = exactVitRec(ctx, static_cast(st.trDst4[0]), ni, nj); - if (nxt0 != NEG_INF) best = std::max(best, e + st.trSc4[0] + nxt0); - const double nxt1 = exactVitRec(ctx, static_cast(st.trDst4[1]), ni, nj); - if (nxt1 != NEG_INF) best = std::max(best, e + st.trSc4[1] + nxt1); - const double nxt2 = exactVitRec(ctx, static_cast(st.trDst4[2]), ni, nj); - if (nxt2 != NEG_INF) best = std::max(best, e + st.trSc4[2] + nxt2); - const double nxt3 = exactVitRec(ctx, static_cast(st.trDst4[3]), ni, nj); - if (nxt3 != NEG_INF) best = std::max(best, e + st.trSc4[3] + nxt3); - } else { - for (int t = 0; t < st.trCount; ++t) { - const size_t ti = st.trOff + static_cast(t); - const int y = ctx.trDst[ti]; - const double nxt = exactVitRec(ctx, y, ni, nj); - if (nxt == NEG_INF) { - continue; - } - best = std::max(best, e + ctx.trSc[ti] + nxt); - } - } - ctx.chartSeen[dk] = ctx.chartGen; - ctx.chartScore[dk] = static_cast(best); - return best; -} - -static void exactTraceRec(ExactRecCtx &ctx, - int v, - int i, - int j, - int &minUsed, - int &maxUsed, - int obsCount[4], - double modelAggRaw[4], - std::string &traceOps, - std::vector &traceStates) { - const double best = exactVitRec(ctx, v, i, j); - if (best == NEG_INF) { - return; - } - const ExactStateExec &st = ctx.execData[static_cast(v)]; - if (st.type == CM_ST_E) { - return; - } - traceStates.push_back(v); - if (st.type == CM_ST_B) { - const int y = st.bLeft; - const int z = st.bRight; - int kBeg = i - 1; - int kEnd = j; - const int d = j - i + 1; - if (st.splitKMax >= st.splitKMin) { - kBeg = std::max(kBeg, i + st.splitKMin - 1); - kEnd = std::min(kEnd, i + st.splitKMax - 1); - } - if (st.splitRMax >= st.splitRMin) { - kBeg = std::max(kBeg, i + (d - st.splitRMax) - 1); - kEnd = std::min(kEnd, i + (d - st.splitRMin) - 1); - } - // Always pick argmax over the enumerated split range. Earlier code - // tried a tolerance-match early-exit on `best - cand` < 1e-6, but - // cand is recomputed from float-rounded chart cells while best is the - // float chart value cast to double; on ties or near-ties the early - // match could pick a non-optimal k. - int bestK = -1; - double bestApprox = NEG_INF; - for (int k = kBeg; k <= kEnd; ++k) { - const double left = exactVitRec(ctx, y, i, k); - const double right = exactVitRec(ctx, z, k + 1, j); - if (left == NEG_INF || right == NEG_INF) { - continue; - } - const double cand = left + right; - if (cand > bestApprox) { - bestApprox = cand; - bestK = k; - } - } - if (bestK >= 0) { - exactTraceRec(ctx, y, i, bestK, minUsed, maxUsed, obsCount, modelAggRaw, traceOps, traceStates); - exactTraceRec(ctx, z, bestK + 1, j, minUsed, maxUsed, obsCount, modelAggRaw, traceOps, traceStates); - } - return; - } - - const int ni = i + st.niShift; - const int nj = j - (st.consumesRight ? 1 : 0); - const char op = traceOpForState(st.type); - if (op != '\0') { - for (int x = 0; x < st.dConsume; ++x) { - traceOps.push_back(op); - } - } - if (st.consumesLeft) { - minUsed = std::min(minUsed, i); - maxUsed = std::max(maxUsed, i); - const int bi = (*ctx.seqCode)[static_cast(i)]; - if (bi >= 0 && bi < 4) { - obsCount[bi]++; - } - } - if (st.consumesRight) { - minUsed = std::min(minUsed, j); - maxUsed = std::max(maxUsed, j); - const int bj = (*ctx.seqCode)[static_cast(j)]; - if (bj >= 0 && bj < 4) { - obsCount[bj]++; - } - } - modelAggRaw[0] += st.null2Agg[0]; - modelAggRaw[1] += st.null2Agg[1]; - modelAggRaw[2] += st.null2Agg[2]; - modelAggRaw[3] += st.null2Agg[3]; - const double e = cmStateEmitScoreFast(st, *ctx.seqCode, i, j); - // Pick argmax over all children. The previous tolerance-match early-exit - // could short-circuit on a non-optimal child when float chart rounding - // pushed cand within 1e-6 of best for a sub-optimal transition. Always - // enumerating is essentially free here — trace runs once per hit while - // fill runs O(M*N²) per target. - int bestY = -1; - double bestApprox = NEG_INF; - const int trCount = st.trCount; - if (trCount <= 4) { - for (int t = 0; t < trCount; ++t) { - const int y = static_cast(st.trDst4[t]); - const double nxt = exactVitRec(ctx, y, ni, nj); - if (nxt == NEG_INF) { - continue; - } - const double cand = e + st.trSc4[t] + nxt; - if (cand > bestApprox) { - bestApprox = cand; - bestY = y; - } - } - } else { - for (int t = 0; t < trCount; ++t) { - const size_t ti = st.trOff + static_cast(t); - const int y = ctx.trDst[ti]; - const double nxt = exactVitRec(ctx, y, ni, nj); - if (nxt == NEG_INF) { - continue; - } - const double cand = e + ctx.trSc[ti] + nxt; - if (cand > bestApprox) { - bestApprox = cand; - bestY = y; - } - } - } - (void)best; // best is no longer needed; argmax over enumerated cands suffices. - if (bestY >= 0) { - exactTraceRec(ctx, bestY, ni, nj, minUsed, maxUsed, obsCount, modelAggRaw, traceOps, traceStates); - } -} - -static void runInfernalExactScan(const InfernalExactModel &model, - const std::string &seq, - bool wantInside, - std::vector &outHits, - const std::string &seqId, - int maxHitLen = 0, - int forcedI = -1, - int forcedD = -1, - int anchorI = -1, - int anchorD = -1, - int forceDisableQdb = -1) { - const int N = static_cast(seq.size()); - const int M = static_cast(model.states.size()); - if (N <= 0 || M <= 0) { - return; - } - if (M > static_cast(std::numeric_limits::max())) { - Debug(Debug::ERROR) << "CM state count exceeds uint16_t capacity: " << M << "\n"; - return; - } - // Backstop: after fixing NEG_INF memoization, the map grows only with - // cells that return a finite score. For very large M*N^2 we still bail - // out to prevent pathological runtime. - { - const uint64_t workEstimate = static_cast(M) - * static_cast(N) - * static_cast(N); - static constexpr uint64_t CM_EXACT_MAX_WORK = 200000000000ULL; // 2e11 cell-ops - if (workEstimate > CM_EXACT_MAX_WORK) { - Debug(Debug::WARNING) << "cmscan: skipping scan for M=" << M - << " N=" << N << " (work " - << workEstimate << " exceeds cap)\n"; - return; - } - } - const bool fastLogsum = cmFastLogsumEnabled(); - const std::string modelConsensus = buildConsensusFromExactModel(model); - thread_local ExactScanWorkspace ws; - ws.seqCode.resize(static_cast(N + 1)); - std::fill(ws.seqCode.begin(), ws.seqCode.end(), static_cast(-1)); // 1-based - for (int p = 1; p <= N; ++p) { - ws.seqCode[static_cast(p)] = static_cast(baseToIdx(seq[static_cast(p - 1)])); - } - ws.prefA.assign(static_cast(N + 1), 0); - ws.prefC.assign(static_cast(N + 1), 0); - ws.prefG.assign(static_cast(N + 1), 0); - ws.prefU.assign(static_cast(N + 1), 0); - std::vector &prefA = ws.prefA; - std::vector &prefC = ws.prefC; - std::vector &prefG = ws.prefG; - std::vector &prefU = ws.prefU; - for (int p = 1; p <= N; ++p) { - prefA[static_cast(p)] = prefA[static_cast(p - 1)]; - prefC[static_cast(p)] = prefC[static_cast(p - 1)]; - prefG[static_cast(p)] = prefG[static_cast(p - 1)]; - prefU[static_cast(p)] = prefU[static_cast(p - 1)]; - const int bi = ws.seqCode[static_cast(p)]; - if (bi == 0) prefA[static_cast(p)]++; - else if (bi == 1) prefC[static_cast(p)]++; - else if (bi == 2) prefG[static_cast(p)]++; - else if (bi == 3) prefU[static_cast(p)]++; - } - const size_t needLog2 = static_cast(N + 1); - const size_t oldLog2 = ws.log2Int.size(); - if (oldLog2 < needLog2) { - ws.log2Int.resize(needLog2, NEG_INF); - int start = static_cast(oldLog2); - if (start < 1) { - start = 1; - } - for (int x = start; x <= N; ++x) { - ws.log2Int[static_cast(x)] = std::log2(static_cast(x)); - } - } - const std::vector &log2Int = ws.log2Int; - const double log2Omega3 = (model.null3Omega > 0.0) ? std::log2(model.null3Omega) : NEG_INF; - const double log2Null[4] = { - (model.nullProb[0] > 0.0) ? std::log2(model.nullProb[0]) : NEG_INF, - (model.nullProb[1] > 0.0) ? std::log2(model.nullProb[1]) : NEG_INF, - (model.nullProb[2] > 0.0) ? std::log2(model.nullProb[2]) : NEG_INF, - (model.nullProb[3] > 0.0) ? std::log2(model.nullProb[3]) : NEG_INF - }; - const bool null3Enabled = cmNull3Enabled(); - auto null3CorrByInterval = [&](int i, int j) -> double { - if (!null3Enabled) { - return 0.0; - } - const int cntA = prefA[static_cast(j)] - prefA[static_cast(i - 1)]; - const int cntC = prefC[static_cast(j)] - prefC[static_cast(i - 1)]; - const int cntG = prefG[static_cast(j)] - prefG[static_cast(i - 1)]; - const int cntU = prefU[static_cast(j)] - prefU[static_cast(i - 1)]; - const int known = cntA + cntC + cntG + cntU; - if (known <= 0 || !model.hasNull || !std::isfinite(log2Omega3)) { - return 0.0; - } - const double log2Len = log2Int[static_cast(known)]; - double score = log2Omega3; - if (cntA > 0 && std::isfinite(log2Null[0])) score += static_cast(cntA) * (log2Int[static_cast(cntA)] - log2Len - log2Null[0]); - if (cntC > 0 && std::isfinite(log2Null[1])) score += static_cast(cntC) * (log2Int[static_cast(cntC)] - log2Len - log2Null[1]); - if (cntG > 0 && std::isfinite(log2Null[2])) score += static_cast(cntG) * (log2Int[static_cast(cntG)] - log2Len - log2Null[2]); - if (cntU > 0 && std::isfinite(log2Null[3])) score += static_cast(cntU) * (log2Int[static_cast(cntU)] - log2Len - log2Null[3]); - return log2SumExp2(0.0, score); - }; - std::vector exec(static_cast(M)); - std::vector trDst; - std::vector trSc; - trDst.reserve(static_cast(M) * 4); - trSc.reserve(static_cast(M) * 4); - std::vector isBState(static_cast(M), 0); - std::vector nbNiShift(static_cast(M), 0); - std::vector nbDConsume(static_cast(M), 0); - std::vector nbEmitSize(static_cast(M), 0); - std::vector nbTrCount(static_cast(M), 0); - std::vector nbDmin(static_cast(M), 0); - std::vector nbDmax(static_cast(M), -1); - std::vector nbTrOff(static_cast(M), 0); - std::vector nbEmitPtr(static_cast(M), NULL); - std::vector nbConsumeMask(static_cast(M), 0); - std::vector nbTrDst4(static_cast(M) * 4, 0); - std::vector nbTrScF4(static_cast(M) * 4, -std::numeric_limits::infinity()); - // MMSEQS_CMSCAN_DISABLE_QDB=1 neutralizes per-state QDB1 bands so CYK fills the - // full d-range, mirroring Infernal cmalign's default (HMM-banded but not QDB-banded). - // forceDisableQdb param overrides env var for per-call gating (used by envelope rescan - // to run pass 1 with QDB on and pass 2 with QDB off). - bool disableQdb; - if (forceDisableQdb >= 0) { - disableQdb = (forceDisableQdb != 0); - } else { - const char *envDisableQdb = std::getenv("MMSEQS_CMSCAN_DISABLE_QDB"); - disableQdb = (envDisableQdb != NULL && envDisableQdb[0] == '1'); - } - // MMSEQS_CMSCAN_DROP_DMIN=1 zeros per-state dmin while preserving dmax. Targets the - // FG=1 collapse on short-envelope queries (e.g. 4LCK_3) where forced d=N falls below - // dmin at the root, without inviting qdboff's unbounded-trace regressions on large queries. - const char *envDropDmin = std::getenv("MMSEQS_CMSCAN_DROP_DMIN"); - const bool dropDmin = (envDropDmin != NULL && envDropDmin[0] == '1'); - // MMSEQS_CMSCAN_DROP_DMAX=1: zeros dmin AND removes dmax cap (sentinel -1) so no - // upper d-bound at any state, but still ranges from 0. Mirrors qdboff's band off. - const char *envDropDmax = std::getenv("MMSEQS_CMSCAN_DROP_DMAX"); - const bool dropDmax = (envDropDmax != NULL && envDropDmax[0] == '1'); - for (int v = 0; v < M; ++v) { - const CmState &st = model.states[static_cast(v)]; - ExactStateExec e; - e.type = st.type; - e.niShift = 0; - e.dConsume = 0; - e.consumesLeft = false; - e.consumesRight = false; - e.bLeft = st.cfirst; - e.bRight = st.cnum; - e.trDst4[0] = 0; e.trDst4[1] = 0; e.trDst4[2] = 0; e.trDst4[3] = 0; - e.trScF4[0] = -std::numeric_limits::infinity(); - e.trScF4[1] = -std::numeric_limits::infinity(); - e.trScF4[2] = -std::numeric_limits::infinity(); - e.trScF4[3] = -std::numeric_limits::infinity(); - e.trSc4[0] = NEG_INF; e.trSc4[1] = NEG_INF; e.trSc4[2] = NEG_INF; e.trSc4[3] = NEG_INF; - e.trOff = trDst.size(); - e.trCount = 0; - e.emitPtr = st.emitF.empty() ? NULL : &st.emitF[0]; - e.emitSize = static_cast(st.emit.size()); - e.splitKMin = 0; - e.splitKMax = -1; - e.splitRMin = 0; - e.splitRMax = -1; - e.dmin = (disableQdb || dropDmin || dropDmax) ? 0 : st.dmin1; - e.dmax = (disableQdb || dropDmax) ? -1 : st.dmax1; - e.null2Agg[0] = 0.0; - e.null2Agg[1] = 0.0; - e.null2Agg[2] = 0.0; - e.null2Agg[3] = 0.0; - if (st.type == CM_ST_MP) { - e.niShift = 1; - e.dConsume = 2; - e.consumesLeft = true; - e.consumesRight = true; - } else if (st.type == CM_ST_ML || st.type == CM_ST_IL) { - e.niShift = 1; - e.dConsume = 1; - e.consumesLeft = true; - } else if (st.type == CM_ST_MR || st.type == CM_ST_IR) { - e.dConsume = 1; - e.consumesRight = true; - } - if (st.type == CM_ST_MP && e.emitPtr != NULL && e.emitSize >= 16) { - for (int a = 0; a < 4; ++a) { - double row = 0.0; - double col = 0.0; - for (int b = 0; b < 4; ++b) { - row += emitBitToProbPair(e.emitPtr[a * 4 + b], model, a, b); - col += emitBitToProbPair(e.emitPtr[b * 4 + a], model, b, a); - } - e.null2Agg[a] = row + col; - } - } else if ((st.type == CM_ST_ML || st.type == CM_ST_MR || st.type == CM_ST_IL || st.type == CM_ST_IR) && - e.emitPtr != NULL && e.emitSize >= 4) { - for (int a = 0; a < 4; ++a) { - e.null2Agg[a] = emitBitToProbSingle(e.emitPtr[a], model, a); - } - } - if (st.type == CM_ST_B) { - isBState[static_cast(v)] = 1; - if (!disableQdb && !dropDmax && e.bLeft >= 0 && e.bLeft < M) { - const CmState &l = model.states[static_cast(e.bLeft)]; - if (l.dmin1 >= 0 && l.dmax1 >= l.dmin1) { - e.splitKMin = dropDmin ? 0 : l.dmin1; - e.splitKMax = l.dmax1; - } - } - if (!disableQdb && !dropDmax && e.bRight >= 0 && e.bRight < M) { - const CmState &r = model.states[static_cast(e.bRight)]; - if (r.dmin1 >= 0 && r.dmax1 >= r.dmin1) { - e.splitRMin = dropDmin ? 0 : r.dmin1; - e.splitRMax = r.dmax1; - } - } - } else if (st.type != CM_ST_E) { - const int tn = std::min(st.cnum, static_cast(st.trans.size())); - for (int x = 0; x < tn; ++x) { - const int y = st.cfirst + x; - if (y < 0 || y >= M) { - continue; - } - if (e.trCount < 4) { - const size_t t = static_cast(e.trCount); - e.trDst4[t] = static_cast(y); - e.trSc4[t] = st.trans[static_cast(x)]; - e.trScF4[t] = static_cast(st.trans[static_cast(x)]); - } - trDst.push_back(static_cast(y)); - trSc.push_back(st.trans[static_cast(x)]); - ++e.trCount; - } - } - exec[static_cast(v)] = e; - if (st.type != CM_ST_B && st.type != CM_ST_E) { - const size_t vi = static_cast(v); - nbNiShift[vi] = e.niShift; - nbDConsume[vi] = e.dConsume; - nbEmitSize[vi] = e.emitSize; - nbTrCount[vi] = e.trCount; - nbDmin[vi] = e.dmin; - nbDmax[vi] = e.dmax; - nbTrOff[vi] = e.trOff; - nbEmitPtr[vi] = e.emitPtr; - uint8_t mask = 0; - if (e.consumesLeft) { - mask |= 1u; - } - if (e.consumesRight) { - mask |= 2u; - } - nbConsumeMask[vi] = mask; - const size_t t4 = vi * 4; - nbTrDst4[t4 + 0] = e.trDst4[0]; - nbTrDst4[t4 + 1] = e.trDst4[1]; - nbTrDst4[t4 + 2] = e.trDst4[2]; - nbTrDst4[t4 + 3] = e.trDst4[3]; - nbTrScF4[t4 + 0] = e.trScF4[0]; - nbTrScF4[t4 + 1] = e.trScF4[1]; - nbTrScF4[t4 + 2] = e.trScF4[2]; - nbTrScF4[t4 + 3] = e.trScF4[3]; - } - } - ws.trScF.resize(trSc.size()); - for (size_t k = 0; k < trSc.size(); ++k) { ws.trScF[k] = static_cast(trSc[k]); } - std::vector activeStates; - activeStates.reserve(static_cast(M)); - for (int v = M - 1; v >= 0; --v) { - if (exec[static_cast(v)].type != CM_ST_E) { - activeStates.push_back(v); - } - } - // Fast path: iterative deck-based CYK over full sequence. - // Inside score (if requested) is computed only for the selected best interval. - if (cmExactFastDeckEnabled() && N <= 1024) { - const float NEG_INF_F = -std::numeric_limits::infinity(); - const size_t iStride = static_cast(N + 2); - - int maxSpan = N; - static int wcapMode = -1; - if (wcapMode == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_WCAP"); - wcapMode = (env != NULL && std::string(env) == "0") ? 0 : 1; - } - if (wcapMode == 1 && model.w > 0) { - maxSpan = std::min(maxSpan, model.w); - } - if (maxHitLen > 0) { - maxSpan = std::min(maxSpan, maxHitLen); - } - const bool forceEnv = (forcedI > 0 && forcedD > 0 && - forcedD <= N && forcedI <= N - forcedD + 1); - if (forceEnv) { - maxSpan = forcedD; - } - - if (!ws.stateBase.ensure(static_cast(M))) { - Debug(Debug::ERROR) << "cmscan: failed to allocate stateBase buffer\n"; - return; - } - std::vector &fastBandDmin = ws.exactChartBandDmin; - std::vector &fastBandSize = ws.exactChartBandSize; - fastBandDmin.assign(static_cast(M), 0); - fastBandSize.assign(static_cast(M), 0); - size_t *stateBase = ws.stateBase.data(); - size_t cells = 0; - for (int v = 0; v < M; ++v) { - const ExactStateExec &st = exec[static_cast(v)]; - int dlo = 0; - int dhi = maxSpan; - if (st.type == CM_ST_E) { - dlo = 0; - dhi = 0; - } else if (st.dmax >= st.dmin && st.dmin >= 0) { - dlo = std::max(0, st.dmin); - dhi = std::min(maxSpan, st.dmax); - } - stateBase[static_cast(v)] = cells; - if (dhi >= dlo) { - fastBandDmin[static_cast(v)] = dlo; - fastBandSize[static_cast(v)] = dhi - dlo + 1; - cells += static_cast(fastBandSize[static_cast(v)]) * iStride; - } - } - const size_t vdStride = static_cast(maxSpan + 1); - const size_t vdCells = static_cast(M) * vdStride; - const size_t fastDeckBytes = rawBufferBytesFor(cells, sizeof(float)) - + rawBufferBytesFor(static_cast(M), sizeof(size_t)) - + 2 * rawBufferBytesFor(vdCells, sizeof(int)) - + rawBufferBytesFor(static_cast(N + 1), sizeof(float)) - + rawBufferBytesFor(iStride, sizeof(float)); - FastDeckMemoryGate fastDeckGate(fastDeckBytes); - FastDeckBufferRelease fastDeckRelease(ws, fastDeckGate.active()); - if (!ws.vit.ensure(cells)) { - Debug(Debug::ERROR) << "cmscan: failed to allocate vit buffer\n"; - return; - } - if (!ws.negRow.ensure(iStride)) { - Debug(Debug::ERROR) << "cmscan: failed to allocate negRow buffer\n"; - return; - } - if (!ws.bSplitTmp.ensure(static_cast(N + 1))) { - Debug(Debug::ERROR) << "cmscan: failed to allocate bSplitTmp buffer\n"; - return; - } - float *vit = ws.vit.data(); - std::fill(vit, vit + cells, NEG_INF_F); - std::fill(ws.negRow.data(), ws.negRow.data() + iStride, NEG_INF_F); - auto rowPtr = [&](int v, int d) -> float * { - if (v < 0 || v >= M) return nullptr; - const size_t vi = static_cast(v); - if (d < fastBandDmin[vi] || d >= fastBandDmin[vi] + fastBandSize[vi]) { - return nullptr; - } - return vit + stateBase[vi] + static_cast(d - fastBandDmin[vi]) * iStride; - }; - auto rowPtrConst = [&](int v, int d) -> const float * { - float *row = rowPtr(v, d); - return (row != nullptr) ? row : ws.negRow.data(); - }; - auto cellVal = [&](int v, int d, int i) -> float { - const float *row = rowPtrConst(v, d); - return row[static_cast(i)]; - }; - - if (!ws.bSplitBegByVD.ensure(vdCells)) { - Debug(Debug::ERROR) << "cmscan: failed to allocate bSplitBeg buffer\n"; - return; - } - if (!ws.bSplitEndByVD.ensure(vdCells)) { - Debug(Debug::ERROR) << "cmscan: failed to allocate bSplitEnd buffer\n"; - return; - } - int *bSplitBegByVD = ws.bSplitBegByVD.data(); - int *bSplitEndByVD = ws.bSplitEndByVD.data(); - for (size_t ii = 0; ii < vdCells; ++ii) { - bSplitBegByVD[ii] = 0; - bSplitEndByVD[ii] = -1; - } - const ExactStateExec *execData = exec.data(); - for (int v = 0; v < M; ++v) { - const ExactStateExec &st = exec[static_cast(v)]; - if (st.type != CM_ST_B) { - continue; - } - for (int d = 0; d <= maxSpan; ++d) { - int kBeg = 0; - int kEnd = d; - if (st.splitKMax >= st.splitKMin) { - kBeg = std::max(kBeg, st.splitKMin); - kEnd = std::min(kEnd, st.splitKMax); - } - if (st.splitRMax >= st.splitRMin) { - kBeg = std::max(kBeg, d - st.splitRMax); - kEnd = std::min(kEnd, d - st.splitRMin); - } - const size_t vd = static_cast(v) * vdStride + static_cast(d); - bSplitBegByVD[vd] = kBeg; - bSplitEndByVD[vd] = kEnd; - } - } - - // E states emit empty only. - for (int v = 0; v < M; ++v) { - if (exec[static_cast(v)].type != CM_ST_E) { - continue; - } - const size_t vb = stateBase[static_cast(v)]; - for (int i = 1; i <= N + 1; ++i) { - vit[vb + static_cast(i)] = 0.0f; - } - } - - for (int d = 0; d <= maxSpan; ++d) { - const int iMax = (d == 0) ? (N + 1) : (N - d + 1); - for (size_t vsi = 0; vsi < activeStates.size(); ++vsi) { - const int v = activeStates[vsi]; - const size_t vi = static_cast(v); - const ExactStateExec &st = execData[vi]; - float *curRow = rowPtr(v, d); - if (curRow == nullptr) { - continue; - } - const int dmin = isBState[vi] ? st.dmin : nbDmin[vi]; - const int dmax = isBState[vi] ? st.dmax : nbDmax[vi]; - if (dmax >= dmin && (d < dmin || d > dmax)) { - // Out-of-band: already NEG_INF from the contiguous pre-fill. - continue; - } - if (isBState[vi]) { - const int y = st.bLeft; - const int z = st.bRight; - if (y < 0 || y >= M || z < 0 || z >= M) { - continue; - } - const size_t vd = static_cast(v) * vdStride + static_cast(d); - const int kBeg = bSplitBegByVD[vd]; - const int kEnd = bSplitEndByVD[vd]; - float *const bOut = curRow + 1; - if (kBeg > kEnd) { - std::fill(bOut, bOut + iMax, NEG_INF_F); - } else { - for (int i = 1; i <= iMax; ++i) { - float bBest = NEG_INF_F; - for (int k = kBeg; k <= kEnd; ++k) { - const float l = rowPtrConst(y, k)[static_cast(i)]; - const float r = rowPtrConst(z, d - k)[static_cast(i + k)]; - const float cand = l + r; - bBest = std::fmaxf(bBest, cand); - } - bOut[i - 1] = bBest; - } - } - continue; - } - - const int dConsume = nbDConsume[vi]; - if (d < dConsume) { - continue; - } - const int nd = d - dConsume; - const int niShift = nbNiShift[vi]; - const float *ep = nbEmitPtr[vi]; - const int emitSize = nbEmitSize[vi]; - const int trCount = nbTrCount[vi]; - const size_t trOff = nbTrOff[vi]; - const uint8_t consumeMask = nbConsumeMask[vi]; - const size_t t4 = vi * 4; - const int8_t *sc = ws.seqCode.data(); - const float *trScFPtr = ws.trScF.data(); - const StateId *trDstPtr = trDst.data(); - const float trSc0 = (trCount >= 1) ? nbTrScF4[t4 + 0] : 0.f; - const float trSc1 = (trCount >= 2) ? nbTrScF4[t4 + 1] : 0.f; - const float trSc2 = (trCount >= 3) ? nbTrScF4[t4 + 2] : 0.f; - const float trSc3 = (trCount >= 4) ? nbTrScF4[t4 + 3] : 0.f; - // Build 5-entry emit lookup (indexed by sc+1, sc in {-1,0,1,2,3}) - // NEG_INF_F propagates naturally through IEEE float arithmetic - float efT[5]; - if (consumeMask == 0u) { - efT[0] = efT[1] = efT[2] = efT[3] = efT[4] = 0.0f; - } else if (consumeMask == 1u || consumeMask == 2u) { - efT[0] = -1.0f; - if (ep && emitSize >= 4) { - efT[1] = ep[0]; efT[2] = ep[1]; efT[3] = ep[2]; efT[4] = ep[3]; - } else { - efT[1] = efT[2] = efT[3] = efT[4] = NEG_INF_F; - } - } else { - efT[0] = efT[1] = efT[2] = efT[3] = efT[4] = 0.0f; // pair: handled inline - } - // Contiguous output and source pointers (new layout: ragged d-band rows by state) - float *const outPtr = curRow + 1; - const float *const src0 = (trCount >= 1) ? rowPtrConst(static_cast(nbTrDst4[t4 + 0]), nd) + 1 + static_cast(niShift) : nullptr; - const float *const src1 = (trCount >= 2) ? rowPtrConst(static_cast(nbTrDst4[t4 + 1]), nd) + 1 + static_cast(niShift) : nullptr; - const float *const src2 = (trCount >= 3) ? rowPtrConst(static_cast(nbTrDst4[t4 + 2]), nd) + 1 + static_cast(niShift) : nullptr; - const float *const src3 = (trCount >= 4) ? rowPtrConst(static_cast(nbTrDst4[t4 + 3]), nd) + 1 + static_cast(niShift) : nullptr; - // sc[i] offset: scL[ii] = sc[ii+1] = sc[i]; scR[ii] = sc[ii+d] = sc[i+d-1] - const int8_t *const scL = sc + 1; - const int8_t *const scR = sc + d; -#if defined(CM_HAS_SSE2) - if ((trCount == 1 || trCount == 2 || trCount == 4) && consumeMask != 3u) { - const __m128 trSc0V = _mm_set1_ps(trSc0); - const __m128 trSc1V = (trCount >= 2) ? _mm_set1_ps(trSc1) : _mm_set1_ps(NEG_INF_F); - const __m128 trSc2V = (trCount >= 3) ? _mm_set1_ps(trSc2) : _mm_set1_ps(NEG_INF_F); - const __m128 trSc3V = (trCount >= 4) ? _mm_set1_ps(trSc3) : _mm_set1_ps(NEG_INF_F); - int ii = 0; - for (; ii + 4 <= iMax; ii += 4) { - __m128 ef_v; - if (consumeMask == 0u) { - ef_v = _mm_setzero_ps(); - } else if (consumeMask == 1u) { - ef_v = _mm_set_ps(efT[scL[ii+3]+1], efT[scL[ii+2]+1], - efT[scL[ii+1]+1], efT[scL[ii+0]+1]); - } else { // consumeMask == 2u - ef_v = _mm_set_ps(efT[scR[ii+3]+1], efT[scR[ii+2]+1], - efT[scR[ii+1]+1], efT[scR[ii+0]+1]); - } - __m128 result; - if (trCount == 1) { - result = _mm_add_ps(_mm_add_ps(ef_v, trSc0V), _mm_loadu_ps(src0 + ii)); - } else if (trCount == 2) { - const __m128 c0 = _mm_add_ps(_mm_add_ps(ef_v, trSc0V), _mm_loadu_ps(src0 + ii)); - const __m128 c1 = _mm_add_ps(_mm_add_ps(ef_v, trSc1V), _mm_loadu_ps(src1 + ii)); - result = _mm_max_ps(c0, c1); - } else { // trCount == 4 - const __m128 c0 = _mm_add_ps(_mm_add_ps(ef_v, trSc0V), _mm_loadu_ps(src0 + ii)); - const __m128 c1 = _mm_add_ps(_mm_add_ps(ef_v, trSc1V), _mm_loadu_ps(src1 + ii)); - const __m128 c2 = _mm_add_ps(_mm_add_ps(ef_v, trSc2V), _mm_loadu_ps(src2 + ii)); - const __m128 c3 = _mm_add_ps(_mm_add_ps(ef_v, trSc3V), _mm_loadu_ps(src3 + ii)); - result = _mm_max_ps(_mm_max_ps(c0, c1), _mm_max_ps(c2, c3)); - } - _mm_storeu_ps(outPtr + ii, result); - } - // Scalar remainder - for (; ii < iMax; ++ii) { - const float ef = (consumeMask == 0u) ? 0.0f - : (consumeMask == 1u) ? efT[scL[ii]+1] - : efT[scR[ii]+1]; - float best = NEG_INF_F; - if (trCount >= 1) { const float c = ef + trSc0 + src0[ii]; best = std::fmaxf(best, c); } - if (trCount >= 2) { const float c = ef + trSc1 + src1[ii]; best = std::fmaxf(best, c); } - if (trCount >= 3) { const float c = ef + trSc2 + src2[ii]; best = std::fmaxf(best, c); } - if (trCount >= 4) { const float c = ef + trSc3 + src3[ii]; best = std::fmaxf(best, c); } - outPtr[ii] = best; - } - } else -#endif - { - // General scalar path (consumeMask==3 pair emit, or trCount==3/trCount>4) - // Precompute base pointers for all transitions (loop-invariant over i) - const float *trSrcPtrs[16]; - float trScVals[16]; - const int trCountClamped = (trCount <= 16) ? trCount : 16; - for (int t = 0; t < trCountClamped; ++t) { - const size_t ti = trOff + static_cast(t); - trSrcPtrs[t] = rowPtrConst(static_cast(trDstPtr[ti]), nd) - + static_cast(niShift); - trScVals[t] = trScFPtr[ti]; - } - - if (trCount == 1) { - for (int ii = 0; ii < iMax; ++ii) { - const int i = ii + 1; - float ef; - if (consumeMask == 3u) { - if (!(ep && emitSize >= 16)) { ef = NEG_INF_F; } - else { const int li = sc[i]; const int ri = sc[i + d - 1]; ef = (li >= 0 && ri >= 0) ? ep[li * 4 + ri] : -1.0f; } - } else if (consumeMask == 0u) { ef = 0.0f; } - else if (consumeMask == 1u) { ef = efT[scL[ii]+1]; } - else { ef = efT[scR[ii]+1]; } - outPtr[ii] = ef + trSc0 + src0[ii]; - } - } else if (trCount == 2) { - for (int ii = 0; ii < iMax; ++ii) { - const int i = ii + 1; - float ef; - if (consumeMask == 3u) { - if (!(ep && emitSize >= 16)) { ef = NEG_INF_F; } - else { const int li = sc[i]; const int ri = sc[i + d - 1]; ef = (li >= 0 && ri >= 0) ? ep[li * 4 + ri] : -1.0f; } - } else if (consumeMask == 0u) { ef = 0.0f; } - else if (consumeMask == 1u) { ef = efT[scL[ii]+1]; } - else { ef = efT[scR[ii]+1]; } - float best = ef + trSc0 + src0[ii]; - best = std::fmaxf(best, ef + trSc1 + src1[ii]); - outPtr[ii] = best; - } - } else if (trCount == 4) { - for (int ii = 0; ii < iMax; ++ii) { - const int i = ii + 1; - float ef; - if (consumeMask == 3u) { - if (!(ep && emitSize >= 16)) { ef = NEG_INF_F; } - else { const int li = sc[i]; const int ri = sc[i + d - 1]; ef = (li >= 0 && ri >= 0) ? ep[li * 4 + ri] : -1.0f; } - } else if (consumeMask == 0u) { ef = 0.0f; } - else if (consumeMask == 1u) { ef = efT[scL[ii]+1]; } - else { ef = efT[scR[ii]+1]; } - float best = ef + trSc0 + src0[ii]; - best = std::fmaxf(best, ef + trSc1 + src1[ii]); - best = std::fmaxf(best, ef + trSc2 + src2[ii]); - best = std::fmaxf(best, ef + trSc3 + src3[ii]); - outPtr[ii] = best; - } - } else { - // Loop-swapped with precomputed emission scores - // Step 1: precompute emission into outPtr (reused as temp buffer) - if (consumeMask == 3u && ep && emitSize >= 16) { - for (int ii = 0; ii < iMax; ++ii) { - const int i = ii + 1; - const int li = sc[i]; - const int ri = sc[i + d - 1]; - outPtr[ii] = (li >= 0 && ri >= 0) ? ep[li * 4 + ri] : -1.0f; - } - } else if (consumeMask == 3u) { - std::fill(outPtr, outPtr + iMax, NEG_INF_F); - } else if (consumeMask == 0u) { - std::fill(outPtr, outPtr + iMax, 0.0f); - } else if (consumeMask == 1u) { - for (int ii = 0; ii < iMax; ++ii) - outPtr[ii] = efT[scL[ii]+1]; - } else { - for (int ii = 0; ii < iMax; ++ii) - outPtr[ii] = efT[scR[ii]+1]; - } - // Step 2: for each transition, stream through all i and accumulate - // First transition: bestBuf[ii] = outPtr[ii] + trSc + trSrc[i] - // Use bSplitTmp as temp best buffer - float *bestBuf = ws.bSplitTmp.data(); - const int VS = VECSIZE_FLOAT; - { - const float *tsrc = trSrcPtrs[0]; - const float tsc = trScVals[0]; - const simd_float vtsc = simdf32_set(tsc); - int ii = 0; - for (; ii + VS - 1 < iMax; ii += VS) { - simd_float vo = simdf32_loadu(outPtr + ii); - simd_float vs = simdf32_loadu(tsrc + ii + 1); - simdf32_storeu(bestBuf + ii, simdf32_add(simdf32_add(vo, vtsc), vs)); - } - for (; ii < iMax; ++ii) { - bestBuf[ii] = outPtr[ii] + tsc + tsrc[ii + 1]; - } - } - // Remaining transitions: bestBuf = max(bestBuf, outPtr + tsc + tsrc[i+1]) - for (int t = 1; t < trCountClamped; ++t) { - const float *tsrc = trSrcPtrs[t]; - const float tsc = trScVals[t]; - const simd_float vtsc = simdf32_set(tsc); - int ii = 0; - for (; ii + VS - 1 < iMax; ii += VS) { - simd_float vo = simdf32_loadu(outPtr + ii); - simd_float vs = simdf32_loadu(tsrc + ii + 1); - simd_float vb = simdf32_loadu(bestBuf + ii); - simd_float cand = simdf32_add(simdf32_add(vo, vtsc), vs); - simdf32_storeu(bestBuf + ii, simdf32_max(vb, cand)); - } - for (; ii < iMax; ++ii) { - bestBuf[ii] = std::fmaxf(bestBuf[ii], outPtr[ii] + tsc + tsrc[ii + 1]); - } - } - // Copy result back - std::copy(bestBuf, bestBuf + iMax, outPtr); - } - } - - // EL pre-fill fusion. Mirrors Infernal cm_dpsearch.c:3389-3398: - // for any end-eligible state v, alpha[v][d][i] must reflect - // max(regular recurrence, emit_v(i,d) + el_scA[d-sd] + endsc[v]). - // Doing this INSIDE the (d,v) fill (not as a post-pass) is what - // lets parents w of v reading alpha[v] at later d-iterations see - // the EL-augmented value — so EL contribution propagates up the - // CM tree, which the prior post-pass placement could never do. - if (model.hasLocalCfg && model.elSelf <= 0.0) { - const CmState &csEl = model.states[vi]; - if (csEl.endSc != NEG_INF) { - int sdEl = -1; - if (csEl.type == CM_ST_MP) sdEl = 2; - else if (csEl.type == CM_ST_ML || csEl.type == CM_ST_MR) sdEl = 1; - else if (csEl.type == CM_ST_S) sdEl = 0; - if (sdEl >= 0 && d >= sdEl) { - const float elContrib = static_cast(model.elSelf) * static_cast(d - sdEl) - + static_cast(csEl.endSc); - const int8_t *scEl = ws.seqCode.data(); - for (int ii = 0; ii < iMax; ++ii) { - const int i = ii + 1; - float ef; - if (consumeMask == 0u) { - ef = 0.0f; - } else if (consumeMask == 1u) { - const int li = scEl[i]; - if (li < 0) continue; - ef = (ep && emitSize >= 4) ? ep[li] : -1.0f; - } else if (consumeMask == 2u) { - const int ri = scEl[i + d - 1]; - if (ri < 0) continue; - ef = (ep && emitSize >= 4) ? ep[ri] : -1.0f; - } else { - const int li = scEl[i]; - const int ri = scEl[i + d - 1]; - if (li < 0 || ri < 0) continue; - ef = (ep && emitSize >= 16) ? ep[li * 4 + ri] : -1.0f; - } - const float cand = ef + elContrib; - if (outPtr[ii] < cand) outPtr[ii] = cand; - } - } - } - } - } - } - - const int root = model.rootState; - - // Local-end pickup (Infernal cm_dpsearch.c). After regular CYK fills - // alpha[v][d][i] for end-eligible head v, take a max with the local-end - // candidate: emit_v(i, d) + el_scA[d - sd_v] + endSc[v], where - // el_scA[k] = elSelf * k. Emission must be included because state v - // emits its M-state residue(s) before transitioning to EL. - if (model.hasLocalCfg && model.elSelf <= 0.0) { - const float elSelfF = static_cast(model.elSelf); - const int8_t *sc = ws.seqCode.data(); - for (int v = 0; v < M; ++v) { - const CmState &cs = model.states[static_cast(v)]; - if (cs.endSc == NEG_INF) continue; - const float endScF = static_cast(cs.endSc); - const size_t vi = static_cast(v); - const float *ep = nbEmitPtr[vi]; - const int emitSize = nbEmitSize[vi]; - const uint8_t consumeMask = nbConsumeMask[vi]; - int sd = 0; - if (cs.type == CM_ST_MP) sd = 2; - else if (cs.type == CM_ST_ML || cs.type == CM_ST_MR) sd = 1; - else if (cs.type == CM_ST_S) sd = 0; - else continue; // only MP/ML/MR/S are end-eligible heads - for (int d = sd; d <= maxSpan; ++d) { - float *row0 = rowPtr(v, d); - if (row0 == nullptr) continue; - const float elContrib = elSelfF * static_cast(d - sd) + endScF; - const int iMax = (d == 0) ? (N + 1) : (N - d + 1); - float *row = row0 + 1; - for (int ii = 0; ii < iMax; ++ii) { - const int i = ii + 1; - float ef; - if (consumeMask == 0u) { - ef = 0.0f; - } else if (consumeMask == 1u) { - const int li = sc[i]; - if (li < 0) continue; - ef = (ep && emitSize >= 4) ? ep[li] : -1.0f; - } else if (consumeMask == 2u) { - const int ri = sc[i + d - 1]; - if (ri < 0) continue; - ef = (ep && emitSize >= 4) ? ep[ri] : -1.0f; - } else { - const int li = sc[i]; - const int ri = sc[i + d - 1]; - if (li < 0 || ri < 0) continue; - ef = (ep && emitSize >= 16) ? ep[li * 4 + ri] : -1.0f; - } - const float cand = ef + elContrib; - if (row[ii] < cand) row[ii] = cand; - } - } - } - } - - // Infernal semantics evaluate all legal spans under state dmin/dmax/w constraints. - int minSpan = 1; - // Optional CLEN-relative envelope floor used by diagnostics/benchmarking. - { - static double dminFrac = -1.0; - if (dminFrac < 0.0) { - const char *env = std::getenv("MMSEQS_CMSCAN_DMIN_CLEN_FRAC"); - dminFrac = (env != NULL) ? std::atof(env) : 0.0; - if (dminFrac < 0.0 || dminFrac > 1.0) dminFrac = 0.0; - } - if (dminFrac > 0.0 && model.clen > 0) { - int floorD = static_cast(dminFrac * model.clen); - if (floorD > minSpan) minSpan = floorD; - if (minSpan > maxSpan) minSpan = maxSpan; - } - } - if (forceEnv) { - minSpan = forcedD; - } - - // Hoisted: MMSEQS_CMSCAN_FORCE_GLOBAL env-cache. Read once here so the - // local-begin pass below and the selection block further down both see - // the same value. Selection block uses this same `forceGlobal`. - static int forceGlobal = -1; - if (forceGlobal == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_FORCE_GLOBAL"); - if (env != NULL) { - if (std::string(env) == "1") forceGlobal = 1; - else if (std::string(env) == "2") forceGlobal = 2; - else if (std::string(env) == "3") forceGlobal = 3; - else if (std::string(env) == "4") forceGlobal = 4; - else forceGlobal = 0; - } else { - forceGlobal = 0; - } - } - - // Local-begin pass (Infernal cm_dpsearch.c:576-610). After all states - // are filled, the root cell can also be reached by jumping directly to - // any begin-eligible state y at score `alpha[y][d][i] + beginSc[y]`. - // - // FG=0/2/3/4: spray local-begin alternates across every (i, d) ROOT_S - // cell. Default mode picks argmax over the row, so the per-cell - // pickup is harmless (every cell is internally consistent with its - // own argmax y). FG=2 sweeps i at fixed d; FG=3/4 scan a band. - // - // FG=1: trace is forced to seed at (i=1, d=min(maxSpan,N)). Per-cell - // pickup pollutes other root cells with values whose argmax y has - // nothing to do with that corner — see project_4LCK_FG1_collapse_ - // decomposed.md and project_cyk_celldiff_root_local_begin.md (102 - // polluted cells). Apply local-begin only at the trace-seed corner, - // mirroring Inside DP at lines 3683-3691 (bit-exact to Infernal). - // - // Glocal CMs leave beginSc=NEG_INF and skip the loop body either way. - if (model.hasLocalCfg) { - if (forceGlobal == 1) { - // Corner-OVERWRITE: clear ROOT_S at the trace-seed corner - // before applying local-begin. Mirrors Infernal cm_localize - // (cm_modelconfig.c:491) which zeros ROOT_S→child transitions - // so the only path to ROOT_S is via local-begin. Differs from - // the per-cell max-update branch below: there we *add* - // local-begin alternates on top of the regular ROOT_S - // recurrence, which inflates the cell score above what the - // local-begin trace can actually produce. Under TRACE_LBO=1 + - // FG=1 the trace is forced to descend via local-begin, so - // the cell value must reflect the local-begin path only — - // otherwise score-vs-trace is inconsistent and bad alignments - // get scored as if they took the regular-recurrence path. - const int cornerD = std::min(maxSpan, N); - const int cornerI = 1; - if (cornerD >= 1 && cornerI + cornerD - 1 <= N) { - float *rootCorner = rowPtr(root, cornerD); - if (rootCorner != nullptr) { - float &rootCell = rootCorner[static_cast(cornerI)]; - rootCell = NEG_INF_F; - for (int y = 1; y < M; ++y) { - const CmState &cs = model.states[static_cast(y)]; - if (cs.beginSc == NEG_INF) continue; - const float bsc = static_cast(cs.beginSc); - const float yVal = cellVal(y, cornerD, cornerI); - if (yVal == NEG_INF_F) continue; - const float cand = yVal + bsc; - if (rootCell < cand) rootCell = cand; - } - } - } - } else if (forceGlobal == 5) { - // FG=5 hybrid: only the corner cell (FG=1 candidate) and the - // midpoint-span cells (FG=3 candidates) are read by the - // selection logic below. Skip the O(M*N²) per-cell loop and - // touch only those cells (O(M*span) work), yielding - // FG=1-class wall time even though we maintain both - // candidates. Both pickups are corner-OVERWRITE-style for - // consistency with FG=1. - const int cornerD = std::min(maxSpan, N); - if (cornerD >= 1 && cornerD <= N) { - float *rootCorner = rowPtr(root, cornerD); - if (rootCorner != nullptr) { - float &rootCell = rootCorner[1]; - rootCell = NEG_INF_F; - for (int y = 1; y < M; ++y) { - const CmState &cs = model.states[static_cast(y)]; - if (cs.beginSc == NEG_INF) continue; - const float bsc = static_cast(cs.beginSc); - const float yVal = cellVal(y, cornerD, 1); - if (yVal == NEG_INF_F) continue; - const float cand = yVal + bsc; - if (rootCell < cand) rootCell = cand; - } - } - } - if (anchorI > 0 && anchorD > 0 - && anchorI <= N && anchorI + anchorD - 1 <= N) { - const int midpoint = anchorI + (anchorD - 1) / 2; - for (int d = minSpan; d <= maxSpan; ++d) { - const int iMax = N - d + 1; - const int iLo = std::max(1, midpoint - d + 1); - const int iHi = std::min(iMax, midpoint); - for (int i = iLo; i <= iHi; ++i) { - float *rootCellRow = rowPtr(root, d); - if (rootCellRow == nullptr) continue; - float &rootCell = rootCellRow[static_cast(i)]; - rootCell = NEG_INF_F; - for (int y = 1; y < M; ++y) { - const CmState &cs = model.states[static_cast(y)]; - if (cs.beginSc == NEG_INF) continue; - const float bsc = static_cast(cs.beginSc); - const float yVal = cellVal(y, d, i); - if (yVal == NEG_INF_F) continue; - const float cand = yVal + bsc; - if (rootCell < cand) rootCell = cand; - } - } - } - } - } else { - for (int y = 1; y < M; ++y) { - const CmState &cs = model.states[static_cast(y)]; - if (cs.beginSc == NEG_INF) { - continue; - } - const float bsc = static_cast(cs.beginSc); - const simd_float vbsc = simdf32_set(bsc); - const int VS = VECSIZE_FLOAT; - for (int d = 0; d <= maxSpan; ++d) { - const int iMax = (d == 0) ? (N + 1) : (N - d + 1); - float *rootBaseRow = rowPtr(root, d); - if (rootBaseRow == nullptr) continue; - float *rootRow = rootBaseRow + 1; - const float *yRow = rowPtrConst(y, d) + 1; - // SIMD: rootRow[i] = max(rootRow[i], yRow[i] + bsc) - int i = 0; - for (; i + VS - 1 < iMax; i += VS) { - simd_float vy = simdf32_loadu(yRow + i); - simd_float vr = simdf32_loadu(rootRow + i); - simd_float cand = simdf32_add(vy, vbsc); - simdf32_storeu(rootRow + i, simdf32_max(vr, cand)); - } - for (; i < iMax; ++i) { - const float cand = yRow[i] + bsc; - if (rootRow[i] < cand) rootRow[i] = cand; - } - } - } - } - } - float bestSc = NEG_INF_F; - int bestI = 1; - int bestD = N; - char bestMode = 'J'; - double bestNull3Corr = 0.0; - const bool enableTruncModes = cmTruncModesEnabled(); - float bestL = NEG_INF_F, bestR = NEG_INF_F, bestT = NEG_INF_F; - int bestIL = 1, bestIR = 1, bestIT = 1; - int bestDL = N, bestDR = N, bestDT = N; - double bestLCorr = 0.0, bestRCorr = 0.0, bestTCorr = 0.0; - // Diagnostic: when MMSEQS_CMSCAN_DUMP_TID matches seqId, dump the score landscape. - const char *dumpTid = std::getenv("MMSEQS_CMSCAN_DUMP_TID"); - const bool dumpThisTarget = (dumpTid != NULL && seqId == dumpTid); - if (dumpThisTarget) { - fprintf(stderr, "DUMP_HEADER tid=%s N=%d M=%d maxSpan=%d minSpan=%d truncModes=%d\n", - seqId.c_str(), N, M, maxSpan, minSpan, enableTruncModes ? 1 : 0); - } - // Per-d best raw and corrected scores, for diagnostic dump. - std::vector dBestRaw(static_cast(maxSpan + 1), NEG_INF_F); - std::vector dBestSc(static_cast(maxSpan + 1), NEG_INF_F); - std::vector dBestI(static_cast(maxSpan + 1), -1); - for (int d = minSpan; d <= maxSpan; ++d) { - const int iMax = N - d + 1; - int iLo = 1, iHi = iMax; - if (forceEnv) { iLo = forcedI; iHi = forcedI; } - for (int i = iLo; i <= iHi; ++i) { - const float rawSc = cellVal(root, d, i); - if (rawSc == NEG_INF_F) { - continue; - } - const int j = i + d - 1; - const bool mayImproveJ = rawSc > bestSc; - const bool mayImproveL = enableTruncModes && j == N && rawSc > bestL; - const bool mayImproveR = enableTruncModes && i == 1 && rawSc > bestR; - const bool mayImproveT = enableTruncModes && i == 1 && j == N && rawSc > bestT; - const bool dumpCell = dumpThisTarget && rawSc > dBestRaw[static_cast(d)]; - if (!(mayImproveJ || mayImproveL || mayImproveR || mayImproveT) && !dumpCell) { - continue; - } - const double corr = null3CorrByInterval(i, j); - const float sc = static_cast(static_cast(rawSc) - corr); - if (dumpCell) { - dBestRaw[static_cast(d)] = rawSc; - dBestSc[static_cast(d)] = sc; - dBestI[static_cast(d)] = i; - } - if (mayImproveJ && sc > bestSc) { - bestSc = sc; - bestI = i; - bestD = d; - bestMode = 'J'; - bestNull3Corr = corr; - } - if (enableTruncModes) { - if (j == N && sc > bestL) { - bestL = sc; - bestIL = i; - bestDL = d; - bestLCorr = corr; - } - if (i == 1 && sc > bestR) { - bestR = sc; - bestIR = i; - bestDR = d; - bestRCorr = corr; - } - if (i == 1 && j == N && sc > bestT) { - bestT = sc; - bestIT = i; - bestDT = d; - bestTCorr = corr; - } - } - } - } - if (enableTruncModes) { - if (bestL > bestSc) { - bestSc = bestL; - bestI = bestIL; - bestD = bestDL; - bestMode = 'L'; - bestNull3Corr = bestLCorr; - } - if (bestR > bestSc) { - bestSc = bestR; - bestI = bestIR; - bestD = bestDR; - bestMode = 'R'; - bestNull3Corr = bestRCorr; - } - if (bestT > bestSc) { - bestSc = bestT; - bestI = bestIT; - bestD = bestDT; - bestMode = 'T'; - bestNull3Corr = bestTCorr; - } - } - // MMSEQS_CMSCAN_FORCE_GLOBAL=1: force trace to start at (i=1, d=min(N, maxSpan)), - // i.e. full envelope alignment from position 1. Worst for envelopes where the - // conserved core sits at i>1. - // MMSEQS_CMSCAN_FORCE_GLOBAL=2: pin d=min(N, maxSpan), pick i = argmax alpha[ROOT_S][d][i]. - // Full-length subspan, but slid to wherever score is best — handles 4LCK_3-style - // envelopes where the conserved region is not at i=1. - // MMSEQS_CMSCAN_FORCE_GLOBAL=3: midpoint-anchor. argmax over (i, d) where - // i <= prefilter-midpoint <= i+d-1. Free d (adapts per hit), but envelope - // must straddle the prefilter's most-confident point. - // MMSEQS_CMSCAN_FORCE_GLOBAL=4: span-contain. argmax over (i, d) where the - // prefilter [anchorI..anchorI+anchorD-1] is fully contained in [i..i+d-1]. - // Strictest gate; equivalent to "envelope must cover the prefilter span". - // MMSEQS_CMSCAN_FORCE_GLOBAL=5: per-target hybrid. Try FG=1 corner first; if its - // null3-corrected score < MMSEQS_CMSCAN_FG_HYBRID_THRESHOLD (default 0 bits), - // fall back to FG=3 midpoint anchor. Preserves column stability for hits where - // FG=1 works, recovers FG=3 alignment for targets where FG=1 picks junk. - { - // forceGlobal is hoisted above the local-begin pass; reuse it here. - if (forceGlobal == 1) { - const int gD = std::min(maxSpan, N); - const int gI = 1; - if (gD >= minSpan && gI + gD - 1 <= N) { - const float gSc = cellVal(root, gD, gI); - if (gSc != NEG_INF_F) { - const double corr = null3CorrByInterval(gI, gI + gD - 1); - bestSc = static_cast(static_cast(gSc) - corr); - bestI = gI; - bestD = gD; - bestMode = 'T'; - bestNull3Corr = corr; - } - } - } else if (forceGlobal == 2) { - const int gD = std::min(maxSpan, N); - if (gD >= minSpan) { - int bestIglob = -1; - float bestScGlob = NEG_INF_F; - double bestCorrGlob = 0.0; - const int iMax = N - gD + 1; - for (int gI = 1; gI <= iMax; ++gI) { - const float raw = cellVal(root, gD, gI); - if (raw == NEG_INF_F) continue; - const double corr = null3CorrByInterval(gI, gI + gD - 1); - const float sc = static_cast(static_cast(raw) - corr); - if (sc > bestScGlob) { - bestScGlob = sc; - bestIglob = gI; - bestCorrGlob = corr; - } - } - if (bestIglob > 0) { - bestSc = bestScGlob; - bestI = bestIglob; - bestD = gD; - bestMode = 'T'; - bestNull3Corr = bestCorrGlob; - } - } - } else if ((forceGlobal == 3 || forceGlobal == 4) && anchorI > 0 && anchorD > 0 - && anchorI <= N && anchorI + anchorD - 1 <= N) { - const int anchorEnd = anchorI + anchorD - 1; - const int midpoint = anchorI + (anchorD - 1) / 2; - int bestIa = -1, bestDa = -1; - float bestScA = NEG_INF_F; - double bestCorrA = 0.0; - for (int d = minSpan; d <= maxSpan; ++d) { - const int iMax = N - d + 1; - int iLo, iHi; - if (forceGlobal == 3) { - // envelope must contain midpoint: i <= midpoint <= i+d-1 - iLo = std::max(1, midpoint - d + 1); - iHi = std::min(iMax, midpoint); - } else { - // envelope must contain [anchorI..anchorEnd]: - // i <= anchorI and i+d-1 >= anchorEnd - iLo = std::max(1, anchorEnd - d + 1); - iHi = std::min(iMax, anchorI); - } - for (int i = iLo; i <= iHi; ++i) { - const float raw = cellVal(root, d, i); - if (raw == NEG_INF_F) continue; - const double corr = null3CorrByInterval(i, i + d - 1); - const float sc = static_cast(static_cast(raw) - corr); - if (sc > bestScA) { - bestScA = sc; - bestIa = i; - bestDa = d; - bestCorrA = corr; - } - } - } - if (bestIa > 0) { - bestSc = bestScA; - bestI = bestIa; - bestD = bestDa; - bestMode = 'J'; - bestNull3Corr = bestCorrA; - } - } else if (forceGlobal == 5) { - // Margin-gated fallback: keep FG=1 corner unless FG=3 best is - // meaningfully better. Preserves FG=1 column stability across - // targets where corner is a reasonable choice; only switches - // to FG=3 when corner is dramatically worse (e.g., CP150205.1 - // case: corner=-19, fg3=+50, gap=69 bits → fall back). - // MMSEQS_CMSCAN_FG_HYBRID_MARGIN: gap (bits) above which we - // fall back. Default 10. Higher value biases toward FG=1. - static const float fgHybridMargin = []() -> float { - const char *e = std::getenv("MMSEQS_CMSCAN_FG_HYBRID_MARGIN"); - if (e == NULL || *e == '\0') return 10.0f; - return static_cast(std::atof(e)); - }(); - const int gD1 = std::min(maxSpan, N); - const int gI1 = 1; - float fg1Sc = NEG_INF_F; - double fg1Corr = 0.0; - if (gD1 >= minSpan && gI1 + gD1 - 1 <= N) { - const float raw = cellVal(root, gD1, gI1); - if (raw != NEG_INF_F) { - fg1Corr = null3CorrByInterval(gI1, gI1 + gD1 - 1); - fg1Sc = static_cast(static_cast(raw) - fg1Corr); - } - } - int fg3I = -1, fg3D = -1; - float fg3Sc = NEG_INF_F; - double fg3Corr = 0.0; - if (anchorI > 0 && anchorD > 0 - && anchorI <= N && anchorI + anchorD - 1 <= N) { - const int midpoint = anchorI + (anchorD - 1) / 2; - for (int d = minSpan; d <= maxSpan; ++d) { - const int iMax = N - d + 1; - const int iLo = std::max(1, midpoint - d + 1); - const int iHi = std::min(iMax, midpoint); - for (int i = iLo; i <= iHi; ++i) { - const float raw = cellVal(root, d, i); - if (raw == NEG_INF_F) continue; - const double corr = null3CorrByInterval(i, i + d - 1); - const float sc = static_cast(static_cast(raw) - corr); - if (sc > fg3Sc) { - fg3Sc = sc; - fg3I = i; - fg3D = d; - fg3Corr = corr; - } - } - } - } - bool useFg3 = false; - if (fg3Sc != NEG_INF_F) { - if (fg1Sc == NEG_INF_F) { - useFg3 = true; - } else if ((fg3Sc - fg1Sc) > fgHybridMargin) { - useFg3 = true; - } - } - if (useFg3) { - bestSc = fg3Sc; - bestI = fg3I; - bestD = fg3D; - bestMode = 'J'; - bestNull3Corr = fg3Corr; - } else if (fg1Sc != NEG_INF_F) { - bestSc = fg1Sc; - bestI = gI1; - bestD = gD1; - bestMode = 'T'; - bestNull3Corr = fg1Corr; - } - } - } - if (dumpThisTarget) { - fprintf(stderr, "DUMP_BEST tid=%s bestI=%d bestD=%d bestSc=%.4f bestMode=%c null3Corr=%.4f\n", - seqId.c_str(), bestI, bestD, bestSc, bestMode, bestNull3Corr); - for (int d = minSpan; d <= maxSpan; ++d) { - if (dBestRaw[static_cast(d)] == NEG_INF_F) continue; - fprintf(stderr, "DUMP_D tid=%s d=%d bestI=%d rawSc=%.4f sc=%.4f\n", - seqId.c_str(), d, dBestI[static_cast(d)], - dBestRaw[static_cast(d)], dBestSc[static_cast(d)]); - } - // Cell-diff vs Infernal: dump alpha[v=0][j=jTarget][d] for j_target from env. - // Default jTarget = N (envelope-end at last residue) and additionally a list - // from MMSEQS_CMSCAN_DUMP_J="167" (comma-separated). - const char *dumpJList = std::getenv("MMSEQS_CMSCAN_DUMP_J"); - std::vector jTargets; - if (dumpJList != NULL) { - std::string s = dumpJList; - size_t pos = 0; - while (pos < s.size()) { - size_t comma = s.find(',', pos); - int jt = std::atoi(s.substr(pos, comma - pos).c_str()); - if (jt > 0 && jt <= N) jTargets.push_back(jt); - if (comma == std::string::npos) break; - pos = comma + 1; - } - } - for (int jT : jTargets) { - for (int d = 1; d <= jT; ++d) { - const int i = jT - d + 1; - if (i < 1 || i > N) continue; - if (d > maxSpan) continue; - const float a = cellVal(root, d, i); - fprintf(stderr, "DUMP_CELL tid=%s j=%d i=%d d=%d alpha=%.4f\n", - seqId.c_str(), jT, i, d, a); - } - } - // For each (j_target, d) of interest, dump alpha across all states v - // that have finite alpha. Lets us see which y is supposed to feed - // local-begin pickup. Gated by MMSEQS_CMSCAN_DUMP_ALL_V=1. - const char *dumpAllV = std::getenv("MMSEQS_CMSCAN_DUMP_ALL_V"); - if (dumpAllV != NULL && dumpAllV[0] == '1') { - const char *dumpDmin = std::getenv("MMSEQS_CMSCAN_DUMP_DMIN"); - int dMin = (dumpDmin != NULL) ? std::atoi(dumpDmin) : 100; - if (dMin < 1) dMin = 1; - for (int jT : jTargets) { - for (int d = dMin; d <= std::min(jT, maxSpan); ++d) { - const int i = jT - d + 1; - if (i < 1 || i > N) continue; - for (int v = 0; v < M; ++v) { - const float a = cellVal(v, d, i); - if (a == NEG_INF_F) continue; - const CmState &cs = model.states[static_cast(v)]; - fprintf(stderr, "DUMP_V tid=%s j=%d d=%d v=%d type=%d node=%d ndtype=%d firstOfNode=%d alpha=%.4f beginSc=%.4f endSc=%.4f\n", - seqId.c_str(), jT, d, v, - static_cast(cs.type), - cs.nodeIdx, - static_cast(cs.nodeType), - cs.isFirstOfNode ? 1 : 0, - a, - static_cast(cs.beginSc), - static_cast(cs.endSc)); - } - } - } - } - } - if (bestSc == NEG_INF_F) { - return; - } - - int minUsed = N + 1; - int maxUsed = 0; - int obsCount[4] = {0, 0, 0, 0}; - double modelAggRaw[4] = {0.0, 0.0, 0.0, 0.0}; - std::string traceOps; - traceOps.reserve(static_cast(bestD + 8)); - std::vector traceStates; - traceStates.reserve(static_cast(M + N)); - struct TbCell { int v; int i; int d; }; - std::vector st; - st.reserve(static_cast(M + N)); - st.push_back(TbCell{root, bestI, bestD}); - // MMSEQS_CMSCAN_DUMP_TRACE_TID=: per-step trace dump for cell-diff - // vs Infernal --tfile. Format mirrors parsetree idx/emitl/emitr/state. - const char *dumpTraceTid = std::getenv("MMSEQS_CMSCAN_DUMP_TRACE_TID"); - const bool dumpThisTrace = (dumpTraceTid != NULL && seqId == dumpTraceTid); - int traceIdxCounter = 0; - if (dumpThisTrace) { - fprintf(stderr, "DUMP_TRACE_HDR tid=%s bestI=%d bestD=%d bestSc=%.4f bestMode=%c\n", - seqId.c_str(), bestI, bestD, bestSc, bestMode); - } - while (!st.empty()) { - TbCell c = st.back(); - st.pop_back(); - const ExactStateExec &sv = exec[static_cast(c.v)]; - if (dumpThisTrace) { - const int j_dbg = c.i + c.d - 1; - fprintf(stderr, "DUMP_TRACE idx=%d v=%d type=%d i=%d j=%d d=%d\n", - traceIdxCounter++, c.v, (int)sv.type, c.i, j_dbg, c.d); - } - if (sv.type == CM_ST_E) { - continue; - } - traceStates.push_back(c.v); - const double cur = cellVal(c.v, c.d, c.i); - if (cur == NEG_INF_F) { - continue; - } - if (sv.type == CM_ST_B) { - const int y = sv.bLeft; - const int z = sv.bRight; - const size_t vd = static_cast(c.v) * vdStride + static_cast(c.d); - const int kBeg = bSplitBegByVD[vd]; - const int kEnd = bSplitEndByVD[vd]; - // Argmax over the split range. l + r is computed in float to - // mirror fill's `_mm_max_ps`/`std::fmaxf` reduction order, so - // ties resolve identically. - int bestK = -1; - float bestApprox = NEG_INF_F; - for (int k = kBeg; k <= kEnd; ++k) { - const float l = cellVal(y, k, c.i); - const float r = cellVal(z, c.d - k, c.i + k); - if (l == NEG_INF_F || r == NEG_INF_F) { - continue; - } - const float cand = l + r; - if (cand > bestApprox) { - bestApprox = cand; - bestK = k; - } - } - if (bestK >= 0) { - st.push_back(TbCell{z, c.i + bestK, c.d - bestK}); - st.push_back(TbCell{y, c.i, bestK}); - } - continue; - } - - const int j = c.i + c.d - 1; - const int consume = sv.dConsume; - const int ni = c.i + sv.niShift; - const char op = traceOpForState(sv.type); - if (op != '\0') { - for (int x = 0; x < consume; ++x) { - traceOps.push_back(op); - } - } - const bool cL = sv.consumesLeft; - const bool cR = sv.consumesRight; - if (cL) { - minUsed = std::min(minUsed, c.i); - maxUsed = std::max(maxUsed, c.i); - const int bi = ws.seqCode[static_cast(c.i)]; - if (bi >= 0 && bi < 4) { - obsCount[bi]++; - } - } - if (cR) { - minUsed = std::min(minUsed, j); - maxUsed = std::max(maxUsed, j); - const int bj = ws.seqCode[static_cast(j)]; - if (bj >= 0 && bj < 4) { - obsCount[bj]++; - } - } - modelAggRaw[0] += sv.null2Agg[0]; - modelAggRaw[1] += sv.null2Agg[1]; - modelAggRaw[2] += sv.null2Agg[2]; - modelAggRaw[3] += sv.null2Agg[3]; - if (c.d < consume) { - continue; - } - const int nd = c.d - consume; - const double e = cmStateEmitScoreFast(sv, ws.seqCode, c.i, j); - const float ef = static_cast(e); - // Argmax over children. Candidate is computed in float as - // `(ef + trF) + n` to mirror fill's left-associative SIMD/scalar - // arithmetic exactly, so trace and fill agree on tie-breaks. - // Replaces a tolerance-match early-exit (`|cur - cand| < 1e-6`) - // that could short-circuit on a non-optimal child when float - // rounding pushed an inferior candidate within the tolerance. - int bestY = -1; - float bestCand = NEG_INF_F; - const int trCount = sv.trCount; - const bool dumpCand = (dumpThisTrace && c.v == root); - // MMSEQS_CMSCAN_TRACE_LBO=1: at root, mirror Infernal cm_localize - // (cm_modelconfig.c:491) which zeroes cm->t[0] when local mode is on, - // making ROOT_S→child via trDst NEG_INF in cm_alignT. Only local-begin - // can leave the root. Equivalent at trace time without poisoning DP fill. - static const char *const lboEnv = std::getenv("MMSEQS_CMSCAN_TRACE_LBO"); - const bool traceLBO = (lboEnv != NULL && lboEnv[0] == '1'); - const bool skipTrDstAtRoot = traceLBO && (c.v == root) && model.hasLocalCfg; - if (skipTrDstAtRoot) { - if (dumpCand) { - fprintf(stderr, "DUMP_TRACE_CAND v=%d kind=tr_skipped reason=LBO\n", c.v); - } - } else if (trCount <= 4) { - for (int t = 0; t < trCount; ++t) { - const int y = sv.trDst4[t]; - const float n = cellVal(y, nd, ni); - if (dumpCand) { - fprintf(stderr, "DUMP_TRACE_CAND v=%d kind=tr t=%d y=%d trSc=%.6f n=%.6f cand=%.6f\n", - c.v, t, y, sv.trScF4[t], n, - (n == NEG_INF_F) ? -INFINITY : (ef + sv.trScF4[t]) + n); - } - if (n == NEG_INF_F) { - continue; - } - const float cand = (ef + sv.trScF4[t]) + n; - if (cand > bestCand) { - bestCand = cand; - bestY = y; - } - } - } else { - for (int t = 0; t < trCount; ++t) { - const size_t ti = sv.trOff + static_cast(t); - const int y = trDst[ti]; - const float trF = static_cast(trSc[ti]); - const float n = cellVal(y, nd, ni); - if (dumpCand) { - fprintf(stderr, "DUMP_TRACE_CAND v=%d kind=tr t=%d y=%d trSc=%.6f n=%.6f cand=%.6f\n", - c.v, t, y, trF, n, - (n == NEG_INF_F) ? -INFINITY : (ef + trF) + n); - } - if (n == NEG_INF_F) { - continue; - } - const float cand = (ef + trF) + n; - if (cand > bestCand) { - bestCand = cand; - bestY = y; - } - } - } - // Local-begin trace: at ROOT_S, alpha[root][i][d] may have been - // reached via a direct local-begin jump to some begin-eligible y - // (the per-cell pickup at CmScan.cpp:2807-2826 mirrors cmsearch - // search semantics). Without considering those candidates here the - // trace walks ROOT_S→child recurrence even when local-begin won at - // the chart, producing a misaligned CIGAR. Cell-diff bug: - // project_cyk_celldiff_root_local_begin.md. - if (c.v == root && model.hasLocalCfg) { - for (int y = 1; y < M; ++y) { - const CmState &cs = model.states[static_cast(y)]; - if (cs.beginSc == NEG_INF) { - continue; - } - const float bsc = static_cast(cs.beginSc); - const float n = cellVal(y, nd, ni); - if (dumpCand) { - fprintf(stderr, "DUMP_TRACE_CAND v=%d kind=lb y=%d bsc=%.6f n=%.6f cand=%.6f\n", - c.v, y, bsc, n, - (n == NEG_INF_F) ? -INFINITY : (ef + bsc) + n); - } - if (n == NEG_INF_F) { - continue; - } - const float cand = (ef + bsc) + n; // ef=0 for ROOT_S - if (cand > bestCand) { - bestCand = cand; - bestY = y; - } - } - } - // EL pop-out candidate (mirrors yshadow=USED_EL in Infernal cm_alignT). - // At any end-eligible state v (endSc[v] != NEG_INF, local mode on), - // alpha[v][j][d] may have been reached via the EL pre-fill at line - // 2700-2742: emit_v(i,d) + elSelf*(d-sd) + endSc[v]. If that beats - // every trDst-via-children candidate above, record EL pop-out: skip - // pushing children. The CIGAR builder is column-driven (line 1593: - // any unwalked col = D op), so the right subtree's columns become - // gaps automatically. Residues popped to EL are accounted for via - // minUsed/maxUsed expansion so the alignment span covers them. - // Gated by MMSEQS_CMSCAN_TRACE_EL=1 for diagnostic A/B. - static const char *const elTraceEnv = std::getenv("MMSEQS_CMSCAN_TRACE_EL"); - const bool traceEL = (elTraceEnv != NULL && elTraceEnv[0] == '1'); - bool pickEL = false; - if (traceEL && model.hasLocalCfg && model.elSelf <= 0.0) { - const CmState &csEL = model.states[static_cast(c.v)]; - if (csEL.endSc != NEG_INF) { - const float elCand = ef - + static_cast(model.elSelf) * static_cast(nd) - + static_cast(csEL.endSc); - if (dumpCand) { - fprintf(stderr, "DUMP_TRACE_CAND v=%d kind=el endSc=%.6f elContrib=%.6f cand=%.6f\n", - c.v, (float)csEL.endSc, - static_cast(model.elSelf) * static_cast(nd), - elCand); - } - if (elCand > bestCand) { - bestCand = elCand; - pickEL = true; - } - } - } - if (dumpThisTrace && c.v == root) { - fprintf(stderr, "DUMP_TRACE_PICK v=%d bestY=%d bestCand=%.6f%s\n", - c.v, bestY, bestCand, pickEL ? " EL" : ""); - } - (void)cur; // cur is only used for the NEG_INF_F early-skip above. - if (pickEL) { - // EL pops `nd` residues without column consumption. Update span - // accounting; do not push children. - if (nd > 0) { - minUsed = std::min(minUsed, ni); - maxUsed = std::max(maxUsed, ni + nd - 1); - } - if (dumpThisTrace) { - fprintf(stderr, "DUMP_TRACE_EL v=%d popped_residues=%d at_i=%d..%d\n", - c.v, nd, ni, ni + nd - 1); - } - } else if (bestY >= 0) { - st.push_back(TbCell{bestY, ni, nd}); - } - } - const int tracedLen = (minUsed <= maxUsed) ? (maxUsed - minUsed + 1) : 0; - if (minUsed > maxUsed || tracedLen < std::max(1, minSpan / 2)) { - minUsed = bestI; - maxUsed = bestI + bestD - 1; - } - Hit h; - h.seqId = seqId; - h.start1 = minUsed; - h.end1 = maxUsed; - h.mode = bestMode; - h.trunc = (bestMode != 'J'); - h.traceStates = encodeTraceStates(traceStates); - { - int qS = -1, qE = -1, aL = 0, leadIns = 0, trailIns = 0; - h.cigar = modelTraceCigarQueryCoord(model, traceStates, &qS, &qE, &aL, - &leadIns, &trailIns); - h.qStart = qS; - h.qEnd = qE; - h.cigarAlnLen = static_cast(std::max(0, aL)); - h.leadingInsertTargets = leadIns; - h.trailingInsertTargets = trailIns; - } - const double null2Corr = scoreCorrectionNull2BitsFromTrace(model, modelAggRaw, obsCount); - h.cyk = static_cast(bestSc) - null2Corr; - if (wantInside) { - // Deck-based inside DP over the restricted interval [bestI, bestJ]. - // Layout: insD[d * dStride + v * vStride + li] where li = i - bestI. - // Iterate d=0..localN (outer), v=M-1..0 (children before parents), - // li=0..localN-d (inner, sequential → cache-friendly reads/writes). - const int localN = bestD; - const size_t vStride = static_cast(localN + 1); - const size_t dStride = static_cast(M) * vStride; - const size_t insSize = static_cast(localN + 1) * dStride; - static constexpr size_t CM_INSIDE_MAX_CELLS = 100000000ULL; // ~800 MB doubles - if (insSize > CM_INSIDE_MAX_CELLS) { - h.inside = NEG_INF; - h.bias = bestNull3Corr + null2Corr; - outHits.push_back(h); - return; - } - if (!ws.insD.ensure(insSize)) { - Debug(Debug::ERROR) << "cmscan: failed to allocate inside buffer\n"; - return; - } - double *insDataAll = ws.insD.data(); - for (size_t ii = 0; ii < insSize; ++ii) { - insDataAll[ii] = NEG_INF; - } - // E states emit the empty string: ins[d=0][E][li] = 0. - for (int v = 0; v < M; ++v) { - if (exec[static_cast(v)].type != CM_ST_E) { continue; } - double *row = insDataAll + static_cast(v) * vStride; - for (int li = 0; li <= localN; ++li) { row[li] = 0.0; } - } - for (int d = 0; d <= localN; ++d) { - const int liMax = localN - d; - double *insSlice = insDataAll + static_cast(d) * dStride; - const double *insData = insDataAll; - const StateId *trDstPtr = trDst.data(); - const double *trScPtr = trSc.data(); - for (int v = M - 1; v >= 0; --v) { - const ExactStateExec &st = exec[static_cast(v)]; - if (st.type == CM_ST_E) { continue; } - if (st.dmax >= st.dmin && (d < st.dmin || d > st.dmax)) { continue; } - if (st.type == CM_ST_B) { - const int y = st.bLeft, z = st.bRight; - if (y < 0 || y >= M || z < 0 || z >= M) { continue; } - const size_t vd = static_cast(v) * vdStride + static_cast(d); - const int kBeg = bSplitBegByVD[vd]; - const int kEnd = bSplitEndByVD[vd]; - double *dst = insSlice + static_cast(v) * vStride; - if (fastLogsum) { - for (int li = 0; li <= liMax; ++li) { - bool has = false; - float acc = NEG_INF_F; - for (int k = kBeg; k <= kEnd; ++k) { - const double lv = insData[static_cast(k) * dStride + static_cast(y) * vStride + static_cast(li)]; - const double rv = insData[static_cast(d - k) * dStride + static_cast(z) * vStride + static_cast(li + k)]; - if (lv != NEG_INF && rv != NEG_INF) { - log2AccFastAddF(static_cast(lv + rv), has, acc); - } - } - dst[li] = has ? static_cast(acc) : NEG_INF; - } - } else { - for (int li = 0; li <= liMax; ++li) { - bool has = false; - double maxVal = NEG_INF; - double scaledSum = 0.0; - for (int k = kBeg; k <= kEnd; ++k) { - const double lv = insData[static_cast(k) * dStride + static_cast(y) * vStride + static_cast(li)]; - const double rv = insData[static_cast(d - k) * dStride + static_cast(z) * vStride + static_cast(li + k)]; - if (lv != NEG_INF && rv != NEG_INF) { - log2AccExactAdd(lv + rv, has, maxVal, scaledSum); - } - } - dst[li] = log2AccExactValue(has, maxVal, scaledSum); - } - } - continue; - } - { - const char *probeV = std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_V"); - if (probeV != NULL && v == std::atoi(probeV)) { - fprintf(stderr, "DUMP_TRANS_PROBE v=%d d=%d type=%d dConsume=%d trCount=%d trOff=%zu emitSize=%d emitPtr=%p niShift=%d\n", - v, d, (int)st.type, st.dConsume, st.trCount, st.trOff, st.emitSize, (void*)st.emitPtr, st.niShift); - } - } - if (d < st.dConsume) { continue; } - const int nd = d - st.dConsume; - const int niShift = st.niShift; - const float *ep = st.emitPtr; - const int8_t *sc = ws.seqCode.data(); - const double *nxtSlice = insDataAll + static_cast(nd) * dStride; - double *dst = insSlice + static_cast(v) * vStride; - if (fastLogsum) { - for (int li = 0; li <= liMax; ++li) { - float ef = 0.0f; - if (st.consumesLeft && st.consumesRight) { - if (!(ep && st.emitSize >= 16)) { - ef = NEG_INF_F; - } else { - const int i = li + bestI; - const int lc = sc[i]; - const int rc = sc[i + d - 1]; - ef = (lc >= 0 && rc >= 0) ? ep[lc * 4 + rc] : -1.0f; - } - } else if (st.consumesLeft) { - if (!(ep && st.emitSize >= 4)) { - ef = NEG_INF_F; - } else { - const int bi = sc[li + bestI]; - ef = (bi >= 0) ? ep[bi] : -1.0f; - } - } else if (st.consumesRight) { - if (!(ep && st.emitSize >= 4)) { - ef = NEG_INF_F; - } else { - const int bi = sc[li + bestI + d - 1]; - ef = (bi >= 0) ? ep[bi] : -1.0f; - } - } - const char *trDumpV = std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_V"); - const int trDumpD = (std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_D") != NULL) ? std::atoi(std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_D")) : -1; - const int trDumpJ = (std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_J") != NULL) ? std::atoi(std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_J")) : -1; - const bool dumpV = (trDumpV != NULL) && (v == std::atoi(trDumpV)); - const bool doDump = dumpV && (d == trDumpD) && (trDumpJ < 0 || li == (trDumpJ - d)); - if (doDump) { - fprintf(stderr, "DUMP_TRANS_LIENTRY v=%d d=%d li=%d ef=%.6f bestI=%d niShift=%d\n", - v, d, li, ef, bestI, niShift); - } - if (ef == NEG_INF_F) { continue; } - const int nli = li + niShift; - bool has = false; - float acc = NEG_INF_F; - // LOCAL-mode EL pre-init (Infernal cm_dpalign.c:1600-1604). - // For end-eligible v with finite endsc[v], seed alpha with - // ef + el_scA[d-sd] + endsc[v] before children FLogsum. - if (model.hasLocalCfg && model.elSelf <= 0.0) { - const CmState &csEl = model.states[static_cast(v)]; - if (csEl.endSc != NEG_INF) { - int sdEl = -1; - if (csEl.type == CM_ST_MP) sdEl = 2; - else if (csEl.type == CM_ST_ML || csEl.type == CM_ST_MR) sdEl = 1; - else if (csEl.type == CM_ST_S) sdEl = 0; - if (sdEl >= 0 && d >= sdEl) { - const float elTerm = ef - + static_cast(model.elSelf) * static_cast(d - sdEl) - + static_cast(csEl.endSc); - log2AccFastAddF(elTerm, has, acc); - } - } - } - for (int t = 0; t < st.trCount; ++t) { - const size_t ti = st.trOff + static_cast(t); - const double n = nxtSlice[static_cast(trDstPtr[ti]) * vStride + static_cast(nli)]; - if (doDump) { - fprintf(stderr, "DUMP_TRANS v=%d d=%d t=%d trDst=%d trSc=%.6f ef=%.6f childAlpha=%.6f sum=%.6f\n", - v, d, t, (int)trDstPtr[ti], trScPtr[ti], ef, n, - (n != NEG_INF) ? (ef + static_cast(trScPtr[ti]) + static_cast(n)) : NEG_INF); - } - if (n != NEG_INF) { - log2AccFastAddF(ef + static_cast(trScPtr[ti]) + static_cast(n), has, acc); - } - } - if (doDump) { - fprintf(stderr, "DUMP_TRANS v=%d d=%d FINAL acc=%.6f\n", v, d, has ? acc : NEG_INF); - } - dst[li] = has ? static_cast(acc) : NEG_INF; - } - } else { - for (int li = 0; li <= liMax; ++li) { - float ef = 0.0f; - if (st.consumesLeft && st.consumesRight) { - if (!(ep && st.emitSize >= 16)) { - ef = NEG_INF_F; - } else { - const int i = li + bestI; - const int lc = sc[i]; - const int rc = sc[i + d - 1]; - ef = (lc >= 0 && rc >= 0) ? ep[lc * 4 + rc] : -1.0f; - } - } else if (st.consumesLeft) { - if (!(ep && st.emitSize >= 4)) { - ef = NEG_INF_F; - } else { - const int bi = sc[li + bestI]; - ef = (bi >= 0) ? ep[bi] : -1.0f; - } - } else if (st.consumesRight) { - if (!(ep && st.emitSize >= 4)) { - ef = NEG_INF_F; - } else { - const int bi = sc[li + bestI + d - 1]; - ef = (bi >= 0) ? ep[bi] : -1.0f; - } - } - const char *trDumpV2 = std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_V"); - const int trDumpD2 = (std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_D") != NULL) ? std::atoi(std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_D")) : -1; - const int trDumpJ2 = (std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_J") != NULL) ? std::atoi(std::getenv("MMSEQS_CMSCAN_DUMP_TRANS_J")) : -1; - const bool dumpV2 = (trDumpV2 != NULL) && (v == std::atoi(trDumpV2)); - const bool doDump2 = dumpV2 && (d == trDumpD2) && (trDumpJ2 < 0 || li == (trDumpJ2 - d)); - if (doDump2) { - fprintf(stderr, "DUMP_TRANS_LIENTRY v=%d d=%d li=%d ef=%.6f bestI=%d niShift=%d (exact)\n", - v, d, li, ef, bestI, niShift); - } - if (ef == NEG_INF_F) { continue; } - const int nli = li + niShift; - bool has = false; - double maxVal = NEG_INF; - double scaledSum = 0.0; - // LOCAL-mode EL pre-init (Infernal cm_dpalign.c:1600-1604). - if (model.hasLocalCfg && model.elSelf <= 0.0) { - const CmState &csEl = model.states[static_cast(v)]; - if (csEl.endSc != NEG_INF) { - int sdEl = -1; - if (csEl.type == CM_ST_MP) sdEl = 2; - else if (csEl.type == CM_ST_ML || csEl.type == CM_ST_MR) sdEl = 1; - else if (csEl.type == CM_ST_S) sdEl = 0; - if (sdEl >= 0 && d >= sdEl) { - const double elTerm = static_cast(ef) - + model.elSelf * static_cast(d - sdEl) - + csEl.endSc; - log2AccExactAdd(elTerm, has, maxVal, scaledSum); - } - } - } - for (int t = 0; t < st.trCount; ++t) { - const size_t ti = st.trOff + static_cast(t); - const double n = nxtSlice[static_cast(trDstPtr[ti]) * vStride + static_cast(nli)]; - if (doDump2) { - fprintf(stderr, "DUMP_TRANS v=%d d=%d t=%d trDst=%d trSc=%.6f ef=%.6f childAlpha=%.6f sum=%.6f (exact)\n", - v, d, t, (int)trDstPtr[ti], trScPtr[ti], ef, n, - (n != NEG_INF) ? (static_cast(ef) + trScPtr[ti] + n) : NEG_INF); - } - if (n != NEG_INF) { - log2AccExactAdd(static_cast(ef) + trScPtr[ti] + n, has, maxVal, scaledSum); - } - } - const double finalA = log2AccExactValue(has, maxVal, scaledSum); - if (doDump2) { - fprintf(stderr, "DUMP_TRANS v=%d d=%d FINAL acc=%.6f (exact)\n", v, d, finalA); - } - dst[li] = finalA; - } - } - } - } - // LOCAL-mode local-begin pickup (Infernal cm_dpalign.c:1712-1721). - // After all states are filled, the root cell at (d=L, li=0) can be - // reached by jumping directly to any begin-eligible state y at score - // alpha[y][L][L] + beginsc[y]. FLogsum these into alpha[0][L][L]. - if (model.hasLocalCfg) { - const int root0 = model.rootState; - const size_t rootCellIdx = static_cast(bestD) * dStride - + static_cast(root0) * vStride + 0; - const double rootA = insDataAll[rootCellIdx]; - bool has = false; - double maxVal = NEG_INF; - double scaledSum = 0.0; - if (rootA != NEG_INF) log2AccExactAdd(rootA, has, maxVal, scaledSum); - for (int y = 1; y < M; ++y) { - const CmState &csB = model.states[static_cast(y)]; - if (csB.beginSc == NEG_INF) continue; - const double yA = insDataAll[static_cast(bestD) * dStride - + static_cast(y) * vStride + 0]; - if (yA == NEG_INF) continue; - log2AccExactAdd(yA + csB.beginSc, has, maxVal, scaledSum); - } - insDataAll[rootCellIdx] = log2AccExactValue(has, maxVal, scaledSum); - } - h.inside = insDataAll[static_cast(bestD) * dStride + static_cast(root) * vStride + 0] - - bestNull3Corr - null2Corr; - h.bias = bestNull3Corr + null2Corr; - // Cell-diff vs Infernal cm_InsideAlign: dump alpha[v][j][d] for chosen - // j_target (envelope-relative coords 1..localN). MMSEQS_CMSCAN_DUMP_INSIDE_J - // is a comma list. Indexing: alpha[v][j][d] = insDataAll[d*dStride + v*vStride + li] - // where li = j - d (since the envelope starts at bestI=1 here). - const char *dumpInsJList = std::getenv("MMSEQS_CMSCAN_DUMP_INSIDE_J"); - if (dumpInsJList != NULL) { - std::vector insJTargets; - std::string s = dumpInsJList; - size_t pos = 0; - while (pos < s.size()) { - size_t comma = s.find(',', pos); - int jt = std::atoi(s.substr(pos, comma - pos).c_str()); - if (jt > 0 && jt <= bestD) insJTargets.push_back(jt); - if (comma == std::string::npos) break; - pos = comma + 1; - } - for (int jT : insJTargets) { - for (int d = 0; d <= jT; ++d) { - const int li = jT - d; - if (li < 0 || li > bestD) continue; - const double *insSlice = insDataAll + static_cast(d) * dStride; - for (int v = 0; v < M; ++v) { - const double a = insSlice[static_cast(v) * vStride + static_cast(li)]; - if (a == NEG_INF) continue; - const ExactStateExec &st = exec[static_cast(v)]; - fprintf(stderr, "DUMP_INSIDE tid=%s j=%d d=%d v=%d type=%d alpha=%.6f\n", - seqId.c_str(), jT, d, v, - static_cast(st.type), - a); - } - } - } - } - } else { - h.inside = NEG_INF; - h.bias = bestNull3Corr + null2Corr; - } - outHits.push_back(h); - return; - } - - // Compute maxSpan first so we can size the per-state ragged chart precisely. - int maxSpan = N; - if (model.w > 0) { - maxSpan = std::min(maxSpan, model.w); - } - // Cap d-space by the prefilter alignment length when available — a true - // CM hit has d within a small multiple of the RNA search envelope length, - // and the memoized Viterbi only wastes memory on larger spans. - if (maxHitLen > 0) { - maxSpan = std::min(maxSpan, maxHitLen); - } - - // Per-state ragged chart: each state v gets bandSize[v]*(N+1) cells, with - // bands derived from QDB (CmState::dmin1/dmax1 as copied into exec[v].dmin/dmax). - // For big rRNA CMs (M ~ 9000) bands are typically tight (avg ~20) so the - // ragged chart is ~30x smaller than a flat M*(N+1)*(maxSpan+1) chart. - std::vector &chartOffset = ws.exactChartOffset; - std::vector &bandDmin = ws.exactChartBandDmin; - std::vector &bandSize = ws.exactChartBandSize; - chartOffset.assign(static_cast(M) + 1, 0); - bandDmin.assign(static_cast(M), 0); - bandSize.assign(static_cast(M), 0); - const size_t rowPlus1 = static_cast(N + 1); - for (int v = 0; v < M; ++v) { - const ExactStateExec &e = exec[static_cast(v)]; - int dlo; - int dhi; - if (e.dmax >= e.dmin && e.dmin >= 0) { - dlo = std::max(0, e.dmin); - dhi = std::min(maxSpan, e.dmax); - } else { - // Unset/invalid band — fall back to full range [0..maxSpan]. - dlo = 0; - dhi = maxSpan; - } - if (dhi < dlo) { - bandDmin[v] = 0; - bandSize[v] = 0; - } else { - bandDmin[v] = dlo; - bandSize[v] = dhi - dlo + 1; - } - chartOffset[v + 1] = chartOffset[v] + - static_cast(bandSize[v]) * rowPlus1; - } - size_t chartCells = chartOffset[M]; - - // Safety budget. 2G cells = 8GB float + 8GB uint32 = 16GB per-thread worst case. - // Big rRNA CMs (M=6869-9017, like CP000968) vs comparable-length targets - // land at ~8G cells = 65GB with wide QDB bands; we skip rather than OOM. - // TODO: streaming/windowed CYK or HMM pre-filter to unblock these queries. - static constexpr size_t CM_EXACT_CHART_MAX_CELLS = 2000000000ULL; - if (chartCells > CM_EXACT_CHART_MAX_CELLS) { - Debug(Debug::WARNING) << "cmscan: per-state ragged chart too large " - << "(" << chartCells << " > " << CM_EXACT_CHART_MAX_CELLS - << " cells) for M=" << M << " N=" << N << "; skipping target\n"; - return; - } - - std::vector &exactChart = ws.exactChart; - std::vector &exactChartSeen = ws.exactChartSeen; - uint32_t &exactChartGen = ws.exactChartGen; - if (exactChart.size() < chartCells) { - exactChart.resize(chartCells); - } - if (exactChartSeen.size() < chartCells) { - exactChartSeen.resize(chartCells, 0); - } - ++exactChartGen; - if (exactChartGen == 0) { - std::fill(exactChartSeen.begin(), exactChartSeen.end(), 0u); - exactChartGen = 1; - } - - ExactRecCtx recCtx; - recCtx.N = N; - recCtx.M = M; - recCtx.exec = &exec; - recCtx.execData = exec.data(); - recCtx.trDst = trDst.empty() ? NULL : &trDst[0]; - recCtx.trSc = trSc.empty() ? NULL : &trSc[0]; - recCtx.seqCode = &ws.seqCode; - recCtx.chartScore = exactChart.data(); - recCtx.chartSeen = exactChartSeen.data(); - recCtx.chartGen = exactChartGen; - recCtx.chartOffset = chartOffset.data(); - recCtx.chartBandDmin = bandDmin.data(); - recCtx.chartBandSize = bandSize.data(); - // Infernal semantics evaluate all legal spans under state dmin/dmax/w constraints. - int minSpan = 1; - // Fix B: force prefilter envelope on memoized path too (same as fast path). - // Only enable if forcedD fits within the chart bands already allocated. - const bool forceEnv2 = (forcedI > 0 && forcedD > 0 && - forcedD <= N && forcedI <= N - forcedD + 1 && - forcedD <= maxSpan); - if (forceEnv2) { - minSpan = forcedD; - maxSpan = forcedD; - } - double bestSc = NEG_INF; - int bestI = 1; - int bestJ = N; - char bestMode = 'J'; - double bestNull3Corr = 0.0; - const bool enableTruncModes = cmTruncModesEnabled(); - double bestL = NEG_INF, bestR = NEG_INF, bestT = NEG_INF; - int bestIL = 1, bestIR = 1, bestIT = 1; - int bestJL = N, bestJR = N, bestJT = N; - double bestLCorr = 0.0, bestRCorr = 0.0, bestTCorr = 0.0; - for (int d = minSpan; d <= maxSpan; ++d) { - const int iMax = N - d + 1; - int iLo = 1, iHi = iMax; - if (forceEnv2) { iLo = forcedI; iHi = forcedI; } - for (int i = iLo; i <= iHi; ++i) { - const int j = i + d - 1; - const double rawSc = exactVitRec(recCtx, model.rootState, i, j); - if (rawSc == NEG_INF) { - continue; - } - const bool mayImproveJ = rawSc > bestSc; - const bool mayImproveL = enableTruncModes && j == N && rawSc > bestL; - const bool mayImproveR = enableTruncModes && i == 1 && rawSc > bestR; - const bool mayImproveT = enableTruncModes && i == 1 && j == N && rawSc > bestT; - if (!(mayImproveJ || mayImproveL || mayImproveR || mayImproveT)) { - continue; - } - const double corr = null3CorrByInterval(i, j); - const double sc = rawSc - corr; - if (mayImproveJ && sc > bestSc) { - bestSc = sc; - bestI = i; - bestJ = j; - bestMode = 'J'; - bestNull3Corr = corr; - } - if (enableTruncModes) { - if (j == N && sc > bestL) { - bestL = sc; - bestIL = i; - bestJL = j; - bestLCorr = corr; - } - if (i == 1 && sc > bestR) { - bestR = sc; - bestIR = i; - bestJR = j; - bestRCorr = corr; - } - if (i == 1 && j == N && sc > bestT) { - bestT = sc; - bestIT = i; - bestJT = j; - bestTCorr = corr; - } - } - } - } - if (enableTruncModes) { - if (bestL > bestSc) { - bestSc = bestL; - bestI = bestIL; - bestJ = bestJL; - bestMode = 'L'; - bestNull3Corr = bestLCorr; - } - if (bestR > bestSc) { - bestSc = bestR; - bestI = bestIR; - bestJ = bestJR; - bestMode = 'R'; - bestNull3Corr = bestRCorr; - } - if (bestT > bestSc) { - bestSc = bestT; - bestI = bestIT; - bestJ = bestJT; - bestMode = 'T'; - bestNull3Corr = bestTCorr; - } - } - if (bestSc == NEG_INF) { - return; - } - int minUsed = N + 1; - int maxUsed = 0; - int obsCount[4] = {0, 0, 0, 0}; - double modelAggRaw[4] = {0.0, 0.0, 0.0, 0.0}; - std::string traceOps; - traceOps.reserve(static_cast(bestJ - bestI + 4)); - std::vector traceStates; - traceStates.reserve(static_cast(M + N)); - exactTraceRec(recCtx, model.rootState, bestI, bestJ, minUsed, maxUsed, obsCount, modelAggRaw, traceOps, traceStates); - const int tracedLen = (minUsed <= maxUsed) ? (maxUsed - minUsed + 1) : 0; - if (minUsed > maxUsed || tracedLen < std::max(1, minSpan / 2)) { - minUsed = bestI; - maxUsed = bestJ; - } - - Hit h; - h.seqId = seqId; - h.start1 = minUsed; - h.end1 = maxUsed; - h.mode = bestMode; - h.trunc = (bestMode != 'J'); - h.traceStates = encodeTraceStates(traceStates); - { - int qS = -1, qE = -1, aL = 0, leadIns = 0, trailIns = 0; - h.cigar = modelTraceCigarQueryCoord(model, traceStates, &qS, &qE, &aL, - &leadIns, &trailIns); - h.qStart = qS; - h.qEnd = qE; - h.cigarAlnLen = static_cast(std::max(0, aL)); - h.leadingInsertTargets = leadIns; - h.trailingInsertTargets = trailIns; - } - const double null2Corr = scoreCorrectionNull2BitsFromTrace(model, modelAggRaw, obsCount); - h.cyk = bestSc - null2Corr; - if (wantInside) { - // Deck-based inside DP over the restricted interval [bestI, bestJ]. - // Replaces the memoized insideRec for better cache performance. - const int localN2 = bestJ - bestI + 1; - const size_t vStride2 = static_cast(localN2 + 1); - const size_t dStride2 = static_cast(M) * vStride2; - const size_t insSize2 = static_cast(localN2 + 1) * dStride2; - static constexpr size_t CM_INSIDE_MAX_CELLS2 = 100000000ULL; // ~800 MB doubles - if (insSize2 > CM_INSIDE_MAX_CELLS2) { - h.inside = NEG_INF; - h.bias = bestNull3Corr + null2Corr; - outHits.push_back(h); - return; - } - if (!ws.insD.ensure(insSize2)) { - Debug(Debug::ERROR) << "cmscan: failed to allocate inside buffer2\n"; - return; - } - double *insDataAll2 = ws.insD.data(); - for (size_t ii = 0; ii < insSize2; ++ii) { - insDataAll2[ii] = NEG_INF; - } - for (int v = 0; v < M; ++v) { - if (exec[static_cast(v)].type != CM_ST_E) { continue; } - double *row = insDataAll2 + static_cast(v) * vStride2; - for (int li = 0; li <= localN2; ++li) { row[li] = 0.0; } - } - const float NEG_INF_F2 = -std::numeric_limits::infinity(); - for (int d = 0; d <= localN2; ++d) { - const int liMax2 = localN2 - d; - double *insSlice2 = insDataAll2 + static_cast(d) * dStride2; - const double *insData2 = insDataAll2; - const StateId *trDstPtr = trDst.data(); - const double *trScPtr = trSc.data(); - for (int v = M - 1; v >= 0; --v) { - const ExactStateExec &st = exec[static_cast(v)]; - if (st.type == CM_ST_E) { continue; } - if (st.dmax >= st.dmin && (d < st.dmin || d > st.dmax)) { continue; } - if (st.type == CM_ST_B) { - const int y = st.bLeft, z = st.bRight; - if (y < 0 || y >= M || z < 0 || z >= M) { continue; } - int kBeg2 = 0, kEnd2 = d; - if (st.splitKMax >= st.splitKMin) { kBeg2 = std::max(kBeg2, st.splitKMin); kEnd2 = std::min(kEnd2, st.splitKMax); } - if (st.splitRMax >= st.splitRMin) { kBeg2 = std::max(kBeg2, d - st.splitRMax); kEnd2 = std::min(kEnd2, d - st.splitRMin); } - double *dst2 = insSlice2 + static_cast(v) * vStride2; - for (int li = 0; li <= liMax2; ++li) { - if (fastLogsum) { - bool has = false; - float acc = NEG_INF_F2; - for (int k = kBeg2; k <= kEnd2; ++k) { - const double lv = insData2[static_cast(k) * dStride2 + static_cast(y) * vStride2 + static_cast(li)]; - const double rv = insData2[static_cast(d - k) * dStride2 + static_cast(z) * vStride2 + static_cast(li + k)]; - if (lv != NEG_INF && rv != NEG_INF) { log2AccFastAddF(static_cast(lv + rv), has, acc); } - } - dst2[li] = has ? static_cast(acc) : NEG_INF; - } else { - LogSumAccBase2Exact acc; - for (int k = kBeg2; k <= kEnd2; ++k) { - const double lv = insData2[static_cast(k) * dStride2 + static_cast(y) * vStride2 + static_cast(li)]; - const double rv = insData2[static_cast(d - k) * dStride2 + static_cast(z) * vStride2 + static_cast(li + k)]; - if (lv != NEG_INF && rv != NEG_INF) { acc.add(lv + rv); } - } - dst2[li] = acc.value(); - } - } - continue; - } - if (d < st.dConsume) { continue; } - const int nd2 = d - st.dConsume; - const float *ep = st.emitPtr; - const int8_t *sc = ws.seqCode.data(); - const double *nxtSlice2 = insDataAll2 + static_cast(nd2) * dStride2; - double *dst2 = insSlice2 + static_cast(v) * vStride2; - for (int li = 0; li <= liMax2; ++li) { - float ef = 0.0f; - if (st.consumesLeft && st.consumesRight) { - if (!(ep && st.emitSize >= 16)) { - ef = NEG_INF_F2; - } else { - const int ii = li + bestI; - const int lc = sc[ii]; - const int rc = sc[ii + d - 1]; - ef = (lc >= 0 && rc >= 0) ? ep[lc * 4 + rc] : -1.0f; - } - } else if (st.consumesLeft) { - if (!(ep && st.emitSize >= 4)) { - ef = NEG_INF_F2; - } else { - const int bi = sc[li + bestI]; - ef = (bi >= 0) ? ep[bi] : -1.0f; - } - } else if (st.consumesRight) { - if (!(ep && st.emitSize >= 4)) { - ef = NEG_INF_F2; - } else { - const int bi = sc[li + bestI + d - 1]; - ef = (bi >= 0) ? ep[bi] : -1.0f; - } - } - if (ef == NEG_INF_F2) { continue; } - const int nli = li + st.niShift; - if (fastLogsum) { - bool has = false; - float acc = NEG_INF_F2; - for (int t = 0; t < st.trCount; ++t) { - const size_t ti = st.trOff + static_cast(t); - const double n = nxtSlice2[static_cast(trDstPtr[ti]) * vStride2 + static_cast(nli)]; - if (n != NEG_INF) { log2AccFastAddF(ef + static_cast(trScPtr[ti]) + static_cast(n), has, acc); } - } - dst2[li] = has ? static_cast(acc) : NEG_INF; - } else { - LogSumAccBase2Exact acc; - for (int t = 0; t < st.trCount; ++t) { - const size_t ti = st.trOff + static_cast(t); - const double n = nxtSlice2[static_cast(trDstPtr[ti]) * vStride2 + static_cast(nli)]; - if (n != NEG_INF) { acc.add(static_cast(ef) + trScPtr[ti] + n); } - } - dst2[li] = acc.value(); - } - } - } - } - h.inside = insDataAll2[static_cast(localN2) * dStride2 + static_cast(model.rootState) * vStride2 + 0] - - bestNull3Corr - null2Corr; - h.bias = bestNull3Corr + null2Corr; - // Cell-diff vs Infernal cm_InsideAlign: dump Inside alpha[v][j][d] for all - // states v at the (j_target, d) cells specified by MMSEQS_CMSCAN_DUMP_INSIDE_J. - // j_target is in *envelope* coords (1..localN2). Run cmalign on the envelope - // sub-sequence to align j between our run and Infernal's. - const char *dumpInsJList = std::getenv("MMSEQS_CMSCAN_DUMP_INSIDE_J"); - if (dumpInsJList != NULL) { - std::vector insJTargets; - std::string s = dumpInsJList; - size_t pos = 0; - while (pos < s.size()) { - size_t comma = s.find(',', pos); - int jt = std::atoi(s.substr(pos, comma - pos).c_str()); - if (jt > 0 && jt <= localN2) insJTargets.push_back(jt); - if (comma == std::string::npos) break; - pos = comma + 1; - } - for (int jT : insJTargets) { - for (int d = 0; d <= jT; ++d) { - const int li = jT - d; - if (li < 0 || li > localN2) continue; - const double *insSlice = insDataAll2 + static_cast(d) * dStride2; - for (int v = 0; v < M; ++v) { - const double a = insSlice[static_cast(v) * vStride2 + static_cast(li)]; - if (a == NEG_INF) continue; - const ExactStateExec &st = exec[static_cast(v)]; - fprintf(stderr, "DUMP_INSIDE tid=%s j=%d d=%d v=%d type=%d alpha=%.6f\n", - seqId.c_str(), jT, d, v, - static_cast(st.type), - a); - } - } - } - } - } else { - h.inside = NEG_INF; - h.bias = bestNull3Corr + null2Corr; - } - outHits.push_back(h); -} - -// Per-CM-column max-marginal match emission table for peak-anchor. -// Index 1..clen. For ML/MR states: direct 4-vector. For MP states: -// max over partner residue (cheap marginalization that preserves the -// conserved-core peak). Built once per query. -static std::vector> buildPerColumnMaxEmissions(const InfernalExactModel &model) { - const int clen = model.clen; - std::vector> out(static_cast(clen + 1), {0.0f, 0.0f, 0.0f, 0.0f}); - std::vector have(static_cast(clen + 1), false); - for (size_t si = 0; si < model.states.size(); ++si) { - const CmState &s = model.states[si]; - if ((s.type == CM_ST_ML || s.type == CM_ST_MR) && s.emit.size() >= 4) { - const int pos = (s.type == CM_ST_ML) ? s.mapLeft : s.mapRight; - if (pos > 0 && pos <= clen) { - for (int a = 0; a < 4; ++a) { - const float v = static_cast(s.emit[static_cast(a)]); - if (!have[pos] || v > out[pos][a]) out[pos][a] = v; - } - have[pos] = true; - } - } else if (s.type == CM_ST_MP && s.emit.size() >= 16) { - if (s.mapLeft > 0 && s.mapLeft <= clen) { - for (int a = 0; a < 4; ++a) { - float row = -std::numeric_limits::infinity(); - for (int b = 0; b < 4; ++b) { - const float v = static_cast(s.emit[static_cast(a * 4 + b)]); - if (v > row) row = v; - } - if (!have[s.mapLeft] || row > out[s.mapLeft][a]) out[s.mapLeft][a] = row; - } - have[s.mapLeft] = true; - } - if (s.mapRight > 0 && s.mapRight <= clen) { - for (int b = 0; b < 4; ++b) { - float col = -std::numeric_limits::infinity(); - for (int a = 0; a < 4; ++a) { - const float v = static_cast(s.emit[static_cast(a * 4 + b)]); - if (v > col) col = v; - } - if (!have[s.mapRight] || col > out[s.mapRight][b]) out[s.mapRight][b] = col; - } - have[s.mapRight] = true; - } - } - } - return out; -} - -// Walk SW backtrace L→R, accumulate per-CM-column ML emissions on M ops, -// return full-target 0-indexed coord of cumulative-score argmax. With cmbuild -// --hand the CM column equals the 1-indexed query position. qStart is the -// 1-indexed query start from the m8 row; dbStart is 0-indexed full-target. -// Returns -1 on empty backtrace, no scored M ops, or all-N target window. -static int peakAnchorFromCigar(const std::string &bt, - int qStart, - int dbStart, - const std::vector> &emitCol, - int clen, - const std::string &fullTargetSeq) { - if (bt.empty() || clen <= 0) return -1; - int qPos = qStart; // 1-indexed CM column under --hand - int tPos = dbStart; // 0-indexed full-target coord - float cum = 0.0f; - float bestCum = -std::numeric_limits::infinity(); - int bestT = -1; - bool any = false; - const int tLen = static_cast(fullTargetSeq.size()); - for (size_t k = 0; k < bt.size(); ++k) { - const char op = bt[k]; - if (op == 'M') { - if (qPos >= 1 && qPos <= clen && tPos >= 0 && tPos < tLen) { - int a = -1; - switch (fullTargetSeq[static_cast(tPos)]) { - case 'A': case 'a': a = 0; break; - case 'C': case 'c': a = 1; break; - case 'G': case 'g': a = 2; break; - case 'U': case 'u': case 'T': case 't': a = 3; break; - default: break; - } - if (a >= 0) { - cum += emitCol[static_cast(qPos)][static_cast(a)]; - if (!any || cum > bestCum) { - bestCum = cum; - bestT = tPos; - } - any = true; - } - } - ++qPos; ++tPos; - } else if (op == 'I') { - ++qPos; - } else if (op == 'D') { - ++tPos; - } - } - return any ? bestT : -1; -} - -// Stage 8: re-align each hit's envelope sub-sequence using the truncation DP -// (J/L/R/T modes via runTrCYKInsideAlign + runTrCYKAlignT). Mirrors Infernal -// cmsearch's per-envelope re-alignment with cm_TrCYKInsideAlign + cm_tr_alignT. -// Replaces hit.traceStates and hit.cigar in place; keeps start1/end1 from the -// prefilter scan (envelope coords are unchanged — only the trace through them -// is upgraded from J-only to truncation-aware). -// -// Gated on MMSEQS_CMSCAN_TRUNC_DP=1 + MMSEQS_CMSCAN_LOCAL_MODE=1; also requires -// the truncation tables (m.trpL/lmesc/rmesc) to have been built upstream. -static void runTruncDpReAlignHits(const InfernalExactModel &model, - const std::string &seq, - std::vector &hits) { - if (!model.hasLocalCfg) return; - if (model.trpL.empty()) return; - const int N = static_cast(seq.size()); - if (N <= 0 || hits.empty()) return; - const char *envDump = std::getenv("MMSEQS_CMSCAN_TRUNC_REALIGN_DUMP"); - const bool dump = (envDump != NULL && envDump[0] == '1'); - // MMSEQS_CMSCAN_REALIGN_FULL=1: ignore prior CYK trim (h.start1/h.end1 - // from initial pass) and re-align over the FULL target sequence. Used by - // the regression test to make our DP comparable to Infernal cmalign which - // always runs over the full input. - static const char *const realignFullEnv = std::getenv("MMSEQS_CMSCAN_REALIGN_FULL"); - const bool realignFull = (realignFullEnv != nullptr && realignFullEnv[0] == '1'); - // MMSEQS_CMSCAN_TIMING=1 emits per-thread cumulative DP time at end of this - // hit batch. Used to A/B test optimization changes without bench-running. - static const char *const timingEnv = std::getenv("MMSEQS_CMSCAN_TIMING"); - const bool timing = (timingEnv != nullptr && timingEnv[0] == '1'); - long long dpUsTotal = 0; - int dpCalls = 0; - for (Hit &h : hits) { - int lo = std::min(h.start1, h.end1); - int hi = std::max(h.start1, h.end1); - if (realignFull) { lo = 1; hi = N; } - if (lo < 1 || hi > N || lo > hi) continue; - const int subLen = hi - lo + 1; - std::vector dsq(static_cast(subLen + 2), -1); - for (int p = 1; p <= subLen; ++p) { - const char c = seq[static_cast(lo - 1 + p - 1)]; - if (c=='A'||c=='a') dsq[(size_t)p] = 0; - else if (c=='C'||c=='c') dsq[(size_t)p] = 1; - else if (c=='G'||c=='g') dsq[(size_t)p] = 2; - else if (c=='T'||c=='t'||c=='U'||c=='u') dsq[(size_t)p] = 3; - else dsq[(size_t)p] = -1; - } - const int pty_idx = 0; // TRPENALTY_5P_AND_3P (most permissive) - // MMSEQS_CMSCAN_TRUNC_FORCE_J=1: force mode J only (mirror Infernal - // --notrunc behavior). Mode L/R/T silently drop right/left/both-arm - // states from the rendered MSA → forcing J keeps the parsetree dense - // and bench-stable. - static const char *const forceJEnv = std::getenv("MMSEQS_CMSCAN_TRUNC_FORCE_J"); - const char preset = (forceJEnv != nullptr && forceJEnv[0] == '1') ? 'J' : 'U'; - std::chrono::steady_clock::time_point _dp_t0; - if (timing) _dp_t0 = std::chrono::steady_clock::now(); - const TrCykResult &res = runTrCYKInsideAlign(model, dsq, subLen, preset, pty_idx); - if (timing) { - auto _dp_t1 = std::chrono::steady_clock::now(); - dpUsTotal += std::chrono::duration_cast(_dp_t1 - _dp_t0).count(); - ++dpCalls; - } - if (!std::isfinite(res.score)) continue; - TrParsetree tr = runTrCYKAlignT(model, res, subLen, pty_idx); - if (!tr.ok || tr.nodes.empty()) continue; - dumpTrParsetree(tr, model); // gated by MMSEQS_CMSCAN_DUMP_TRPT=1 - std::vector states = trParsetreeStates(tr); - std::vector modes; - modes.reserve(tr.nodes.size()); - for (const TrParseNode &n : tr.nodes) modes.push_back(n.mode); - int qS = -1, qE = -1, aL = 0, leadIns = 0, trailIns = 0; - std::string newCigar = modelTraceCigarQueryCoord(model, states, &qS, &qE, &aL, - &leadIns, &trailIns, &modes); - if (newCigar.empty() || newCigar == "NA") continue; - // EL pseudo-state (v == M) accounting. modelTraceCigarQueryCoord skips - // these nodes; their absorbed target residues need to be folded into - // leading/trailing inserts so the m8 t-coord trim shrinks the reported - // span to match CIGAR M+D consume. Without this, downstream - // result2dnamsa walks CIGAR over a wider t-window than CIGAR covers - // and mis-registers residues into wrong CM columns (78%-94% of rows - // in pre-fix wire-up). - // Mode-aware emit semantics (Infernal cm_dpalign_trunc.c): - // mode J: MP emits {emitl, emitr}; ML {emitl}; MR {emitr} - // mode L: MP {emitl}; ML {emitl}; MR silent - // mode R: MP {emitr}; ML silent; MR {emitr} - // mode T: same as J for emit accounting (truncations are at trace edges) - const int M_states = static_cast(model.states.size()); - int matchMin = std::numeric_limits::max(); - int matchMax = 0; - auto bumpMatch = [&](int pos) { - if (pos <= 0) return; - matchMin = std::min(matchMin, pos); - matchMax = std::max(matchMax, pos); - }; - for (const TrParseNode &n : tr.nodes) { - if (n.v < 0 || n.v >= M_states) continue; - const CmStateType st = model.states[(size_t)n.v].type; - const bool emitsLeft = (st == CM_ST_MP || st == CM_ST_ML) - ? (n.mode == TR_TRMODE_J || n.mode == TR_TRMODE_L - || n.mode == TR_TRMODE_T) - : false; - const bool emitsRight = (st == CM_ST_MP || st == CM_ST_MR) - ? (n.mode == TR_TRMODE_J || n.mode == TR_TRMODE_R - || n.mode == TR_TRMODE_T) - : false; - if (emitsLeft) bumpMatch(n.emitl); - if (emitsRight) bumpMatch(n.emitr); - } - int extraLead = 0, extraTrail = 0, middleEL = 0; - for (const TrParseNode &n : tr.nodes) { - if (n.v != M_states) continue; - const int absorbed = std::max(0, n.emitr - n.emitl + 1); - if (absorbed == 0) continue; - if (matchMin == std::numeric_limits::max()) { - extraTrail += absorbed; - } else if (n.emitr < matchMin) { - extraLead += absorbed; - } else if (n.emitl > matchMax) { - extraTrail += absorbed; - } else { - // Middle/bifurcation EL: residues absorbed between matches, - // shrinking either end of t-span chops the wrong residues. - // Skip — span will exceed CIGAR M+D for these (renderer - // under-walks but doesn't overrun). - middleEL += absorbed; - } - } - leadIns += extraLead; - trailIns += extraTrail; - if (dump) { - std::fprintf(stderr, - "[TRUNC_REALIGN] %s lo=%d hi=%d L=%d mode=%c b=%d score=%.4f trpenalty=%.4f nodes=%zu el_lead=%d el_trail=%d\n", - h.seqId.c_str(), lo, hi, subLen, - res.mode, res.b, res.score, tr.trpenalty, tr.nodes.size(), - extraLead, extraTrail); - } - h.traceStates = encodeTraceStates(states); - h.cigar = newCigar; - h.qStart = qS; - h.qEnd = qE; - h.cigarAlnLen = static_cast(std::max(0, aL)); - h.leadingInsertTargets = leadIns; - h.trailingInsertTargets = trailIns; - h.mode = res.mode; - h.trunc = (res.mode != 'J'); - h.cyk = res.score; - } - if (timing && dpCalls > 0) { - Debug(Debug::INFO) << "TRUNC_DP timing: " << dpCalls << " envelopes, " - << dpUsTotal << " us total, " - << (double)dpUsTotal / (double)dpCalls << " us/env\n"; - } -} - -// Per-envelope CYK re-scan wrapper. When MMSEQS_CMSCAN_RESCORE_ENVELOPE=1, -// runs CYK twice: first to identify peak (i,j), then re-runs on a sub-sequence -// of [max(1, i-pad) .. min(N, j+pad)] which mirrors Infernal's dispatch#2 behavior -// (envelope-restricted CYK that yields ~+60 bits at the same logical cell on -// 2YGH_1 KJ798010.1). Pad defaults to 5; override with MMSEQS_CMSCAN_RESCORE_PAD. -static void runInfernalExactScanWithEnvelopeRescore(const InfernalExactModel &model, - const std::string &seq, - bool wantInside, - std::vector &outHits, - const std::string &seqId, - int maxHitLen = 0, - int forcedI = -1, - int forcedD = -1, - int anchorI = -1, - int anchorD = -1) { - static int rescoreEnabled = -1; - static int rescorePad = -1; - static int rescanQdbOff = -1; - if (rescoreEnabled == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_RESCORE_ENVELOPE"); - rescoreEnabled = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - if (rescorePad == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_RESCORE_PAD"); - rescorePad = (env != NULL) ? std::atoi(env) : 5; - if (rescorePad < 0) rescorePad = 0; - } - if (rescanQdbOff == -1) { - // MMSEQS_CMSCAN_RESCAN_QDB_OFF=1: pass 1 keeps QDB, pass 2 forces QDB off. - // Mirrors LOCAL=0+DISABLE_QDB lift (project_qdboff_2YGH_1_native_beats_shellout) - // without the broken-envelope regressions on 3Q1Q_3 / 5CCB_3 (per - // project_qdboff_does_not_generalize). - const char *env = std::getenv("MMSEQS_CMSCAN_RESCAN_QDB_OFF"); - rescanQdbOff = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - - static const char *const _wrapTimingEnv = std::getenv("MMSEQS_CMSCAN_TIMING"); - const bool _wrapTiming = (_wrapTimingEnv != nullptr && _wrapTimingEnv[0] == '1'); - // MMSEQS_CMSCAN_SKIP_TRUNC_REALIGN=1: skip the post-scan TRUNC_DP realign step. - // Under FORCE_J=1 the realign re-runs the same J-mode CYK that the initial scan - // already produced a trace for — pure duplicate work. Skipping is safe when the - // scan's CIGAR is acceptable for downstream consumers (it always is for R-scape - // .sto rendering via result2dnamsa). Output drops L/R/T-mode hits, but FORCE_J=1 - // never produces those anyway. - static const char *const _skipTruncRealignEnv = std::getenv("MMSEQS_CMSCAN_SKIP_TRUNC_REALIGN"); - const bool skipTruncRealign = (_skipTruncRealignEnv != nullptr && _skipTruncRealignEnv[0] == '1'); - if (rescoreEnabled == 0) { - std::chrono::steady_clock::time_point _t0, _t1, _t2; - if (_wrapTiming) _t0 = std::chrono::steady_clock::now(); - runInfernalExactScan(model, seq, wantInside, outHits, seqId, maxHitLen, forcedI, forcedD, - anchorI, anchorD); - if (_wrapTiming) _t1 = std::chrono::steady_clock::now(); - if (!skipTruncRealign) { - runTruncDpReAlignHits(model, seq, outHits); - } - if (_wrapTiming) { - _t2 = std::chrono::steady_clock::now(); - long long us_scan = std::chrono::duration_cast(_t1 - _t0).count(); - long long us_dp = std::chrono::duration_cast(_t2 - _t1).count(); - Debug(Debug::INFO) << "WRAP timing: N=" << seq.size() << " hits=" << outHits.size() - << " scan=" << us_scan << "us dp=" << us_dp << "us\n"; - } - return; - } + return out; +} - // First pass: locate envelope peak with a CYK-only scan (faster than Inside). - // When rescanQdbOff is set, force QDB ON for pass 1 (overriding any DISABLE_QDB - // env var) so envelopes come from the QDB-banded path, then force QDB OFF for - // pass 2 below for bit-exact alignment on the sub-sequence. - std::vector probeHits; - const int pass1DisableQdb = rescanQdbOff ? 0 : -1; - runInfernalExactScan(model, seq, /*wantInside=*/false, probeHits, seqId, maxHitLen, forcedI, forcedD, - anchorI, anchorD, pass1DisableQdb); - if (probeHits.empty()) { - outHits = std::move(probeHits); - return; +FastaSeq decodeOneSequence(DBReader &dbr, size_t id, BaseMatrix &subMat, + Sequence &seqObj, bool useDinucMapping, bool isGpuDb, + unsigned int thread_idx) { + FastaSeq cur; + cur.key = dbr.getDbKey(id); + if (dbr.getLookupSize() > 0) { + const size_t lid = dbr.getLookupIdByKey(cur.key); + cur.id = dbr.getLookupEntryName(lid); + } else { + cur.id = std::to_string(cur.key); } - - const int N = static_cast(seq.size()); - for (Hit &probe : probeHits) { - const int lo = std::min(probe.start1, probe.end1); - const int hi = std::max(probe.start1, probe.end1); - const int wi = std::max(1, lo - rescorePad); - const int wj = std::min(N, hi + rescorePad); - if (wi >= wj) { - outHits.push_back(std::move(probe)); - continue; - } - const int subLen = wj - wi + 1; - std::string subSeq = seq.substr(static_cast(wi - 1), static_cast(subLen)); - std::vector rescoreHits; - const int pass2DisableQdb = rescanQdbOff ? 1 : -1; - runInfernalExactScan(model, subSeq, wantInside, rescoreHits, seqId, maxHitLen, - /*forcedI=*/-1, /*forcedD=*/-1, - /*anchorI=*/-1, /*anchorD=*/-1, - pass2DisableQdb); - if (rescoreHits.empty()) { - outHits.push_back(std::move(probe)); - continue; - } - // Sub-seq positions [1..subLen] map back to absolute [wi..wj]. - Hit &best = rescoreHits.front(); - best.start1 += (wi - 1); - best.end1 += (wi - 1); - outHits.push_back(std::move(best)); + const size_t seqLen = dbr.getSeqLen(id); + if (isGpuDb) { + const unsigned char *data = reinterpret_cast(dbr.getDataUncompressed(id)); + seqObj.mapSequence(id, cur.key, std::make_pair(data, seqLen)); + } else { + const char *data = dbr.getData(id, thread_idx); + seqObj.mapSequence(id, cur.key, data, seqLen); } - runTruncDpReAlignHits(model, seq, outHits); + const Sequence::SeqAuxInfo *auxInfo = Sequence::getAuxInfo(seqObj.getSeqType()); + const unsigned char *num2outputnum = (useDinucMapping && auxInfo != NULL) ? auxInfo->num2outputnum : NULL; + cur.seq = decodeMappedSequenceToRna(seqObj, subMat, num2outputnum); + return cur; } +bool hasDbIndex(const std::string &path) { + return FileUtil::fileExists((path + ".index").c_str()); +} -} // namespace - -int cmscan(int argc, const char **argv, const Command &command) { - MMseqsMPI::init(argc, argv); - LocalParameters &par = LocalParameters::getLocalInstance(); - par.parseParameters(argc, argv, command, true, 0, MMseqsParameter::COMMAND_ALIGN); +bool looksLikeInfernalCm(const std::string &path) { + std::ifstream in(path.c_str()); + if (!in.good()) return false; + std::string line; + while (std::getline(in, line)) { + const std::string t = trim(line); + if (t.empty()) continue; + return t.rfind("INFERNAL", 0) == 0 || t == "CM" || t.rfind("NAME", 0) == 0; + } + return false; +} + +struct InfModel { + // shared read-only across threads (immutable) + ESL_ALPHABET *abc = NULL; + // configured template; per-thread clones own their scratch + CM_t *cm = NULL; + int clen = 0; + bool valid = false; + // scan/alignment modes Inside vs CYK + bool useInside = true; + // flags for tcm->align_opts + int alignOpts = CM_ALIGN_NONBANDED | CM_ALIGN_CYK; + // HMM-banded? (alidisplay tau) + bool alignBanded = false; +}; - if (MMseqsMPI::isMaster() == false) { - return EXIT_SUCCESS; +// Load an INFERNAL1/a CM from in-memory text and configure it for scannin non-banded alignment +InfModel loadModel(const std::string &cmtext, bool useInside, int alignOpts, bool alignBanded, bool useLocal) { + InfModel m; + m.useInside = useInside; + m.alignOpts = alignOpts; + m.alignBanded = alignBanded; + std::vector buf(cmtext.begin(), cmtext.end()); + buf.push_back('\0'); + char errbuf[eslERRBUFSIZE]; + errbuf[0] = '\0'; + + CM_FILE *cmfp = NULL; + if (cm_file_OpenBuffer(buf.data(), static_cast(cmtext.size()), FALSE, &cmfp) != eslOK) { + Debug(Debug::ERROR) << "cmscan: cm_file_OpenBuffer failed\n"; + return m; } - - const char *subcmd = (command.cmd != NULL) ? command.cmd : "cmsearch"; - Debug(Debug::INFO) << "Running in-tree CM dynamic programming (" << subcmd << ", full DP, no heuristics)\n"; - - struct QueryModel { - unsigned int key; - InfernalExactModel exactModel; - }; - - // For DB input, keep reader open and parse CMs lazily to avoid OOM - // For single-file input, load immediately - struct QueryModelRef { - unsigned int key; - size_t dbIdx; // index into cmReader, or SIZE_MAX for single-file - }; - std::vector queryRefs; - DBReader *cmReader = NULL; - QueryModel singleModel; // only used for single-file case - if (hasDbIndex(par.db1)) { - const std::string cmIndex = par.db1 + ".index"; - cmReader = new DBReader(par.db1.c_str(), - cmIndex.c_str(), - par.threads > 0 ? par.threads : 1, - DBReader::USE_DATA | DBReader::USE_INDEX); - cmReader->open(DBReader::NOSORT); - queryRefs.reserve(cmReader->getSize()); - for (size_t i = 0; i < cmReader->getSize(); ++i) { - QueryModelRef ref; - ref.key = cmReader->getDbKey(i); - ref.dbIdx = i; - queryRefs.push_back(ref); - } - Debug(Debug::INFO) << "CM database: " << queryRefs.size() << " models (lazy loading)\n"; - } else { - if (looksLikeInfernalCm(par.db1) == false) { - Debug(Debug::ERROR) << "cmscan requires an Infernal CM (run cmbuild first): " << par.db1 << "\n"; - return EXIT_FAILURE; - } - Debug(Debug::INFO) << "Loading Infernal CM: " << par.db1 << "\n"; - singleModel.key = 0; - singleModel.exactModel = parseInfernalCmExactModel(par.db1); - QueryModelRef ref; - ref.key = 0; - ref.dbIdx = SIZE_MAX; - queryRefs.push_back(ref); + // null so close is safe + cmfp->hfp = NULL; + if (cm_file_Read(cmfp, FALSE, &m.abc, &m.cm) != eslOK || m.cm == NULL) { + Debug(Debug::ERROR) << "cmscan: cm_file_Read failed: " << cmfp->errbuf << "\n"; + cm_file_Close(cmfp); + return m; } - - const size_t nThreads = (par.threads > 0) ? static_cast(par.threads) : 1u; - - const std::string seqDbPath = par.db2; - const std::string seqDbIndex = par.db2 + ".index"; - DBReader seqDbr(seqDbPath.c_str(), - seqDbIndex.c_str(), - static_cast(nThreads), - DBReader::USE_DATA - | DBReader::USE_INDEX - | DBReader::USE_LOOKUP); - seqDbr.open(DBReader::NOSORT); - const unsigned int seqExt = DBReader::getExtendedDbtype(seqDbr.getDbtype()); - const bool seqGpuDb = (seqExt & Parameters::DBTYPE_EXTENDED_GPU) != 0; - const bool decodeSeqDinuc = ((seqExt & Parameters::DBTYPE_EXTENDED_DINUCLEOTIDE) != 0) || seqGpuDb; - const int seqDecodeType = effectiveDecodeSeqType(seqDbr.getDbtype(), decodeSeqDinuc); - NucleotideMatrix nucMat(Parameters::getInstance().scoringMatrixFile.values.nucleotide().c_str(), 1.0, 0.0); - BaseMatrix &subMat = static_cast(nucMat); - - const size_t observedDbResidues = seqDbr.getAminoAcidDBSize(); - const double strandMult = (par.strand == 2) ? 2.0 : 1.0; - const double targetDbResidues = (par.dbSize > 0) - ? static_cast(par.dbSize) - : (static_cast(observedDbResidues) * strandMult); - const bool wantInsideUser = (par.cmMode == LocalParameters::CM_MODE_INSIDE); - - // Open the result DB like any MMseqs2 result consumer: stream per-query - // payloads on demand via thread_idx'd getData; no upfront parsing. - DBReader resultReader(par.db3.c_str(), - (par.db3 + ".index").c_str(), - static_cast(nThreads), - DBReader::USE_DATA - | DBReader::USE_INDEX); - resultReader.open(DBReader::NOSORT); - - const float cmRegionFlanking = par.cmRegionFlanking; - Debug(Debug::INFO) << subcmd << " output DB: " << par.db4 << "\n"; - DBWriter resultWriter(par.db4.c_str(), - par.db4Index.c_str(), - static_cast(nThreads), - par.compressed, - Parameters::DBTYPE_ALIGNMENT_RES); - resultWriter.open(); - - if (cmFastMathEnabled()) { - Debug(Debug::WARNING) << "MMSEQS_CMSCAN_FASTMATH=1 enabled: using approximate log/exp in Inside DP (scores may drift)\n"; + cm_file_Close(cmfp); + m.cm->config_opts |= CM_CONFIG_SCANMX | CM_CONFIG_NONBANDEDMX; + // glocal by default; local adds CM local begins/ends + matching CP9 (HMM-banded aln) local config + if (useLocal) m.cm->config_opts |= CM_CONFIG_LOCAL | CM_CONFIG_HMMLOCAL | CM_CONFIG_HMMEL; + if (useInside) m.cm->search_opts |= CM_SEARCH_INSIDE; // else CYK scan (flag unset) + if (cm_Configure(m.cm, errbuf, -1) != eslOK) { + Debug(Debug::ERROR) << "cmscan: cm_Configure failed: " << errbuf << "\n"; + FreeCM(m.cm); m.cm = NULL; + esl_alphabet_Destroy(m.abc); m.abc = NULL; + return m; } + m.clen = m.cm->clen; + m.valid = true; + return m; +} - size_t totalHits = 0; - char buffer[1024 + 32768 * 4]; - // Outer per-query loop is serial; inner parallelism is over candidate - // target lines (Phase 2 below). The 9-query rRNA bench has only 1 query, - // so per-query parallelism wastes 15/16 threads on the slow CYK step. - for (long qi = 0; qi < static_cast(queryRefs.size()); ++qi) { - const QueryModelRef &ref = queryRefs[qi]; - - // Load CM lazily, in-memory: parse the DB entry directly via istringstream. - QueryModel qm; - if (ref.dbIdx != SIZE_MAX && cmReader != NULL) { - qm.key = ref.key; - const char *raw = cmReader->getData(ref.dbIdx, 0); - size_t len = cmReader->getEntryLen(ref.dbIdx); - std::string text(raw, len); - const size_t nul = text.find('\0'); - if (nul != std::string::npos) { - text.resize(nul); - } - std::istringstream iss(text); - const std::string srcLabel = "cm-db-entry[" + std::to_string(qm.key) + "]"; - qm.exactModel = parseInfernalCmExactModelFromStream(iss, srcLabel); - } else { - qm = singleModel; - } +void freeModel(InfModel &m) { + if (m.cm != NULL) { FreeCM(m.cm); m.cm = NULL; } + if (m.abc != NULL) { esl_alphabet_Destroy(m.abc); m.abc = NULL; } + m.valid = false; +} - // Infernal default: Inside score is the primary ranking signal; - // CYK remains the trace source. `wantInsideUser` only changes which - // score we report, not whether Inside is computed. - const bool promoteInsideToPrimary = !wantInsideUser; - // MMSEQS_CMSCAN_SKIP_INSIDE=1: skip the Inside DP entirely (CYK still - // runs as the trace source). For pipelines that don't consume h.inside - // (the bench's permissive -e 10000 path doesn't), this halves cmsearch - // CPU time. Verify byte-identical aln output before relying on it. - static const char *const _skipInsideEnv = std::getenv("MMSEQS_CMSCAN_SKIP_INSIDE"); - const bool wantInside = !(_skipInsideEnv != nullptr && _skipInsideEnv[0] == '1'); +// Per-region hit +struct InfHit { + bool valid = false; + // Inside score, null3-corrected + double score = -std::numeric_limits::infinity(); + // window 1-based target coord of first MATCH residue + int start1 = 0; + // window 1-based target coord of last MATCH residue + int end1 = 0; + // 0-based first consensus column (cfrom_emit-1) + int qStart = -1; + // 0-based last consensus column (cto_emit-1) + int qEnd = -1; + // I=target-consume, D=query-consume, over [firstM..lastM] + std::string cigar = "NA"; + int cigarAlnLen = 0; + float seqId = -1.0f; + unsigned int dbKey = 0; + unsigned int dbLen = 0; +}; - // Slack for the CYK d-cap. We take the max of: - // - 3x the RNA search prefilter envelope length (local extension) - // - 1.5x the CM consensus length (global: a true CM hit can stretch - // well beyond the prefilter envelope, especially for structured - // RNAs where iter-3 msa-profile mass is concentrated around the - // center but the CM consensus spans the full rRNA). - // model.w (Infernal's W) remains the hard upper bound on the CM side. - static constexpr int CM_MAXSPAN_ENV_SLACK = 3; - static constexpr double CM_MAXSPAN_CLEN_SLACK = 1.5; - const int clenFloor = static_cast(qm.exactModel.clen * CM_MAXSPAN_CLEN_SLACK); +// scan one strand-oriented window with Infernal +InfHit scanRegionInfernal(const InfModel &model, CM_t *tcm, const std::string ®ionSeq) { + InfHit h; + const int Lwin = static_cast(regionSeq.size()); + if (Lwin <= 0) return h; - // Peak-anchor (MMSEQS_CMSCAN_ANCHOR_MODE=peak): rescore SW backtrace - // from the prefilter results table with CM match emissions; argmax-j - // of the cumulative bit-score becomes a point anchor for FORCE_GLOBAL=3 - // instead of the geometric midpoint of the prefilter envelope. Only - // used when forceGlobal in {3,4} and a backtrace is present in the - // prefilter row. Built once per query (model-only; cand-independent). - // peakAnchorMode: 0=off, 1=peak (point anchor at SW-CIGAR bit-score peak), - // 2=peak_walk (anchor span = SW match extended naively - // leftward to CM col 1 and rightward to CM col CLEN; FG=3 - // then straddles the midpoint of that span). The CIGAR - // itself isn't needed for peak_walk — only qStart/qEnd - // and clen — but we keep the same gating as peak (skip - // reverse-strand for v1). - static int peakAnchorMode = -1; - if (peakAnchorMode == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_ANCHOR_MODE"); - if (env != NULL) { - if (std::string(env) == "peak") peakAnchorMode = 1; - else if (std::string(env) == "peak_walk") peakAnchorMode = 2; - else peakAnchorMode = 0; - } else { - peakAnchorMode = 0; - } - } - std::vector> perColMaxEmit; - if (peakAnchorMode == 1) { - perColMaxEmit = buildPerColumnMaxEmissions(qm.exactModel); - } + char errbuf[eslERRBUFSIZE]; + errbuf[0] = '\0'; - std::vector hits; - hits.reserve(1024); + ESL_DSQ *dsq = NULL; + if (esl_abc_CreateDsq(model.abc, regionSeq.c_str(), &dsq) != eslOK || dsq == NULL) { + return h; + } - // Finalize each scanned hit: remap strand-specific coords to forward, - // promote Inside -> primary score when Infernal-default, compute - // evalue, and (forward only, with backtrace) compute trace-based pid. - auto finalizeHit = [&](Hit &h, int strand, const std::string &targetFullSeq, - unsigned int tKey, unsigned int fullLen, - int offset, int regionLen) { - h.dbKey = tKey; - h.dbLen = fullLen; - if (strand > 0) { - if (offset > 0) { - h.start1 += offset; - h.end1 += offset; - } - } else { - // revcomp pos p corresponds to forward pos (regionLen - p + 1); - // Infernal convention: start1 > end1 indicates minus strand. - int fwdStart = regionLen - h.start1 + 1 + offset; - int fwdEnd = regionLen - h.end1 + 1 + offset; - h.start1 = fwdStart; - h.end1 = fwdEnd; - } - if (promoteInsideToPrimary && h.inside != NEG_INF) { - h.cyk = h.inside; - } - double ev = 0.0; - if (infernalExactScoreToEvalue(qm.exactModel, /*evalueModeInside=*/true, h.mode, h.cyk, targetDbResidues, ev)) { - h.evalue = ev; - h.hasEvalue = true; - } - const bool hasBacktrace = (!h.cigar.empty() && h.cigar != "NA"); - if (hasBacktrace - && !h.traceStates.empty() && h.traceStates != "NA" - && h.start1 > 0 && h.end1 >= h.start1) { - const size_t s0 = static_cast(h.start1 - 1); - const size_t slen = static_cast(h.end1 - h.start1 + 1); - if (s0 < targetFullSeq.size()) { - const std::string obs = targetFullSeq.substr(s0, std::min(slen, targetFullSeq.size() - s0)); - const std::vector trace = decodeTraceStates(h.traceStates); - const std::string cons = modelTraceConsensusForCigar(qm.exactModel, trace); - h.precomputedSeqId = seqIdFromCigarConsensus(h.cigar, cons, obs); - } - } - }; + CM_TOPHITS *th = cm_tophits_Create(); + float sc = 0.0f; + int scanStatus; + if (model.useInside) { + scanStatus = FastIInsideScan( + tcm, errbuf, tcm->smx, SMX_QDB1_TIGHT, dsq, + 1, Lwin, 0.0f, th, TRUE /*do_null3*/, 0.0f, NULL, NULL, NULL, &sc + ); + } else { + scanStatus = FastCYKScan( + tcm, errbuf, tcm->smx, SMX_QDB1_TIGHT, dsq, + 1, Lwin, 0.0f, th, TRUE /*do_null3*/, 0.0f, NULL, NULL, NULL, &sc + ); + } + if (scanStatus != eslOK || th->N == 0) { + cm_tophits_Destroy(th); + free(dsq); + return h; + } + + // best hit by score + CM_HIT *best = NULL; + for (uint64_t i = 0; i < th->N; ++i) { + CM_HIT *hh = &th->unsrt[i]; + if (best == NULL || hh->score > best->score) best = hh; + } + if (best == NULL || best->start < 1 || best->stop > Lwin || best->stop < best->start) { + cm_tophits_Destroy(th); + free(dsq); + return h; + } + const double hitScore = best->score; + const int64_t hStart = best->start; + const int64_t hStop = best->stop; + const char hMode = (char) best->mode; + + // non-banded CYK align of the best hit + ESL_SQ *winSq = esl_sq_CreateDigitalFrom(model.abc, "win", dsq, Lwin, NULL, NULL, NULL); + ESL_SQ *sq2aln = esl_sq_CreateDigitalFrom(model.abc, "hit", dsq + hStart - 1, + hStop - hStart + 1, NULL, NULL, NULL); + tcm->align_opts = model.alignOpts; + ESL_STOPWATCH *watch = esl_stopwatch_Create(); + esl_stopwatch_Start(watch); + CM_ALNDATA *adata = NULL; + const float mxsize = 8192.0f; + const int alnStatus = DispatchSqAlignment(tcm, errbuf, sq2aln, -1, mxsize, hMode, + PLI_PASS_STD_ANY, FALSE /*cp9b invalid*/, + NULL, NULL, NULL, &adata); + esl_stopwatch_Stop(watch); + if (alnStatus != eslOK || adata == NULL) { + if (adata != NULL) cm_alndata_Destroy(adata, FALSE); + esl_sq_Destroy(sq2aln); esl_sq_Destroy(winSq); esl_stopwatch_Destroy(watch); + cm_tophits_Destroy(th); free(dsq); + Debug(Debug::WARNING) << "cmscan: alignment failed, skipping hit\n"; + return h; + } + + CM_ALIDISPLAY *ad = NULL; + const int adStatus = cm_alidisplay_Create(tcm, errbuf, adata, winSq, hStart, + model.alignBanded ? tcm->tau : -1.0, watch->elapsed, &ad); + if (adStatus != eslOK || ad == NULL) { + if (ad != NULL) cm_alidisplay_Destroy(ad); + cm_alndata_Destroy(adata, FALSE); + esl_sq_Destroy(sq2aln); esl_sq_Destroy(winSq); esl_stopwatch_Destroy(watch); + cm_tophits_Destroy(th); free(dsq); + Debug(Debug::WARNING) << "cmscan: alidisplay failed, skipping hit\n"; + return h; + } + + const int N = ad->N; + std::vector ops; + // window 1-based coord of the residue consumed (M/I); -1 for D + std::vector targetCoord; + ops.reserve(N); + targetCoord.reserve(N); + long curTarget = static_cast(ad->sqfrom) - 1; + int idCount = 0, idDenom = 0; + for (int k = 0; k < N; ++k) { + const char mc = ad->model[k]; + const char ac = ad->aseq[k]; + const bool modelCons = (std::isalpha(static_cast(mc)) != 0); + const bool aseqRes = (std::isalpha(static_cast(ac)) != 0); + char op = '\0'; + if (modelCons && aseqRes) op = 'M'; + else if (!modelCons && aseqRes) op = 'I'; // target residue, no consensus + else if (modelCons && !aseqRes) op = 'D'; // consensus, no target residue + if (op == '\0') continue; + long coord = -1; + if (op == 'M' || op == 'I') { ++curTarget; coord = curTarget; } + ops.push_back(op); + targetCoord.push_back(coord); + } + // trim to the first/last MATCH column so the reported span starts/ends on M + int firstM = -1, lastM = -1; + for (size_t k = 0; k < ops.size(); ++k) { + if (ops[k] == 'M') { if (firstM < 0) firstM = static_cast(k); lastM = static_cast(k); } + } + if (firstM >= 0) { + std::string internal; + internal.reserve(lastM - firstM + 1); + for (int k = firstM; k <= lastM; ++k) internal.push_back(ops[k]); + // identity/seqId: re-walk the alidisplay columns + { + int opIdx = -1; + for (int k = 0; k < N; ++k) { + const char mc = ad->model[k]; + const char ac = ad->aseq[k]; + const bool modelCons = (std::isalpha(static_cast(mc)) != 0); + const bool aseqRes = (std::isalpha(static_cast(ac)) != 0); + char op = '\0'; + if (modelCons && aseqRes) op = 'M'; + else if (!modelCons && aseqRes) op = 'I'; + else if (modelCons && !aseqRes) op = 'D'; + if (op == '\0') continue; + ++opIdx; + if (opIdx < firstM || opIdx > lastM) continue; + ++idDenom; + if (op == 'M') { + const char a = normalizeBase(mc); + const char b = normalizeBase(ac); + if (a == b && a != 'N') ++idCount; + } + } + } + h.cigar = rleTraceOps(internal); + h.cigarAlnLen = static_cast(internal.size()); + h.start1 = static_cast(targetCoord[firstM]); + h.end1 = static_cast(targetCoord[lastM]); + // Query coords must match the cigar + int qConsBefore = 0; + for (int k = 0; k < firstM; ++k) { + if (ops[k] == 'M' || ops[k] == 'D') { + ++qConsBefore; + } + } + int qConsSpan = 0; + for (int k = firstM; k <= lastM; ++k) { + if (ops[k] == 'M' || ops[k] == 'D') { + ++qConsSpan; + } + } + h.qStart = qConsBefore; + h.qEnd = qConsBefore + qConsSpan - 1; + h.seqId = (idDenom > 0) ? static_cast(idCount) / static_cast(idDenom) : -1.0f; + h.score = hitScore; + h.valid = true; + } + + cm_alidisplay_Destroy(ad); + cm_alndata_Destroy(adata, FALSE); + esl_sq_Destroy(sq2aln); + esl_sq_Destroy(winSq); + esl_stopwatch_Destroy(watch); + cm_tophits_Destroy(th); + free(dsq); + return h; +} + +// Coordinate remap window-local to absolute target coords +void finalizeHit(InfHit &h, int strand, unsigned int tKey, unsigned int fullLen, + int offset, int regionLen) { + h.dbKey = tKey; + h.dbLen = fullLen; + if (strand > 0) { + if (offset > 0) { h.start1 += offset; h.end1 += offset; } + } else { + const int fwdStart = regionLen - h.start1 + 1 + offset; + const int fwdEnd = regionLen - h.end1 + 1 + offset; + h.start1 = fwdStart; + h.end1 = fwdEnd; + } +} + +// Full per-query scan pipeline: parse cmText -> loadModel -> gather candidates -> +// scan -> write alignments. Shared by cmscan (cmText from CM DB) and cmbuildscan +// (cmText built in-memory). Identical quantization: same cmText -> same cm_file parse. +// Wrap an already-built, already-scan-configured CM (from the direct build path) as +// an InfModel, no ASCII parse. abc stays alive via cm->abc; freeModel frees both. +static InfModel makeModelFromCM(CM_t *cm, bool useInside, int alignOpts, bool alignBanded) { + InfModel m; + m.cm = cm; m.abc = (cm != NULL) ? const_cast(cm->abc) : NULL; m.clen = (cm != NULL) ? cm->clen : 0; + m.useInside = useInside; m.alignOpts = alignOpts; m.alignBanded = alignBanded; + m.valid = (cm != NULL); + return m; +} - // Phase 1: walk the per-query candidate list once (sequential), - // parsing prefilter coords and deduping by target key. This is cheap - // (just integer parsing + hash insert) so single-threaded is fine. - // MMSEQS_CMSCAN_ALLOW_MULTIHIT=1: skip the per-target dedup so multiple - // non-overlapping prefilter hits per target sequence each get realigned - // and emitted (mirrors Infernal cmalign behavior). Combined with a - // multi-hit prefilter, this can substantially raise R-scape AUC by - // adding more independent co-evolution evidence per target. - static const char *const allowMultiHitEnv = std::getenv("MMSEQS_CMSCAN_ALLOW_MULTIHIT"); - const bool allowMultiHit = (allowMultiHitEnv != nullptr && allowMultiHitEnv[0] == '1'); +static void processQueryModel(LocalParameters &par, const InfModel &model, + unsigned int queryKey, + DBReader &seqDbr, DBReader &resultReader, + DBWriter &resultWriter, BaseMatrix &subMat, NucleotideMatrix &nucMat, + int seqDecodeType, bool seqGpuDb, bool decodeSeqDinuc, size_t nThreads) { + (void) nucMat; + const unsigned int modelLen = static_cast(std::max(0, model.clen)); + const int W = model.cm->W; // CM max hit length (Infernal band setup) + // --cm-region sizes the CM alignment window: the model is aligned to the + // prefilter hit region [dbStart,dbEnd] widened by (regionFlank * W) nt on + // EACH side. 1.0 (default) = pad by a full W (a hit can't reach further); + // <1 tightens the window (faster, may clip edge hits); <=0 = no window + // (align the whole target). + const float regionFlank = par.cmRegionFlanking; + + // Gather prefilter candidate regions for this query, first-per-target. struct Cand { unsigned int tKey; - int dbStart; - int dbEnd; - int qStart; - int qEnd; - int qLen = 0; - bool hasRegionCoord; - bool prefilterIsRev; - std::string backtrace; // SW per-char M/I/D from m8 col 10, if present + int dbStart = 0, dbEnd = 0, qStart = 0, qEnd = 0, qLen = 0; + bool hasRegionCoord = false; + bool prefilterIsRev = false; }; std::vector cands; - cands.reserve(1024); std::unordered_set seen; - - const size_t rid = resultReader.getId(qm.key); + const size_t rid = resultReader.getId(queryKey); if (rid != UINT_MAX) { char *data = resultReader.getData(rid, 0); - while (*data != '\0') { - while (*data == ' ' || *data == '\t') { - ++data; - } - // Format: targetKey score seqId evalue qStart qEnd qLen dbStart dbEnd dbLen [backtrace] + while (data != NULL && *data != '\0') { + while (*data == ' ' || *data == '\t') { ++data; } const char *lineStart = data; char *endptr = NULL; const unsigned long k = std::strtoul(data, &endptr, 10); @@ -6915,14 +496,11 @@ int cmscan(int argc, const char **argv, const Command &command) { continue; } const unsigned int tKey = static_cast(k); - // Dedup: only score first occurrence (best hit per target), - // unless MMSEQS_CMSCAN_ALLOW_MULTIHIT=1. - if (!allowMultiHit && seen.insert(tKey).second == false) { + if (seen.insert(tKey).second == false) { data = Util::skipLine(data); continue; } - - Cand cand{}; + Cand cand; cand.tKey = tKey; if (*endptr == '\t') { const char *p = endptr; @@ -6930,10 +508,7 @@ int cmscan(int argc, const char **argv, const Command &command) { const char *colStart[12] = {NULL}; colStart[0] = lineStart; while (*p != '\n' && *p != '\0' && col < 11) { - if (*p == '\t') { - colStart[col] = p + 1; - col++; - } + if (*p == '\t') { colStart[col] = p + 1; col++; } p++; } if (col >= 10 && colStart[4] && colStart[5] && colStart[7] && colStart[8]) { @@ -6942,20 +517,12 @@ int cmscan(int argc, const char **argv, const Command &command) { cand.qStart = Util::fast_atoi(colStart[4]); cand.qEnd = Util::fast_atoi(colStart[5]); cand.qLen = (colStart[6] != NULL) ? Util::fast_atoi(colStart[6]) : 0; - // mmseqs/iter3 may encode minus strand by reversing - // either side. Strand is XOR of the two reversals. const bool dbRev = (cand.dbStart > cand.dbEnd); const bool qRev = (cand.qStart > cand.qEnd); cand.prefilterIsRev = (dbRev != qRev); if (dbRev) std::swap(cand.dbStart, cand.dbEnd); if (qRev) std::swap(cand.qStart, cand.qEnd); cand.hasRegionCoord = true; - if (col >= 11 && colStart[10]) { - const char *bs = colStart[10]; - const char *be = bs; - while (*be != '\n' && *be != '\0' && *be != '\t') ++be; - cand.backtrace.assign(bs, static_cast(be - bs)); - } } } data = Util::skipLine(data); @@ -6963,9 +530,7 @@ int cmscan(int argc, const char **argv, const Command &command) { } } - // Phase 2: parallelize CYK across candidates. Each thread keeps a - // local Hit accumulator and a local Sequence buffer; the global - // `hits` is merged once under critical at the end. + std::vector hits; #pragma omp parallel num_threads(static_cast(nThreads)) { unsigned int thread_idx = 0; @@ -6973,259 +538,103 @@ int cmscan(int argc, const char **argv, const Command &command) { thread_idx = static_cast(omp_get_thread_num()); #endif Sequence seqObjLocal(seqDbr.getMaxSeqLen(), seqDecodeType, &nucMat, 0, false, false); - std::vector localHits; - localHits.reserve(64); - + std::vector localHits; + + // per-thread CM clone: owns its own smx + align matrices + char cerr[eslERRBUFSIZE]; + cerr[0] = '\0'; + CM_t *tcm = NULL; + const int cloneStatus = cm_Clone(model.cm, cerr, &tcm); + if (cloneStatus != eslOK || tcm == NULL) { +#pragma omp critical + Debug(Debug::ERROR) << "cmscan: cm_Clone failed: " << cerr << "\n"; + } else { #pragma omp for schedule(dynamic, 1) nowait - for (long ci = 0; ci < static_cast(cands.size()); ++ci) { - const Cand &cand = cands[ci]; - const unsigned int tKey = cand.tKey; - - const size_t tId = seqDbr.getId(tKey); - if (tId == UINT_MAX) { - continue; - } - FastaSeq fs = decodeOneSequence(seqDbr, tId, subMat, seqObjLocal, - decodeSeqDinuc, seqGpuDb, thread_idx); - - // Build region slice + d-cap. - std::string regionSeq; - int offset = 0; - int maxHitLen = 0; - if (cmRegionFlanking > 0.0f && cand.hasRegionCoord) { - const int qAlnLen = std::max(1, cand.qEnd - cand.qStart + 1); - // Match the reference branch windowing: keep slack proportional - // to the missing query/model span and bias it toward the missing side. - int leftFlank = 0; - int rightFlank = 0; - const int qLen = std::max(qAlnLen, - (cand.qLen > 0) ? cand.qLen - : qm.exactModel.clen); - if (qLen > 0) { - const int qStartClamped = std::max(0, std::min(cand.qStart, qLen - 1)); - const int qEndClamped = std::max(qStartClamped, - std::min(cand.qEnd, qLen - 1)); - const int leftMissing = qStartClamped; - const int rightMissing = std::max(0, qLen - qEndClamped - 1); - const int totalMissing = leftMissing + rightMissing; - const double missingFrac = static_cast(totalMissing) - / static_cast(qLen); - const int baseFlank = static_cast(std::ceil( - static_cast(cmRegionFlanking) - * static_cast(qAlnLen) - * missingFrac)); - leftFlank = baseFlank + leftMissing; - rightFlank = baseFlank + rightMissing; - } else { - const int baseFlank = static_cast(cmRegionFlanking * qAlnLen); - leftFlank = baseFlank; - rightFlank = baseFlank; - } - const int sLen = static_cast(fs.seq.size()); - const int regStart = std::max(0, cand.dbStart - leftFlank); - const int regEnd = std::min(sLen, cand.dbEnd + rightFlank + 1); - if (regStart >= sLen || regEnd <= regStart) { - continue; - } - regionSeq = fs.seq.substr(static_cast(regStart), - static_cast(regEnd - regStart)); - offset = regStart; - } else { - regionSeq = fs.seq; - } - if (cand.hasRegionCoord) { - const int dbSpan = std::max(1, cand.dbEnd - cand.dbStart + 1); - maxHitLen = std::max(dbSpan * CM_MAXSPAN_ENV_SLACK, clenFloor); - } else if (clenFloor > 0) { - maxHitLen = clenFloor; - } - { - const char *dumpTid = std::getenv("MMSEQS_CMSCAN_DUMP_TID"); - if (dumpTid != NULL && fs.id == dumpTid) { - fprintf(stderr, "DUMP_REGION tid=%s dbStart=%d dbEnd=%d offset=%d regLen=%d maxHitLen=%d clenFloor=%d strand=%c\n", - fs.id.c_str(), cand.dbStart, cand.dbEnd, offset, - static_cast(regionSeq.size()), maxHitLen, clenFloor, - cand.hasRegionCoord ? '+' : '?'); - } - } - - // If the prefilter already disambiguated strand (hasRegionCoord), - // scan only that strand. Fall back to both strands otherwise. - const bool scanFwd = !cand.hasRegionCoord || !cand.prefilterIsRev; - const bool scanRev = !cand.hasRegionCoord || cand.prefilterIsRev; - // Fix B: thread prefilter envelope into CYK as a forced (i, d). - // Hypothesis under test: cells inside the prefilter envelope produce - // Infernal-quality alignment columns when the DP is forbidden from - // shrinking d. Enabled only when MMSEQS_CMSCAN_FORCE_PREFILTER_ENV=1 - // and the candidate has a prefilter region coord. - int forceI = -1, forceD = -1; - { - static int forcePrefilter = -1; - if (forcePrefilter == -1) { - const char *env = std::getenv("MMSEQS_CMSCAN_FORCE_PREFILTER_ENV"); - forcePrefilter = (env != NULL && std::string(env) == "1") ? 1 : 0; - } - if (forcePrefilter == 1 && cand.hasRegionCoord) { - forceI = (cand.dbStart - offset) + 1; // 1-indexed in regionSeq - forceD = std::max(1, cand.dbEnd - cand.dbStart + 1); - } - } - // Anchor coords for FORCE_GLOBAL=3/4: prefilter [dbStart..dbEnd] mapped - // into regionSeq, 1-indexed. Plumbed unconditionally — the trace argmax - // only consults them when forceGlobal in {3,4}. - int anchorI = -1, anchorD = -1; - if (cand.hasRegionCoord) { - anchorI = (cand.dbStart - offset) + 1; - anchorD = std::max(1, cand.dbEnd - cand.dbStart + 1); - } - // Peak-anchor override: replace box-derived (anchorI, anchorD) with - // a point anchor at the SW-backtrace bit-score peak. Skip reverse-strand - // for v1 (residue lookup would need RC handling). - if (peakAnchorMode == 1 && cand.hasRegionCoord && !cand.prefilterIsRev - && !cand.backtrace.empty() && !perColMaxEmit.empty()) { - const int peakFull = peakAnchorFromCigar( - cand.backtrace, cand.qStart, cand.dbStart, - perColMaxEmit, qm.exactModel.clen, fs.seq); - if (peakFull >= 0) { - const int peakLocal = peakFull + 1 - offset; - if (peakLocal >= 1 && peakLocal <= static_cast(regionSeq.size())) { - anchorI = peakLocal; - anchorD = 1; - } - } - } - // peak_walk anchor: extend SW endpoints naively (1-to-1) to CM - // col 1 on the left and CM col CLEN on the right, clamped to the - // regionSeq window. This adapts anchorD to ~CLEN around wherever - // SW matched: full-target hits → full span (FG=1-like); partial - // hits → CLEN-bounded span centered on the SW match (FG=3-like). - // Forward strand only for v1 (mirror logic at line ~4687 handles - // the reverse-strand case symmetrically once enabled). - if (peakAnchorMode == 2 && cand.hasRegionCoord && !cand.prefilterIsRev) { - const int clen = qm.exactModel.clen; - // Naively extend SW (qStart..qEnd) → CM (1..CLEN) by adding - // (qStart-1) target residues left and (CLEN-qEnd) right. - int leftFull = cand.dbStart - std::max(0, cand.qStart - 1); - int rightFull = cand.dbEnd + std::max(0, clen - cand.qEnd); - if (leftFull < 0) leftFull = 0; - const int tLen = static_cast(fs.seq.size()); - if (rightFull >= tLen) rightFull = tLen - 1; - int leftLocal = leftFull + 1 - offset; - int rightLocal = rightFull + 1 - offset; - const int regSize = static_cast(regionSeq.size()); - if (leftLocal < 1) leftLocal = 1; - if (rightLocal > regSize) rightLocal = regSize; - if (leftLocal <= rightLocal) { - anchorI = leftLocal; - anchorD = rightLocal - leftLocal + 1; - } - } - std::vector fwdHits, revHits; - if (scanFwd) { - runInfernalExactScanWithEnvelopeRescore(qm.exactModel, regionSeq, wantInside, fwdHits, fs.id, - maxHitLen, forceI, forceD, anchorI, anchorD); - } - if (scanRev) { - const std::string revSeq = reverseComplement(regionSeq); - int revForceI = -1, revForceD = -1; - int revAnchorI = -1, revAnchorD = -1; - const int M_local = static_cast(regionSeq.size()); - if (forceI > 0 && forceD > 0) { - // Forward [forceI..forceI+forceD-1] maps to revcomp - // [M-forceI-forceD+2..M-forceI+1]. - revForceI = M_local - forceI - forceD + 2; - revForceD = forceD; - } - if (anchorI > 0 && anchorD > 0) { - revAnchorI = M_local - anchorI - anchorD + 2; - revAnchorD = anchorD; - } - runInfernalExactScanWithEnvelopeRescore(qm.exactModel, revSeq, wantInside, revHits, fs.id, - maxHitLen, revForceI, revForceD, - revAnchorI, revAnchorD); - } - - const unsigned int fullLen = static_cast(fs.seq.size()); - const int regionLen = static_cast(regionSeq.size()); - for (Hit &h : fwdHits) { - finalizeHit(h, +1, fs.seq, tKey, fullLen, offset, regionLen); - localHits.emplace_back(std::move(h)); - } - for (Hit &h : revHits) { - finalizeHit(h, -1, fs.seq, tKey, fullLen, offset, regionLen); - localHits.emplace_back(std::move(h)); - } - } - + for (long ci = 0; ci < static_cast(cands.size()); ++ci) { + const Cand &cand = cands[static_cast(ci)]; + const size_t tId = seqDbr.getId(cand.tKey); + if (tId == UINT_MAX) continue; + FastaSeq fs = decodeOneSequence(seqDbr, tId, subMat, seqObjLocal, + decodeSeqDinuc, seqGpuDb, thread_idx); + const int seqLen = static_cast(fs.seq.size()); + if (seqLen <= 0) continue; + + // Rescore a WINDOW around the prefilter envelope, padded by W. + // Bounds DP by model size, not the target length. + std::string regionBuf; + const std::string *regionSeqPtr = &fs.seq; + int offset = 0; + if (cand.hasRegionCoord && regionFlank > 0.0f) { + const int basePad = (W > 0) ? W : std::max(1, model.clen); + const int pad = std::max(1, static_cast(regionFlank * basePad)); + const int regStart = std::max(0, cand.dbStart - pad); + const int regEnd = std::min(seqLen, cand.dbEnd + 1 + pad); + if (regEnd <= regStart) continue; + if (regStart != 0 || regEnd != seqLen) { + regionBuf = fs.seq.substr(static_cast(regStart), + static_cast(regEnd - regStart)); + regionSeqPtr = ®ionBuf; + } + offset = regStart; + } + const std::string ®ionSeq = *regionSeqPtr; + const int regionLen = static_cast(regionSeq.size()); + + const bool scanFwd = !cand.hasRegionCoord || !cand.prefilterIsRev; + const bool scanRev = !cand.hasRegionCoord || cand.prefilterIsRev; + if (scanFwd) { + InfHit h = scanRegionInfernal(model, tcm, regionSeq); + if (h.valid) { + finalizeHit(h, +1, cand.tKey, static_cast(seqLen), offset, regionLen); + localHits.push_back(h); + } + } + if (scanRev) { + const std::string revSeq = reverseComplement(regionSeq); + InfHit h = scanRegionInfernal(model, tcm, revSeq); + if (h.valid) { + finalizeHit(h, -1, cand.tKey, static_cast(seqLen), offset, regionLen); + localHits.push_back(h); + } + } + } + } + if (tcm != NULL) FreeCM(tcm); #pragma omp critical { - hits.insert(hits.end(), - std::make_move_iterator(localHits.begin()), - std::make_move_iterator(localHits.end())); + hits.insert(hits.end(), localHits.begin(), localHits.end()); } - } // end inner omp parallel + } - std::sort(hits.begin(), hits.end(), [](const Hit &a, const Hit &b) { + std::sort(hits.begin(), hits.end(), [](const InfHit &a, const InfHit &b) { if (a.dbKey != b.dbKey) return a.dbKey < b.dbKey; if (a.start1 != b.start1) return a.start1 < b.start1; return a.end1 < b.end1; }); - totalHits += hits.size(); - - const unsigned int modelLen = static_cast(std::max(0, qm.exactModel.clen)); - // Stream the per-query result block to the writer one hit at a time. - // Outer loop is sequential, so always use thread slot 0. + char buffer[1024 + 32768 * 4]; resultWriter.writeStart(0); for (size_t i = 0; i < hits.size(); ++i) { - const Hit &h = hits[i]; - // Trim db range by inserts that fall outside the [firstM,lastM] - // CIGAR window: those leading/trailing insert-state target - // residues are not part of the reported alignment region. - // For reverse-strand hits finalizeHit produces start1 > end1 - // (Infernal convention), so the trim direction flips. - const int rawDbStart = std::max(0, h.start1 - 1); - const int rawDbEnd = std::max(0, h.end1 - 1); - int dbStartOut, dbEndOut; - if (rawDbStart <= rawDbEnd) { - dbStartOut = std::min(rawDbEnd, - rawDbStart + h.leadingInsertTargets); - dbEndOut = std::max(dbStartOut, - rawDbEnd - h.trailingInsertTargets); - } else { - dbStartOut = std::max(rawDbEnd, - rawDbStart - h.leadingInsertTargets); - dbEndOut = std::min(dbStartOut, - rawDbEnd + h.trailingInsertTargets); - } - // qLen = real query sequence length. With cmbuild --hand we - // ensured CM clen == qLen so modelLen is the right value. + const InfHit &h = hits[i]; + int dbStartOut = std::max(0, h.start1 - 1); + int dbEndOut = std::max(0, h.end1 - 1); const unsigned int qLen = (modelLen > 0) ? modelLen : static_cast(std::max(1, dbEndOut - dbStartOut + 1)); - // qStart/qEnd come from the trace's first/last match-state column - // (0-indexed query coord). Fall back to a degenerate full span if - // the trace produced no match states. int qStartOut = (h.qStart >= 0) ? h.qStart : 0; - int qEndOut = (h.qEnd >= 0) ? h.qEnd - : static_cast(qLen) - 1; + int qEndOut = (h.qEnd >= 0) ? h.qEnd : static_cast(qLen) - 1; if (qEndOut < qStartOut) qEndOut = qStartOut; - // alnLen is total CIGAR ops (M+D+I) — the canonical MMseqs view. const unsigned int alnLen = (h.cigarAlnLen > 0) - ? h.cigarAlnLen - : static_cast(std::max(1, dbEndOut - dbStartOut + 1)); + ? static_cast(h.cigarAlnLen) + : static_cast(std::max(1, std::abs(dbEndOut - dbStartOut) + 1)); const unsigned int qSpan = static_cast(qEndOut - qStartOut + 1); const unsigned int dbSpan = static_cast(std::max(1, std::abs(dbEndOut - dbStartOut) + 1)); const float qcov = (qLen > 0) ? static_cast(qSpan) / static_cast(qLen) : 0.0f; const float dbcov = (h.dbLen > 0) ? static_cast(dbSpan) / static_cast(h.dbLen) : 0.0f; - const int bitScore = static_cast(std::lrint(h.cyk)); - const double evalue = h.hasEvalue ? h.evalue : 1.0; + const int bitScore = static_cast(std::lrint(h.score)); + const double evalue = 1.0; // E-values out of scope const bool hasBacktrace = (!h.cigar.empty() && h.cigar != "NA"); - // CmScan internal CIGAR convention: I = target-consume (insert state), D = query-consume - // (consensus column with target gap). MMseqs/result2*msa convention is the opposite: - // I = query-consume, D = target-consume. Swap I<->D once at the emit boundary so the - // serialized result_t carries the canonical convention; internal helpers - // (seqIdFromCigarConsensus, etc.) keep their original interpretation. + // Swap I/D (I=target-consume, D=query-consume) to MMseqs2 convention std::string emitCigar; if (hasBacktrace) { emitCigar.reserve(h.cigar.size()); @@ -7238,410 +647,122 @@ int cmscan(int argc, const char **argv, const Command &command) { if (dbStartOut > dbEndOut && qStartOut <= qEndOut) { std::swap(qStartOut, qEndOut); std::swap(dbStartOut, dbEndOut); - if (hasBacktrace) { - emitCigar = reverseRleTraceOps(emitCigar); - } + if (hasBacktrace) emitCigar = reverseRleTraceOps(emitCigar); } - - float seqIdVal = h.precomputedSeqId; + float seqIdVal = h.seqId; if (seqIdVal < 0.0f) { const unsigned int bitScorePos = static_cast(std::max(0, bitScore)); - seqIdVal = Matcher::estimateSeqIdByScorePerCol(static_cast(std::min(bitScorePos, 65535u)), - std::max(1u, alnLen), - std::max(1u, alnLen)); + seqIdVal = Matcher::estimateSeqIdByScorePerCol( + static_cast(std::min(bitScorePos, 65535u)), + std::max(1u, alnLen), std::max(1u, alnLen)); } - Matcher::result_t res(h.dbKey, - bitScore, + Matcher::result_t res(h.dbKey, bitScore, std::min(1.0f, std::max(0.0f, qcov)), std::min(1.0f, std::max(0.0f, dbcov)), std::min(1.0f, std::max(0.0f, seqIdVal)), - evalue, - alnLen, - qStartOut, - qEndOut, - qLen, - dbStartOut, - dbEndOut, - h.dbLen, + evalue, alnLen, qStartOut, qEndOut, qLen, + dbStartOut, dbEndOut, h.dbLen, hasBacktrace ? emitCigar : std::string()); const size_t len = Matcher::resultToBuffer(buffer, res, hasBacktrace, false); resultWriter.writeAdd(buffer, len, 0); } - resultWriter.writeEnd(qm.key, 0); - } - resultWriter.close(true); - resultReader.close(); - - // Write companion header DB so integer target keys can be mapped back to FASTA headers. - const std::string headerDb = par.db4 + "_h"; - const std::string headerDbIdx = par.db4Index + "_h"; - DBWriter headerWriter(headerDb.c_str(), - headerDbIdx.c_str(), - 1, - par.compressed, - Parameters::DBTYPE_GENERIC_DB); - headerWriter.open(); - const bool hasLookup = (seqDbr.getLookupSize() > 0); - for (size_t i = 0; i < seqDbr.getSize(); ++i) { - const unsigned int key = seqDbr.getDbKey(i); - std::string name; - if (hasLookup) { - const size_t lid = seqDbr.getLookupIdByKey(key); - name = seqDbr.getLookupEntryName(lid); - } else { - name = std::to_string(key); - } - name.push_back('\n'); - headerWriter.writeData(name.c_str(), name.size(), key, 0); - } - headerWriter.close(); - Debug(Debug::INFO) << subcmd << " header DB: " << headerDb << "\n"; - - seqDbr.close(); - if (cmReader != NULL) { - cmReader->close(); - delete cmReader; - } - - Debug(Debug::INFO) << subcmd << " finished. Hits: " << totalHits << "\n"; - return EXIT_SUCCESS; -} - -int cmsearch(int argc, const char **argv, const Command &command) { - return cmscan(argc, argv, command); -} - -// ===================================================================== -// cmprefilter — CM emit-only multi-hit prefilter -// -// Takes an existing RNA search (k-mer/SW) prefilter result, adds CM-detected -// alternative hits per target. For each target sequence already passing the -// RNA search filter, slides a per-consensus-column emission profile along the -// target and emits high-scoring positions as additional prefilter rows. -// -// Output format = same as input (m8 prefilter), so cmsearch can consume it -// directly with MMSEQS_CMSCAN_ALLOW_MULTIHIT=1. -// -// Speed target: O(L * CLEN) per target with SIMD (AVX2 8-wide float adds); -// for typical 10kb target * 100 CLEN: ~1M ops/target → ~30s-1min per query -// over 74k targets at ~75 GFLOPS effective. -// ===================================================================== - -#include - -// Build a per-consensus-column emission profile from the CM model. -// profile[c][nt] = emit log-odds score at consensus column c for nucleotide nt. -// MP states contribute marginal scores (mean over the other side's bases). -// ML/MR states contribute their direct emit scores. -static void buildCmpfEmitProfile(const InfernalExactModel &model, - std::vector> &profile) { - const int clen = static_cast(model.clen); - profile.assign(static_cast(clen), {0.f, 0.f, 0.f, 0.f}); - std::vector counts(static_cast(clen), 0); - for (size_t v = 0; v < model.states.size(); ++v) { - const CmState &s = model.states[v]; - if (s.type == CM_ST_ML && s.mapLeft >= 1 && s.mapLeft <= clen - && s.emitF.size() >= 4) { - for (int nt = 0; nt < 4; ++nt) { - profile[(size_t)(s.mapLeft - 1)][nt] += s.emitF[(size_t)nt]; - } - counts[(size_t)(s.mapLeft - 1)]++; - } else if (s.type == CM_ST_MR && s.mapRight >= 1 && s.mapRight <= clen - && s.emitF.size() >= 4) { - for (int nt = 0; nt < 4; ++nt) { - profile[(size_t)(s.mapRight - 1)][nt] += s.emitF[(size_t)nt]; - } - counts[(size_t)(s.mapRight - 1)]++; - } else if (s.type == CM_ST_MP && s.emitF.size() >= 16) { - // MP emit is 4x4 log-odds: emit[a][b] = log2(P(a,b)/(null[a]*null[b])) - // Marginal for left col a: mean over b (rough approximation; proper - // is log-sum-exp but mean is a stable rank-preserving heuristic). - if (s.mapLeft >= 1 && s.mapLeft <= clen) { - for (int a = 0; a < 4; ++a) { - float sum = 0.f; - for (int b = 0; b < 4; ++b) sum += s.emitF[(size_t)(a * 4 + b)]; - profile[(size_t)(s.mapLeft - 1)][a] += sum * 0.25f; - } - counts[(size_t)(s.mapLeft - 1)]++; - } - if (s.mapRight >= 1 && s.mapRight <= clen) { - for (int b = 0; b < 4; ++b) { - float sum = 0.f; - for (int a = 0; a < 4; ++a) sum += s.emitF[(size_t)(a * 4 + b)]; - profile[(size_t)(s.mapRight - 1)][b] += sum * 0.25f; - } - counts[(size_t)(s.mapRight - 1)]++; - } - } - } - for (int c = 0; c < clen; ++c) { - if (counts[(size_t)c] > 0) { - const float inv = 1.0f / (float)counts[(size_t)c]; - for (int nt = 0; nt < 4; ++nt) profile[(size_t)c][nt] *= inv; - } - } -} - -// SIMD-vectorized sliding window scan. Returns one float per window position -// (L - CLEN + 1 windows). Score[p] = sum over c in [0,CLEN) of profile[c][seq[p+c]]. -// Uses 8-wide AVX2 if available; falls back to scalar otherwise. -static void cmpfSlidingScore(const std::vector> &profile, - const int8_t *encoded, // length L; -1 for non-ACGU - int L, - std::vector &outScores) { - const int clen = static_cast(profile.size()); - const int nWin = L - clen + 1; - if (nWin <= 0) { outScores.clear(); return; } - outScores.assign(static_cast(nWin), 0.0f); - // Per consensus col c, look up profile[c][nt] for each window position - // (which translates to encoded[p + c]). Vectorize across windows in chunks of 8. - for (int c = 0; c < clen; ++c) { - const float p0 = profile[(size_t)c][0]; - const float p1 = profile[(size_t)c][1]; - const float p2 = profile[(size_t)c][2]; - const float p3 = profile[(size_t)c][3]; - const int8_t *base = encoded + c; - // scalar loop with branchless indexing (encoded[i] in {0..3} or -1) - for (int p = 0; p < nWin; ++p) { - const int8_t b = base[p]; - if (b == 0) outScores[(size_t)p] += p0; - else if (b == 1) outScores[(size_t)p] += p1; - else if (b == 2) outScores[(size_t)p] += p2; - else if (b == 3) outScores[(size_t)p] += p3; - // -1 (N/X): contribute 0 (mask out) - } - } -} - -// Encode an ASCII RNA/DNA sequence to {0,1,2,3} = {A,C,G,U/T}, -1 elsewhere. -static void cmpfEncodeSeq(const char *seq, int L, std::vector &out) { - out.assign(static_cast(L), (int8_t)-1); - for (int i = 0; i < L; ++i) { - const char c = seq[i]; - if (c == 'A' || c == 'a') out[(size_t)i] = 0; - else if (c == 'C' || c == 'c') out[(size_t)i] = 1; - else if (c == 'G' || c == 'g') out[(size_t)i] = 2; - else if (c == 'T' || c == 't' || c == 'U' || c == 'u') out[(size_t)i] = 3; - } -} - -// Pick top-K non-overlapping peaks (NMS by min-distance = clen/2). -// Returns sorted peak positions. -static std::vector cmpfPickPeaks(const std::vector &scores, int clen, - int maxPeaks, float threshold) { - std::vector idxs; - idxs.reserve(scores.size()); - for (size_t i = 0; i < scores.size(); ++i) { - if (scores[i] >= threshold) idxs.push_back((int)i); - } - std::sort(idxs.begin(), idxs.end(), - [&](int a, int b) { return scores[(size_t)a] > scores[(size_t)b]; }); - const int minDist = std::max(1, clen / 2); - std::vector taken; - taken.reserve(maxPeaks); - for (int p : idxs) { - bool ok = true; - for (int t : taken) { - if (std::abs(p - t) < minDist) { ok = false; break; } - } - if (ok) { - taken.push_back(p); - if ((int)taken.size() >= maxPeaks) break; - } - } - std::sort(taken.begin(), taken.end()); - return taken; + resultWriter.writeEnd(queryKey, 0); } -int cmprefilter(int argc, const char **argv, const Command &command) { +int cmscan(int argc, const char **argv, const Command &command) { LocalParameters &par = LocalParameters::getLocalInstance(); par.parseParameters(argc, argv, command, true, 0, MMseqsParameter::COMMAND_ALIGN); - if (MMseqsMPI::isMaster() == false) return EXIT_SUCCESS; - Debug(Debug::INFO) << "Running CM emit-only multi-hit prefilter\n"; + cmInfernalGlobalInit(); - // Tunables via env vars (lets us iterate without rebuild). - const char *thrEnv = std::getenv("MMSEQS_CMPF_SCORE_THRESHOLD"); - const float scoreThreshold = (thrEnv != NULL) ? (float)std::atof(thrEnv) : -45.0f; - const char *maxEnv = std::getenv("MMSEQS_CMPF_MAX_HITS_PER_TARGET"); - const int maxHitsPerTarget = (maxEnv != NULL) ? std::atoi(maxEnv) : 5; - - Debug(Debug::INFO) << " score threshold: " << scoreThreshold - << ", max hits/target: " << maxHitsPerTarget << "\n"; + const size_t nThreads = (par.threads > 0) ? static_cast(par.threads) : 1u; - // -- 1. Parse query CM(s) -- - struct QueryCM { - unsigned int key; - InfernalExactModel model; - std::vector> profile; - }; - std::vector queries; - { - if (hasDbIndex(par.db1)) { - DBReader cmReader(par.db1.c_str(), (par.db1 + ".index").c_str(), - par.threads > 0 ? par.threads : 1, - DBReader::USE_DATA | DBReader::USE_INDEX); - cmReader.open(DBReader::NOSORT); - for (size_t i = 0; i < cmReader.getSize(); ++i) { - QueryCM q; - q.key = cmReader.getDbKey(i); - std::istringstream iss(cmReader.getData(i, 0)); - q.model = parseInfernalCmExactModelFromStream(iss, "cm:" + SSTR(q.key)); - buildCmpfEmitProfile(q.model, q.profile); - queries.emplace_back(std::move(q)); - } - cmReader.close(); - } else { - std::ifstream ifs(par.db1); - QueryCM q; - q.key = 0; - q.model = parseInfernalCmExactModelFromStream(ifs, par.db1); - buildCmpfEmitProfile(q.model, q.profile); - queries.emplace_back(std::move(q)); + struct QueryModelRef { unsigned int key; size_t dbIdx; }; + std::vector queryRefs; + DBReader *cmReader = NULL; + std::string singleCmText; + bool haveSingle = false; + if (hasDbIndex(par.db1)) { + cmReader = new DBReader(par.db1.c_str(), (par.db1 + ".index").c_str(), + static_cast(nThreads), + DBReader::USE_DATA | DBReader::USE_INDEX); + cmReader->open(DBReader::NOSORT); + queryRefs.reserve(cmReader->getSize()); + for (size_t i = 0; i < cmReader->getSize(); ++i) { + queryRefs.push_back(QueryModelRef{cmReader->getDbKey(i), i}); + } + Debug(Debug::INFO) << "cmscan: CM database with " << queryRefs.size() << " models\n"; + } else { + if (!looksLikeInfernalCm(par.db1)) { + Debug(Debug::ERROR) << "cmscan requires an Infernal CM (run cmbuild first): " << par.db1 << "\n"; + return EXIT_FAILURE; } + std::ifstream in(par.db1.c_str()); + std::stringstream ss; + ss << in.rdbuf(); + singleCmText = ss.str(); + haveSingle = true; + queryRefs.push_back(QueryModelRef{0, SIZE_MAX}); } - Debug(Debug::INFO) << "Loaded " << queries.size() << " query CM(s)\n"; - - // -- 2. Open target db -- - DBReader tdbr(par.db2.c_str(), (par.db2 + ".index").c_str(), - par.threads, DBReader::USE_DATA | DBReader::USE_INDEX); - tdbr.open(DBReader::NOSORT); - // -- 3. Open input result reader and output writer -- - DBReader resReader(par.db3.c_str(), (par.db3 + ".index").c_str(), - par.threads, DBReader::USE_DATA | DBReader::USE_INDEX); - resReader.open(DBReader::NOSORT); + DBReader seqDbr(par.db2.c_str(), (par.db2 + ".index").c_str(), + static_cast(nThreads), + DBReader::USE_DATA | DBReader::USE_INDEX + | DBReader::USE_LOOKUP); + seqDbr.open(DBReader::NOSORT); + const unsigned int seqExt = DBReader::getExtendedDbtype(seqDbr.getDbtype()); + const bool seqGpuDb = (seqExt & Parameters::DBTYPE_EXTENDED_GPU) != 0; + const bool decodeSeqDinuc = ((seqExt & Parameters::DBTYPE_EXTENDED_DINUCLEOTIDE) != 0) || seqGpuDb; + const int seqDecodeType = effectiveDecodeSeqType(seqDbr.getDbtype(), decodeSeqDinuc); + NucleotideMatrix nucMat(Parameters::getInstance().scoringMatrixFile.values.nucleotide().c_str(), 1.0, 0.0); + BaseMatrix &subMat = static_cast(nucMat); - DBWriter writer(par.db4.c_str(), (par.db4 + ".index").c_str(), - par.threads, par.compressed, Parameters::DBTYPE_PREFILTER_RES); - writer.open(); + DBReader resultReader(par.db3.c_str(), (par.db3 + ".index").c_str(), + static_cast(nThreads), + DBReader::USE_DATA | DBReader::USE_INDEX); + resultReader.open(DBReader::NOSORT); - // -- 4. For each query, scan each target listed in the input result -- - size_t totalAdded = 0; - size_t totalKept = 0; + DBWriter resultWriter(par.db4.c_str(), par.db4Index.c_str(), + static_cast(nThreads), par.compressed, + Parameters::DBTYPE_ALIGNMENT_RES); + resultWriter.open(); - for (const QueryCM &q : queries) { - const size_t resId = resReader.getId(q.key); - if (resId == UINT_MAX) { - // No prefilter rows for this query — skip. - writer.writeData("", 0, q.key); + for (size_t qi = 0; qi < queryRefs.size(); ++qi) { + const QueryModelRef &ref = queryRefs[qi]; + std::string cmText; + if (ref.dbIdx != SIZE_MAX && cmReader != NULL) { + const char *raw = cmReader->getData(ref.dbIdx, 0); + size_t len = cmReader->getEntryLen(ref.dbIdx); + cmText.assign(raw, len); + const size_t nul = cmText.find('\0'); + if (nul != std::string::npos) cmText.resize(nul); + } else if (haveSingle) { + cmText = singleCmText; + } else { continue; } - const char *resData = resReader.getData(resId, 0); - - // Parse input rows; group by target key for per-target processing. - struct InRow { - unsigned int tKey; - std::string raw; // original line (for pass-through) - }; - std::vector inRows; - std::map> rowsByTarget; - { - const char *p = resData; - while (*p != '\0') { - const char *lineStart = p; - const char *lineEnd = std::strchr(p, '\n'); - if (lineEnd == NULL) lineEnd = p + std::strlen(p); - std::string line(lineStart, lineEnd - lineStart); - p = (*lineEnd == '\0') ? lineEnd : (lineEnd + 1); - if (line.empty()) continue; - char *endp = NULL; - unsigned long k = std::strtoul(line.c_str(), &endp, 10); - if (endp == line.c_str()) continue; - InRow r; - r.tKey = (unsigned int)k; - r.raw = std::move(line); - rowsByTarget[r.tKey].push_back(inRows.size()); - inRows.emplace_back(std::move(r)); - } - } - - std::string outBuf; - outBuf.reserve(1024 * 1024); - // Always include all original rows (decision A — preserve RNA search hits) - for (const InRow &r : inRows) { - outBuf.append(r.raw); - outBuf.push_back('\n'); - totalKept++; - } - - // For each unique target, scan and add CM-found peaks. - const int clen = (int)q.model.clen; - std::vector encoded; - std::vector scores; - - for (auto &kv : rowsByTarget) { - const unsigned int tKey = kv.first; - const size_t tId = tdbr.getId(tKey); - if (tId == UINT_MAX) continue; - const char *seqData = tdbr.getData(tId, 0); - const int seqLen = (int)tdbr.getSeqLen(tId); - if (seqLen < clen) continue; - cmpfEncodeSeq(seqData, seqLen, encoded); - cmpfSlidingScore(q.profile, encoded.data(), seqLen, scores); - std::vector peaks = cmpfPickPeaks(scores, clen, maxHitsPerTarget, scoreThreshold); - - // Avoid emitting peaks that overlap an existing input row's window. - std::vector> existingWindows; - for (size_t rowIdx : kv.second) { - const std::string &raw = inRows[rowIdx].raw; - // Parse tstart/tend (cols 8/9, 0-indexed) from m8 row. - int field = 0; - const char *fp = raw.c_str(); - int ts = -1, te = -1; - while (*fp && field < 9) { - if (*fp == '\t') { - ++field; - if (field == 7 || field == 8) { - char *ep = NULL; - long v = std::strtol(fp + 1, &ep, 10); - if (field == 7) ts = (int)v; - else te = (int)v; - } - } - ++fp; - } - if (ts >= 0 && te >= 0) { - existingWindows.emplace_back(std::min(ts, te), std::max(ts, te)); - } - } - - for (int p : peaks) { - int ts = p; - int te = p + clen - 1; - bool overlapsExisting = false; - for (auto &w : existingWindows) { - if (te >= w.first && ts <= w.second) { - overlapsExisting = true; - break; - } - } - if (overlapsExisting) continue; - const float sc = scores[(size_t)p]; - const int bitScore = std::max(1, (int)std::lrint(sc + 50.0f)); - // m8 row format consistent with RNA search output. - char buf[256]; - std::snprintf(buf, sizeof(buf), - "%u\t%d\t1.00\t1e-30\t0\t%d\t%d\t%d\t%d\t%d\t0\t%d\t0\t0\t%dM\n", - tKey, bitScore, - clen - 1, clen, // qstart/qend, qlen - ts, te, seqLen, // tstart/tend, tlen - clen - 1, // tcov-style - clen); - outBuf.append(buf); - totalAdded++; - } + const bool useInside = (par.cmMode == 1); + int alignOpts = par.cmAlign ? CM_ALIGN_OPTACC : CM_ALIGN_CYK; + if (par.cmAlignBanded == 0) alignOpts |= CM_ALIGN_NONBANDED; + InfModel model = loadModel(cmText, useInside, alignOpts, par.cmAlignBanded != 0, par.cmLocal != 0); + if (!model.valid) { + Debug(Debug::WARNING) << "cmscan: could not load model key " << ref.key << ", skipping\n"; + continue; } - - writer.writeData(outBuf.c_str(), outBuf.size(), q.key); + processQueryModel(par, model, ref.key, seqDbr, resultReader, resultWriter, + subMat, nucMat, seqDecodeType, seqGpuDb, decodeSeqDinuc, nThreads); + freeModel(model); } - writer.close(); - resReader.close(); - tdbr.close(); - - Debug(Debug::INFO) << "cmprefilter finished. Original kept: " << totalKept - << ", new CM-found added: " << totalAdded << "\n"; + resultWriter.close(); + resultReader.close(); + seqDbr.close(); + if (cmReader != NULL) { cmReader->close(); delete cmReader; } return EXIT_SUCCESS; } + +// compat +int cmsearch(int argc, const char **argv, const Command &command) { + return cmscan(argc, argv, command); +} diff --git a/src/rnautils/GenerateCm.cpp b/src/rnautils/GenerateCm.cpp index 4a2a43d..5334b67 100644 --- a/src/rnautils/GenerateCm.cpp +++ b/src/rnautils/GenerateCm.cpp @@ -1,48 +1,46 @@ -#include "LocalCommandDeclarations.h" -#include "CommandDeclarations.h" +#include "LocalParameters.h" #include "Debug.h" #include "DBReader.h" #include "DBWriter.h" +#include "CmBuildScan.h" +#include "LocalParameters.h" #include "FileUtil.h" -#include "MMseqsMPI.h" -#include "Parameters.h" -#include "RNAFoldBridge.h" +#include "RnaFoldingBridge.h" #include "Matcher.h" #include "Orf.h" -#include "LocalParameters.h" #include "MsaFilter.h" #include "MultipleAlignment.h" #include "SubstitutionMatrix.h" #include "Util.h" #include "DinucleotideMapping.h" -#ifdef HAVE_INFERNAL_BRIDGE -#include "infernal/InfernalBridge.h" + +#include +#include +#ifdef RIBOSEEK_CMBUILD_DUMP_STOCKHOLM +#include #endif #ifdef OPENMP #include #endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Native generatecm command: -// maps DB -> DB covariance-model generation to MMseqs profile construction. +extern bool cmbuildFromAlignment( + const std::string &name, + const std::vector &sqnames, + const std::vector &aseqs, + const std::string &ssCons, + std::string &cmText, std::string &err, + double ereOverride, double symfracOpt, bool noss, + bool forScan, bool useInside, bool useLocal, + CM_t **ret_cm +); +extern void cmInfernalGlobalInit(); + +extern int result2profile(int argc, const char **argv, const Command& command); int generatecm(int argc, const char **argv, const Command &command) { return result2profile(argc, argv, command); } -namespace { - struct AlnSeq { std::string id; std::string aln; @@ -135,32 +133,6 @@ static inline unsigned char encodeAlignedNuc(char c) { } } -static std::string buildStockholmText(const std::string &id, - const std::vector &seqs, - const std::string &ssCons) { - std::ostringstream out; - out << "# STOCKHOLM 1.0\n"; - out << "#=GF ID " << (id.empty() ? "mmseqs_model" : id) << "\n"; - for (size_t i = 0; i < seqs.size(); ++i) { - std::string rnaAln = seqs[i].aln; - convertTsToUs(rnaAln); - out << seqs[i].id << " " << rnaAln << "\n"; - } - // Force every alignment column to be a CM match column via --hand: RF line - // of all 'x' makes consensus column c map 1:1 to query position c-1, so - // cmsearch traces are directly query-coord and result_t records stay - // compatible with result2dnamsa / convertalis. - if (!seqs.empty() && !seqs[0].aln.empty()) { - out << "#=GC RF " << std::string(seqs[0].aln.size(), 'x') << "\n"; - } - if (!ssCons.empty()) { - out << "#=GC SS_cons " << ssCons << "\n"; - } - out << "//\n"; - return out.str(); -} - - static std::string trimCopy(const std::string &s) { size_t b = 0; while (b < s.size() && std::isspace(static_cast(s[b]))) { @@ -461,189 +433,31 @@ static bool mergeCmbuildInputs(const std::string &queryDb, return true; } -} // namespace - -int cmbuild(int argc, const char **argv, const Command &command) { - LocalParameters &par = LocalParameters::getLocalInstance(); - // Defaults for the optional MSA row filter (off; rMSA-style cov+id+Ndiff - // when the user opts in via --filter-msa 1). qsc disabled because our - // substitution matrix is dinucleotide so qsc scores would be in the wrong - // space; cov + filterMaxSeqId + Ndiff are pure byte comparisons and unaffected. - par.filterMsa = 0; - par.qsc = -50.0f; - par.covMSAThr = 0.5f; - par.filterMaxSeqId = 0.99f; - par.Ndiff = 128; - - std::string rawTargetDbArg; - std::string rawResultDbArg; - std::vector parseArgStorage; - std::vector parseArgv; - if (argc > 2) { - rawTargetDbArg = argv[1]; - rawResultDbArg = argv[2]; - if (rawTargetDbArg.find(',') != std::string::npos - || rawResultDbArg.find(',') != std::string::npos) { - parseArgStorage.reserve(static_cast(argc)); - parseArgv.reserve(static_cast(argc)); - for (int i = 0; i < argc; ++i) { - parseArgStorage.push_back(argv[i]); - } - const std::vector targetParts = splitCommaList(rawTargetDbArg); - const std::vector resultParts = splitCommaList(rawResultDbArg); - if (!targetParts.empty()) { - parseArgStorage[1] = targetParts[0]; - } - if (!resultParts.empty()) { - parseArgStorage[2] = resultParts[0]; - } - for (size_t i = 0; i < parseArgStorage.size(); ++i) { - parseArgv.push_back(parseArgStorage[i].c_str()); - } - } - } - par.parseParameters(argc, - parseArgv.empty() ? argv : parseArgv.data(), - command, - true, - 0, - 0); - -#ifndef HAVE_INFERNAL_BRIDGE - Debug(Debug::ERROR) << "cmbuild requires Infernal bridge support\n"; - return EXIT_FAILURE; -#else - - const std::vector targetDbList = splitCommaList(rawTargetDbArg.empty() ? par.db2 : rawTargetDbArg); - const std::vector resultDbList = splitCommaList(rawResultDbArg.empty() ? par.db3 : rawResultDbArg); - if (targetDbList.size() != resultDbList.size()) { - Debug(Debug::ERROR) << "cmbuild: targetDB/resultDB list sizes differ (" - << targetDbList.size() << " vs " << resultDbList.size() << ")\n"; - return EXIT_FAILURE; - } - const bool useMergedInputs = (targetDbList.size() > 1 || resultDbList.size() > 1); - if (useMergedInputs) { - const std::string mergedTargetDb = par.db4 + "_target_merged"; - const std::string mergedResultDb = par.db4 + "_result_merged"; - removeDbFiles(mergedTargetDb); - removeDbFiles(mergedResultDb); - std::string mergeErr; - Debug(Debug::INFO) << "cmbuild: merging " << targetDbList.size() - << " target/result DB pairs next to output CM DB\n"; - if (!mergeCmbuildInputs(par.db1, targetDbList, resultDbList, - mergedTargetDb, mergedResultDb, - par.threads, mergeErr)) { - Debug(Debug::ERROR) << "cmbuild: failed to merge multi-DB input: " << mergeErr << "\n"; - return EXIT_FAILURE; - } - par.db2 = mergedTargetDb; - par.db2Index = mergedTargetDb + ".index"; - par.db3 = mergedResultDb; - par.db3Index = mergedResultDb + ".index"; - Debug(Debug::INFO) << "cmbuild: merged target DB: " << par.db2 << "\n"; - Debug(Debug::INFO) << "cmbuild: merged result DB: " << par.db3 << "\n"; - } - - // Fork Infernal workers BEFORE loading DBs. Each worker stays ~small - // (few MB) instead of CoW-ing a post-mmap fat parent, which serializes - // forks under the kernel mm lock. - InfernalBridge::WorkerPool *workerPool = InfernalBridge::startWorkerPool(par.threads); - if (workerPool == NULL) { - Debug(Debug::ERROR) << "cmbuild: failed to start Infernal worker pool\n"; - return EXIT_FAILURE; - } - - DBReader qDbr(par.db1.c_str(), par.db1Index.c_str(), par.threads, - DBReader::USE_INDEX | DBReader::USE_DATA); - qDbr.open(DBReader::NOSORT); - - const bool sameDatabase = (par.db1 == par.db2); - DBReader *tDbr = &qDbr; - if (!sameDatabase) { - tDbr = new DBReader(par.db2.c_str(), par.db2Index.c_str(), par.threads, - DBReader::USE_INDEX | DBReader::USE_DATA); - tDbr->open(DBReader::NOSORT); - } - - DBReader resultReader(par.db3.c_str(), par.db3Index.c_str(), par.threads, - DBReader::USE_INDEX | DBReader::USE_DATA); - resultReader.open(DBReader::LINEAR_ACCCESS); - - DBWriter cmWriter(par.db4.c_str(), par.db4Index.c_str(), par.threads, - par.compressed, Parameters::DBTYPE_GENERIC_DB); - cmWriter.open(); - - Debug(Debug::INFO) << "Query database size: " << qDbr.getSize() << " type: " << qDbr.getDbTypeName() << "\n"; - Debug(Debug::INFO) << "Target database size: " << tDbr->getSize() << " type: " << tDbr->getDbTypeName() << "\n"; - { - std::ostringstream msaEvalStream; - msaEvalStream << std::scientific << par.cmliteMsaEvalThr; - Debug(Debug::INFO) << "cmbuild seed E-value threshold: " << msaEvalStream.str() << "\n"; - } - - Debug::Progress progress(resultReader.getSize()); - - // Stockholm rows are filtered: drop any target with non-gap coverage - // below this threshold. Infernal's cm_parsetree_Doctor() can fail when - // many short fragments dominate the column statistics. - const float minColCoverage = 0.30f; - // Stricter filter for the alifold input: noisy rows degrade the - // covariation signal, so feed only well-covered rows to alifold - // (default 0.70, override with RIBOSEEK_ALIFOLD_MINCOV). - float alifoldMinCov = 0.70f; - if (const char *envCov = std::getenv("RIBOSEEK_ALIFOLD_MINCOV")) { - float v = std::atof(envCov); - if (v > 0.0f && v <= 1.0f) alifoldMinCov = v; - } - - // Optional rMSA/hhfilter-style row filter on the cmbuild input MSA. Knobs - // map 1:1 to MMseqs MsaFilter: --cov-msa, --filter-max-seqid, --diff (Ndiff), - // --qid, --qsc, --filter-min-enable. Default is off. - const bool doMsaFilter = (par.filterMsa != 0); - SubstitutionMatrix subMat(par.scoringMatrixFile.values.aminoacid().c_str(), 2.0f, 0.0f); - const unsigned int targetExt = DBReader::getExtendedDbtype(tDbr->getDbtype()); - const bool targetGpuDb = (targetExt & Parameters::DBTYPE_EXTENDED_GPU) != 0; - const bool decodeTargetDinuc = ((targetExt & Parameters::DBTYPE_EXTENDED_DINUCLEOTIDE) != 0) || targetGpuDb; - const int targetSeqType = effectiveDecodeSeqType(tDbr->getDbtype(), decodeTargetDinuc); - std::vector qid_str_vec = Util::split(par.qid, ","); - std::vector qid_vec; - qid_vec.reserve(qid_str_vec.size()); - for (size_t i = 0; i < qid_str_vec.size(); ++i) { - float qidf = static_cast(std::atof(qid_str_vec[i].c_str())); - qid_vec.push_back(static_cast(qidf * 100)); - } - if (qid_vec.empty()) qid_vec.push_back(0); - std::sort(qid_vec.begin(), qid_vec.end()); - -#pragma omp parallel - { - unsigned int thread_idx = 0; -#ifdef OPENMP - thread_idx = (unsigned int) omp_get_thread_num(); -#endif - std::vector alnResults; - alnResults.reserve(300); - std::set seenKeys; - - // Per-thread MsaFilter scratch. Allocated once, reused across queries. - // maxSeqLen and maxSetSize grow inside MsaFilter as needed (increaseSetSize), - // so seeding with conservative values is fine. - MsaFilter msaFilter(8192, 1024, &subMat, par.gapOpen.values.aminoacid(), par.gapExtend.values.aminoacid()); - Sequence targetMapper(tDbr->getMaxSeqLen(), targetSeqType, &subMat, 0, false, false); - std::vector encBuf; - std::vector rowPtrs; - -#pragma omp for schedule(dynamic, 1) - for (size_t id = 0; id < resultReader.getSize(); id++) { - progress.updateProgress(); - alnResults.clear(); - seenKeys.clear(); - +bool buildQueryCmText(LocalParameters &par, CmBuildCtx &ctx, size_t id, + unsigned int thread_idx, std::string &cmText, std::string &err, + bool forScan, bool useInside, bool useLocal, + CM_t **ret_cm) { + DBReader &qDbr = *ctx.qDbr; + DBReader *tDbr = ctx.tDbr; + DBReader &resultReader = *ctx.resultReader; + SubstitutionMatrix &subMat = *ctx.subMat; + MsaFilter &msaFilter = *ctx.msaFilter; + Sequence &targetMapper = *ctx.targetMapper; + const std::vector &qid_vec = ctx.qid_vec; + const float alifoldMinCov = ctx.alifoldMinCov; + const float minColCoverage = ctx.minColCoverage; + const bool doMsaFilter = ctx.doMsaFilter; + const bool decodeTargetDinuc = ctx.decodeTargetDinuc; + const bool targetGpuDb = ctx.targetGpuDb; + std::vector alnResults; alnResults.reserve(300); + std::set seenKeys; + std::vector encBuf; + std::vector rowPtrs; unsigned int queryKey = resultReader.getDbKey(id); size_t queryId = qDbr.getId(queryKey); if (queryId == UINT_MAX) { - Debug(Debug::WARNING) << "cmbuild: invalid query " << queryKey << "\n"; - continue; + err = "invalid query " + std::to_string(queryKey); + return false; } // Render the query row directly from raw nucleotide DB chars. @@ -807,29 +621,203 @@ int cmbuild(int argc, const char **argv, const Command &command) { stoSeqs = std::move(keptSeqs); } - std::string stoText = buildStockholmText( - "query_" + std::to_string(queryKey), stoSeqs, ssCons); + const std::string modelName = "query_" + std::to_string(queryKey); + std::string buildErr; + std::vector sqnames; + std::vector aseqs; + sqnames.reserve(stoSeqs.size()); + aseqs.reserve(stoSeqs.size()); + for (const AlnSeq &s : stoSeqs) { + sqnames.push_back(s.id); + std::string row = s.aln; + convertTsToUs(row); + aseqs.push_back(std::move(row)); + } +#ifdef RIBOSEEK_CMBUILD_DUMP_STOCKHOLM + // Serializes seed MSA + SS_cons fed to Infernal cmbuild + std::ofstream sto(std::string(RIBOSEEK_CMBUILD_DUMP_STOCKHOLM) + "/" + modelName + ".sto"); + if (sto) { + sto << "# STOCKHOLM 1.0\n#=GF ID " << modelName << "\n"; + for (size_t i = 0; i < sqnames.size(); ++i) + sto << sqnames[i] << " " << aseqs[i] << "\n"; + if (!aseqs.empty() && !aseqs[0].empty()) + sto << "#=GC RF " << std::string(aseqs[0].size(), 'x') << "\n"; + if (!ssCons.empty()) + sto << "#=GC SS_cons " << ssCons << "\n"; + sto << "//\n"; + } +#endif + bool success = cmbuildFromAlignment(modelName, sqnames, aseqs, ssCons, cmText, buildErr, + par.cmbuildEre, par.cmbuildSymfrac, par.cmbuildNoss != 0, + forScan, useInside, useLocal, ret_cm); + if (!success) { err = "build failed for query " + std::to_string(queryKey) + ": " + buildErr; return false; } + return true; +} - if (const char *dumpPath = std::getenv("RIBOSEEK_DUMP_STOCKHOLM")) { - std::string outPath = std::string(dumpPath) + "/query_" + std::to_string(queryKey) + ".sto"; - FILE *fp = std::fopen(outPath.c_str(), "w"); - if (fp != nullptr) { - std::fwrite(stoText.data(), 1, stoText.size(), fp); - std::fclose(fp); - } +int cmbuild(int argc, const char **argv, const Command &command) { + LocalParameters &par = LocalParameters::getLocalInstance(); + // Defaults for the optional MSA row filter (off; rMSA-style cov+id+Ndiff + // when the user opts in via --filter-msa 1). qsc disabled because our + // substitution matrix is dinucleotide so qsc scores would be in the wrong + // space; cov + filterMaxSeqId + Ndiff are pure byte comparisons and unaffected. + par.filterMsa = 0; + par.qsc = -50.0f; + par.covMSAThr = 0.5f; + par.filterMaxSeqId = 0.99f; + par.Ndiff = 128; + + std::string rawTargetDbArg; + std::string rawResultDbArg; + std::vector parseArgStorage; + std::vector parseArgv; + if (argc > 2) { + rawTargetDbArg = argv[1]; + rawResultDbArg = argv[2]; + if (rawTargetDbArg.find(',') != std::string::npos + || rawResultDbArg.find(',') != std::string::npos) { + parseArgStorage.reserve(static_cast(argc)); + parseArgv.reserve(static_cast(argc)); + for (int i = 0; i < argc; ++i) { + parseArgStorage.push_back(argv[i]); + } + const std::vector targetParts = splitCommaList(rawTargetDbArg); + const std::vector resultParts = splitCommaList(rawResultDbArg); + if (!targetParts.empty()) { + parseArgStorage[1] = targetParts[0]; } + if (!resultParts.empty()) { + parseArgStorage[2] = resultParts[0]; + } + for (size_t i = 0; i < parseArgStorage.size(); ++i) { + parseArgv.push_back(parseArgStorage[i].c_str()); + } + } + } + par.parseParameters(argc, + parseArgv.empty() ? argv : parseArgv.data(), + command, + true, + 0, + 0); - // Call Infernal bridge to build CM. Bridge forks per call so OMP workers - // can build in parallel despite Infernal's process-wide global state. - std::string cmText; - std::string infernalErr; - bool success = InfernalBridge::buildCmFromStockholmText(workerPool, stoText, cmText, infernalErr, par.calibrateCm); + // initialize Infernal logsum tables once + cmInfernalGlobalInit(); - if (success) { - cmWriter.writeData(cmText.c_str(), cmText.size(), queryKey, thread_idx); - } else { - Debug(Debug::WARNING) << "cmbuild: Infernal failed for query " << queryKey - << ": " << infernalErr << "\n"; + const std::vector targetDbList = splitCommaList(rawTargetDbArg.empty() ? par.db2 : rawTargetDbArg); + const std::vector resultDbList = splitCommaList(rawResultDbArg.empty() ? par.db3 : rawResultDbArg); + if (targetDbList.size() != resultDbList.size()) { + Debug(Debug::ERROR) << "cmbuild: targetDB/resultDB list sizes differ (" + << targetDbList.size() << " vs " << resultDbList.size() << ")\n"; + return EXIT_FAILURE; + } + const bool useMergedInputs = (targetDbList.size() > 1 || resultDbList.size() > 1); + if (useMergedInputs) { + const std::string mergedTargetDb = par.db4 + "_target_merged"; + const std::string mergedResultDb = par.db4 + "_result_merged"; + removeDbFiles(mergedTargetDb); + removeDbFiles(mergedResultDb); + std::string mergeErr; + Debug(Debug::INFO) << "cmbuild: merging " << targetDbList.size() + << " target/result DB pairs next to output CM DB\n"; + if (!mergeCmbuildInputs(par.db1, targetDbList, resultDbList, + mergedTargetDb, mergedResultDb, + par.threads, mergeErr)) { + Debug(Debug::ERROR) << "cmbuild: failed to merge multi-DB input: " << mergeErr << "\n"; + return EXIT_FAILURE; + } + par.db2 = mergedTargetDb; + par.db2Index = mergedTargetDb + ".index"; + par.db3 = mergedResultDb; + par.db3Index = mergedResultDb + ".index"; + Debug(Debug::INFO) << "cmbuild: merged target DB: " << par.db2 << "\n"; + Debug(Debug::INFO) << "cmbuild: merged result DB: " << par.db3 << "\n"; + } + + DBReader qDbr(par.db1.c_str(), par.db1Index.c_str(), par.threads, + DBReader::USE_INDEX | DBReader::USE_DATA); + qDbr.open(DBReader::NOSORT); + + const bool sameDatabase = (par.db1 == par.db2); + DBReader *tDbr = &qDbr; + if (!sameDatabase) { + tDbr = new DBReader(par.db2.c_str(), par.db2Index.c_str(), par.threads, + DBReader::USE_INDEX | DBReader::USE_DATA); + tDbr->open(DBReader::NOSORT); + } + + DBReader resultReader(par.db3.c_str(), par.db3Index.c_str(), par.threads, + DBReader::USE_INDEX | DBReader::USE_DATA); + resultReader.open(DBReader::LINEAR_ACCCESS); + + DBWriter cmWriter(par.db4.c_str(), par.db4Index.c_str(), par.threads, + par.compressed, Parameters::DBTYPE_GENERIC_DB); + cmWriter.open(); + + Debug(Debug::INFO) << "Query database size: " << qDbr.getSize() << " type: " << qDbr.getDbTypeName() << "\n"; + Debug(Debug::INFO) << "Target database size: " << tDbr->getSize() << " type: " << tDbr->getDbTypeName() << "\n"; + { + std::ostringstream msaEvalStream; + msaEvalStream << std::scientific << par.cmliteMsaEvalThr; + Debug(Debug::INFO) << "cmbuild seed E-value threshold: " << msaEvalStream.str() << "\n"; + } + + Debug::Progress progress(resultReader.getSize()); + + // Stockholm rows are filtered: drop any target with non-gap coverage + // below this threshold. Infernal's cm_parsetree_Doctor() can fail when + // many short fragments dominate the column statistics. + const float minColCoverage = 0.30f; + // Stricter filter for the alifold input: noisy rows degrade the + // covariation signal, so feed only well-covered rows to alifold + // (default 0.70, override with RIBOSEEK_ALIFOLD_MINCOV). + float alifoldMinCov = 0.70f; + if (const char *envCov = std::getenv("RIBOSEEK_ALIFOLD_MINCOV")) { + float v = std::atof(envCov); + if (v > 0.0f && v <= 1.0f) alifoldMinCov = v; + } + + // Optional rMSA/hhfilter-style row filter on the cmbuild input MSA. Knobs + // map 1:1 to MMseqs MsaFilter: --cov-msa, --filter-max-seqid, --diff (Ndiff), + // --qid, --qsc, --filter-min-enable. Default is off. + const bool doMsaFilter = (par.filterMsa != 0); + SubstitutionMatrix subMat(par.scoringMatrixFile.values.aminoacid().c_str(), 2.0f, 0.0f); + const unsigned int targetExt = DBReader::getExtendedDbtype(tDbr->getDbtype()); + const bool targetGpuDb = (targetExt & Parameters::DBTYPE_EXTENDED_GPU) != 0; + const bool decodeTargetDinuc = ((targetExt & Parameters::DBTYPE_EXTENDED_DINUCLEOTIDE) != 0) || targetGpuDb; + const int targetSeqType = effectiveDecodeSeqType(tDbr->getDbtype(), decodeTargetDinuc); + std::vector qid_str_vec = Util::split(par.qid, ","); + std::vector qid_vec; + qid_vec.reserve(qid_str_vec.size()); + for (size_t i = 0; i < qid_str_vec.size(); ++i) { + float qidf = static_cast(std::atof(qid_str_vec[i].c_str())); + qid_vec.push_back(static_cast(qidf * 100)); + } + if (qid_vec.empty()) qid_vec.push_back(0); + std::sort(qid_vec.begin(), qid_vec.end()); + +#pragma omp parallel + { + unsigned int thread_idx = 0; +#ifdef OPENMP + thread_idx = (unsigned int) omp_get_thread_num(); +#endif + MsaFilter msaFilter(8192, 1024, &subMat, par.gapOpen.values.aminoacid(), par.gapExtend.values.aminoacid()); + Sequence targetMapper(tDbr->getMaxSeqLen(), targetSeqType, &subMat, 0, false, false); + CmBuildCtx ctx; + ctx.qDbr = &qDbr; ctx.tDbr = tDbr; ctx.resultReader = &resultReader; + ctx.subMat = &subMat; ctx.msaFilter = &msaFilter; ctx.targetMapper = &targetMapper; + ctx.qid_vec = qid_vec; ctx.alifoldMinCov = alifoldMinCov; ctx.minColCoverage = minColCoverage; + ctx.doMsaFilter = doMsaFilter; ctx.decodeTargetDinuc = decodeTargetDinuc; ctx.targetSeqType = targetSeqType; + ctx.targetGpuDb = targetGpuDb; + +#pragma omp for schedule(dynamic, 1) + for (size_t id = 0; id < resultReader.getSize(); id++) { + progress.updateProgress(); + std::string cmText, buildErr; + if (buildQueryCmText(par, ctx, id, thread_idx, cmText, buildErr)) { + cmWriter.writeData(cmText.c_str(), cmText.size(), resultReader.getDbKey(id), thread_idx); + } else if (!buildErr.empty()) { + Debug(Debug::WARNING) << "cmbuild: " << buildErr << "\n"; } } } @@ -842,7 +830,5 @@ int cmbuild(int argc, const char **argv, const Command &command) { } qDbr.close(); - InfernalBridge::stopWorkerPool(workerPool); return EXIT_SUCCESS; -#endif // HAVE_INFERNAL_BRIDGE } diff --git a/util/regression/regression/run_cmsearch.sh b/util/regression/regression/run_cmsearch.sh new file mode 100755 index 0000000..f9a8100 --- /dev/null +++ b/util/regression/regression/run_cmsearch.sh @@ -0,0 +1,24 @@ +#!/bin/sh -e +QUERY="${RESULTS}/query.fasta" +awk '/^>/{h=$0; getline s; if(length(s)<=500 && n<15){print h; print s; n++}}' \ + "${EXAMPLEDIR}/QUERY.fasta" > "${QUERY}" +QUERYDB="${RESULTS}/query" +"${RIBOSEEK}" createdb "${QUERY}" "${QUERYDB}" + +TARGET="${EXAMPLEDIR}/DB.fasta" +TARGETDB="${RESULTS}/target" +"${RIBOSEEK}" createdb "${TARGET}" "${TARGETDB}" +"${RIBOSEEK}" search "${QUERYDB}" "${TARGETDB}" "${RESULTS}/results" "${RESULTS}/tmp" +"${RIBOSEEK}" cmbuild "${QUERYDB}" "${TARGETDB}" "${RESULTS}/results" "${RESULTS}/cmDB" +"${RIBOSEEK}" cmscan "${RESULTS}/cmDB" "${TARGETDB}" "${RESULTS}/results" "${RESULTS}/cmAln" + +"${RIBOSEEK}" convertalis "${QUERYDB}" "${TARGETDB}" "${RESULTS}/results" "${RESULTS}/search.m8" +"${RIBOSEEK}" convertalis "${QUERYDB}" "${TARGETDB}" "${RESULTS}/cmAln" "${RESULTS}/cm.m8" + +# total aligned columns +SEARCH=$(awk '{s+=$4} END {print s}' "${RESULTS}/search.m8") +ACTUAL=$(awk '{s+=$4} END {print s}' "${RESULTS}/cm.m8") +EXPECTED="48686" +awk -v actual="$ACTUAL" -v expected="$EXPECTED" -v search="$SEARCH" \ + 'BEGIN { print (actual == expected && actual > search) ? "GOOD" : "BAD"; print "Expected: ", expected; print "Actual: ", actual; print "Search baseline: ", search; }' \ + > "${RESULTS}.report" diff --git a/util/regression/run_regression.sh b/util/regression/run_regression.sh index 31ec9a6..6639afb 100755 --- a/util/regression/run_regression.sh +++ b/util/regression/run_regression.sh @@ -63,6 +63,7 @@ set +e run_test SEARCH "run_search.sh" run_test SEARCH_INDEX "run_search_index.sh" run_test SEARCH_GAPLESS_INDEX "run_search_gapless_index.sh" +run_test CMSEARCH "run_cmsearch.sh" if [ -n "${RIBOSEEK_GPU_TESTS}" ]; then run_test SEARCH_GPU_INDEX "run_search_gpu_index.sh" fi diff --git a/util/update_tornadofold.sh b/util/update_tornadofold.sh new file mode 100755 index 0000000..8ceff3e --- /dev/null +++ b/util/update_tornadofold.sh @@ -0,0 +1,2 @@ +#!/bin/sh -e +git subtree pull --prefix lib/tornadofold https://github.com/mirditalab/ToRNAdoFold.git main --squash