Author: Brody Kendall, Ji-Ping Wang, and Keren Li

# DNAcycP2: DNA Cyclizability Prediction v2

Maintainer: Ji-Ping Wang, <jzwang@northwestern.edu>

References for methods:

Kendall, B., Jin, C., Li, K., Ruan, F., Wang, X.A., Wang, J.-P., DNAcycP2: improved estimation of intrinsic DNA cyclizability through data augmentation, Nucleic Acids Research, gkaf145, 2025.

## Introduction

DNAcycP2, short for DNA cyclizability Prediction v2, is an R package (Python version is also available) developed for precise and unbiased prediction of DNA intrinsic cyclizability scores. This tool builds on a deep learning framework that integrates Inception and Residual network architectures with an LSTM layer, providing a robust and accurate prediction mechanism.

DNAcycP2 is an updated version of the earlier DNAcycP tool released by Li et al. in 2021. While DNAcycP was trained on loop-seq data from Basu et al. (2021), DNAcycP2 improves upon it by training on smoothed predictions derived from this dataset. The predicted score, termed C-score, exhibits high accuracy when compared with experimentally measured cyclizability scores obtained from the loop-seq assay. This makes DNAcycP2 a valuable tool for researchers studying DNA mechanics and structure.

### Key differences between DNAcycP2 and DNAcycP

Following the release of DNAcycP, it was found that the intrinsic cyclizability scores derived from Basu et al. (2021) retained residual bias from the biotin effect, resulting in inaccuracies (Kendall et al., 2025). To address this, we employed a data augmentation + moving average smoothing method to produce unbiased estimates of intrinsic DNA cyclizability for each sequence in the original training dataset. A new model, trained on this corrected data but using the same architecture as DNAcycP, was developed, resulting in DNAcycP2. This version also introduces improved computational efficiency through parallelization options. Further details are available in Kendall et al. (2025).

To demonstrate the differences, we compared predictions from DNAcycP and DNAcycP2 in a yeast genomic region at base-pair resolution (Figure 1). The predicted biotin-dependent scores (\(\tilde C_{26}\), \(\tilde C_{29}\), and $ C_{31}$, model trained separately) show 10-bp periodic oscillations due to biotin biases, each with distinct phases. DNAcycP’s predictions improved over the biotin-dependent scores, while still show substantial local fluctuations likely caused by residual bias in the training data (the called intrinsic cyclizability score \(\hat C_0\) from Basu et al. 2021). In contrast, DNAcycP2, trained on corrected intrinsic cyclizability scores, produces much smoother local-scale predictions, indicating a further improvement in removing the biotin bias.

The DNAcycP2 package retains all prediction functions from the original DNAcycP. The improved prediction model, based on smoothed data, can be accessed using the argument smooth=TRUE in the main function (see usage below).

### Available formats of DNAcycP2 and DNAcycP

DNAcycP2 is available in three formats: A web server available at http://DNAcycP.stats.northwestern.edu for real-time prediction and visualization of C-score up to 20K bp, a standalone Python package avilable for free download from https://github.com/jipingw/DNAcycP2-Python, and a new R package available for free download from bioconductor (https://github.com/jipingw/DNAcycP2). DNAcycP2 R package is a wrapper of its Python version, both generate the same prediction results.

DNAcycP Python package is still available for free download from https://github.com/jipingw/DNAcycP. As DNAcycP2 include all functionalities of DNAcycP, users can generate all DNAcycP results using DNAcycP2.

## Installation

DNAcycP2 is available on Bioconductor with `R >= 4.5.0`. To install it, run the following command:

    if (!require("BiocManager", quietly = TRUE))
        install.packages("BiocManager")
    BiocManager::install("DNAcycP2")

## Usage

### Main Functions

The DNAcycP2 R package provides two primary functions for cyclizability prediction:

1. `cycle`: Takes an R object (vector of strings) as input. Each element in the vector is a DNA sequence.
2. `cycle_fasta`: Takes the path of a fasta file as input.

### Selecting the Prediction Model

Both functions use the `smooth` argument to specify the prediction model:

- `smooth=TRUE`: DNAcycP2 (trained on smoothed data, recommended).
- `smooth=FALSE`: DNAcycP (trained on original data).

### Parallelization with `cycle_fasta`

The `cycle_fasta` function is designed for handling larger files and supports parallelization. To enable parallelization, use the following arguments:

- `n_cores`: Number of cores to use (default: 1).
- `chunk_length`: Sequence length (in bp) each core processes at a time (default: 100,000).

The `cycle_fasta` function is designed for larger files, so it has added parallelization capability. To utilize this capability, specify the number of cores to be greater than 1 using the `n_cores` argument (default 1). You can also specify the length of the sequence that each core will predict on at a given time using the `chunk_length` argument (default 100000).

For reference, on a personal computer (16 Gb RAM, M1 chip with 8-core CPU), prediction at full parallelization directly on the yeast genome FASTA file completes in 12 minutes, and on the hg38 human genome Chromosome I FASTA file in just over 4 hours. In our experience, selection of parallelization parameters (`n_cores` and `chunk_length`) has little affect when making predictions on a personal computer, but if using the package on a high- performance compute cluster, prediction time should decrease as the number of cores increases. If you do run into memory issues, we first suggest reducing `chunk_length`.

    library(DNAcycP2)

### Example 1: fasta file input

    ex1_file <- system.file("extdata", "ex1.fasta", package = "DNAcycP2")
    ex1_smooth <- DNAcycP2::cycle_fasta(
        ex1_file, smooth=TRUE, n_cores=1, chunk_length=1000
    )
    #> Installing pyenv ...
    #> Done! pyenv has been installed to '/github/home/.local/share/r-reticulate/pyenv/bin/pyenv'.
    #> Using Python: /github/home/.pyenv/versions/3.11.15/bin/python3.11
    #> Creating virtual environment '/github/home/.cache/R/basilisk/1.24.0/DNAcycP2/1.4.1/env1' ...
    #> + /github/home/.pyenv/versions/3.11.15/bin/python3.11 -m venv /github/home/.cache/R/basilisk/1.24.0/DNAcycP2/1.4.1/env1
    #> Done!
    #> Installing packages: pip, wheel, setuptools
    #> + /github/home/.cache/R/basilisk/1.24.0/DNAcycP2/1.4.1/env1/bin/python -m pip install --upgrade pip wheel setuptools
    #> Installing packages: 'pandas==2.1.2', 'docopt==0.6.2', 'numpy==1.26.1', 'bio==1.7.1', 'tensorflow==2.14.0', 'keras==2.14.0'
    #> + /github/home/.cache/R/basilisk/1.24.0/DNAcycP2/1.4.1/env1/bin/python -m pip install --upgrade --no-user 'pandas==2.1.2' 'docopt==0.6.2' 'numpy==1.26.1' 'bio==1.7.1' 'tensorflow==2.14.0' 'keras==2.14.0'
    #> Virtual environment '/github/home/.cache/R/basilisk/1.24.0/DNAcycP2/1.4.1/env1' successfully created.
    #> Sequence length for ID 1: 12480
    #> Predicting cyclizability...
    #> Chunk size: 1000, num threads: 1
    #> Sequence length for ID 2: 7260
    #> Predicting cyclizability...
    #> Chunk size: 1000, num threads: 1
    ex1_original <- DNAcycP2::cycle_fasta(
        ex1_file, smooth=FALSE, n_cores=1, chunk_length=1000
    )
    #> Sequence length for ID 1: 12480
    #> Predicting cyclizability...
    #> Chunk size: 1000, num threads: 1
    #> Sequence length for ID 2: 7260
    #> Predicting cyclizability...
    #> Chunk size: 1000, num threads: 1

`cycle_fasta` takes the file path as input (`ex1_file`). `smooth=TRUE` specifies that DNAcycP2 be used to make predictions. `smooth=FALSE` specifies that DNAcycP be used to make predictions. `n_cores=2` specifies that 2 cores are to be used in parallel. `chunk_length=1000` specifies that each core will predict on sequences of length 1000 at a given time.

The output (`ex1_smooth` or `ex1_original`) is a list with element names starting with “cycle” followed by the sequence names in the fasta file. For example, `ex1.fasta` contains two sequences with IDs “1” and “2” respectively. Therefore both both `ex1_smooth` and `ex1_original` will be lists of length 2 with names `cycle_1` and `cycle_2` for the first and second sequences respectively.

Each item in the list (e.g. `ex1_smooth$cycle_1`) is a data.frame object with three columns. The first column is always `position`. When `smooth=TRUE`, the second and third columns are `C0S_norm` and `C0S_unnorm`, and when `smooth=FALSE` the second and third columns are `C0_norm` and `C0_unnorm`.

### Example 2: input as a list/vector of sequences

    ex2_file <- system.file("extdata", "ex2.txt", package = "DNAcycP2")
    ex2 <- read.csv(ex2_file, header = FALSE)
    ex2_smooth <- DNAcycP2::cycle(ex2$V1, smooth=TRUE)
    #> Reading sequences...
    #> Not all sequences are length 50, predicting every subsequence...
    #> Completed 10 out of 100 total sequences
    #> Completed 20 out of 100 total sequences
    #> Completed 30 out of 100 total sequences
    #> Completed 40 out of 100 total sequences
    #> Completed 50 out of 100 total sequences
    #> Completed 60 out of 100 total sequences
    #> Completed 70 out of 100 total sequences
    #> Completed 80 out of 100 total sequences
    #> Completed 90 out of 100 total sequences
    #> Completed 100 out of 100 total sequences
    ex2_original <- DNAcycP2::cycle(ex2$V1, smooth=FALSE)
    #> Reading sequences...
    #> Not all sequences are length 50, predicting every subsequence...
    #> Completed 10 out of 100 total sequences
    #> Completed 20 out of 100 total sequences
    #> Completed 30 out of 100 total sequences
    #> Completed 40 out of 100 total sequences
    #> Completed 50 out of 100 total sequences
    #> Completed 60 out of 100 total sequences
    #> Completed 70 out of 100 total sequences
    #> Completed 80 out of 100 total sequences
    #> Completed 90 out of 100 total sequences
    #> Completed 100 out of 100 total sequences

`cycle` takes the sequences themselves as input, so we first read the file (`ex2_file`) and then provide the sequences as input (`ex2$V1`)

The output (`ex2_smooth` or `ex2_original`) is a list with indices corresponding to each sequence from the `sequences` argument (here it is `ex2$V1`). For example, `ex2.txt` contains 100 sequences. Therefore, both `ex2_smooth` and `ex2_original` will be lists of length 100,
where each entry in the list corresponds to the sequence with its same index.

Each item in the list (e.g. `ex2_smooth[[1]]`) is a data.frame object with three columns. The first columns is always `position`. When `smooth=TRUE`, the second and third columns are `C0S_norm` and `C0S_unnorm`, and when `smooth=FALSE` the second and third columns are `C0_norm` and `C0_unnorm`.

### DNAcycP2 prediction – Normalized vs unnormalized

Both `cycle_fasta` and `cycle` output the prediction results in normalized (`C0_norm`,`C0S_norm`) and unnomralized (`C0_unnorm`,`C0S_unnorm`) version.

In DNAcycP2, the predicted cyclizability always contains normalized and unnormalized values. the unnormalized results were based on the model trained on unnormalized \(\hat C_0\) or \(\hat C_0^s\) scores. In contrast, the normalized results were predicted by the model trained on the normalized \(\hat C_0\) or \(\hat C_0^s\) values. The cyclizability score from different loop-seq libraries may be subject to a systematic library-specific constant difference due to its definition (see
Basu et al 2021), and hence it’s a relative measure and not directly comparable between libraries. The normalization will force the training data to have mean = 0 and standard deviation = 1 such that the 50 bp sequences from yeast genome roughly have mean = 0 and standard deviation = 1 for intrinsic cyclizabilty score. Thus for any sequence under prediciton, the normalized C-score can be more informative in terms of its cyclizabilty relative to the population. For example, the C-score provides statisitcal significance indicator, i.e. a C-score of 1.96 indicates 97.5% in the distribution.

### Save DNAcycP2 prediciton to external file

Both `cycle_fasta` and `cycle` provides an argument `save_path_prefix` to save the prediction results onto local hard drive. For example:

    ex2_smooth <- DNAcycP2::cycle(
        ex2$V1,
        smooth=TRUE,
        save_path_prefix="ex2_smooth"
    )

This will execute the same predictions as previously, and additionally save two files named ‘ex2_smooth_C0S_norm.txt’ and ‘ex2_smooth_C0S_unnorm.txt’ to the current working directory. The output files from `cycle_fasta` have the same format as the function output, but for consistency with the Python pacakge it is important to note that the output files from `cycle` have a different format than the function output. Namely, rather than writing a single file for every sequence, the function always writes two files (regardless of the number of sequences), one containing normalized predictions for every sequence (ending in ‘C0S_norm.txt’ or ‘C0_norm.txt’) and the other containing unnormalized predictions for every sequence (ending in ‘C0S_unnorm.txt’ or ‘C0_unnorm.txt’). C-scores in each line correspond to the sequence from the `sequences` input in the same order.

For any input sequence, DNAcycP2 predicts the C-score for every 50 bp. Regardless of the input sequence format the first C-score in the output file corresponds to the sequence from position 1-50, second for 2-51 and so forth.

### Example 3 (Single Sequence):

If you want the predict C-scores for a single sequence, you can follow the same protocol as Example 1 or 2, depending on the input format. We have included two example files representing the same 1000bp stretch of S. Cerevisiae sacCer3 Chromosome I (1:1000) in .fasta and .txt format.

First, we will consider the .fasta format:

    ex3_fasta_file <- system.file(
        "extdata", "ex3_single_seq.fasta", package = "DNAcycP2"
    )
    ex3_fasta_smooth <- DNAcycP2::cycle_fasta(ex3_fasta_file,smooth=TRUE)
    #> Sequence length for ID 1: 1000
    #> Predicting cyclizability...
    #> Chunk size: 100000, num threads: 1
    ex3_fasta_original <- DNAcycP2::cycle_fasta(ex3_fasta_file,smooth=FALSE)
    #> Sequence length for ID 1: 1000
    #> Predicting cyclizability...
    #> Chunk size: 100000, num threads: 1

The output (`ex3_fasta_smooth` or `ex3_fasta_original`) is a list with 1 entry named “cycle_1”.

Let’s say we are interested only in the smooth (DNAcycP2), normalized predictions for the subsequence defined by the first 100bp (corresponding to subsequences defined by regions [1,50], [2,51], …, and [51-100], or `position`s 25, 26, …, and 75). We can access the outputs for this subsequence using the following command:

    ex3_fasta_smooth[[1]][1:51,c("position", "C0S_norm")]
    #>    position    C0S_norm
    #> 1        25  0.94794816
    #> 2        26  0.88397890
    #> 3        27  1.05313051
    #> 4        28  1.31837559
    #> 5        29  1.43364513
    #> 6        30  1.47490740
    #> 7        31  1.68856978
    #> 8        32  1.79380059
    #> 9        33  1.78517425
    #> 10       34  1.51873469
    #> 11       35  1.83588862
    #> 12       36  1.81811321
    #> 13       37  1.89403594
    #> 14       38  1.94740176
    #> 15       39  1.67138922
    #> 16       40  1.55992007
    #> 17       41  1.66846383
    #> 18       42  1.62409902
    #> 19       43  1.54238105
    #> 20       44  1.46230042
    #> 21       45  1.41134238
    #> 22       46  1.27757096
    #> 23       47  1.02153277
    #> 24       48  1.01221049
    #> 25       49  0.93556392
    #> 26       50  1.00608754
    #> 27       51  1.09152961
    #> 28       52  1.32879055
    #> 29       53  1.25343060
    #> 30       54  1.34268773
    #> 31       55  1.13988316
    #> 32       56  1.00737357
    #> 33       57  1.13837850
    #> 34       58  1.11469972
    #> 35       59  0.96556205
    #> 36       60  0.85989273
    #> 37       61  0.91100574
    #> 38       62  0.87353730
    #> 39       63  0.55437249
    #> 40       64  0.60366714
    #> 41       65  0.41495007
    #> 42       66  0.22158630
    #> 43       67  0.14434634
    #> 44       68  0.08841632
    #> 45       69 -0.09656693
    #> 46       70 -0.19039944
    #> 47       71 -0.34424984
    #> 48       72 -0.41227320
    #> 49       73 -0.38969812
    #> 50       74 -0.33039850
    #> 51       75 -0.44366425

Or, equivalently,

    ex3_fasta_smooth$cycle_1[1:51,c("position", "C0S_norm")]
    #>    position    C0S_norm
    #> 1        25  0.94794816
    #> 2        26  0.88397890
    #> 3        27  1.05313051
    #> 4        28  1.31837559
    #> 5        29  1.43364513
    #> 6        30  1.47490740
    #> 7        31  1.68856978
    #> 8        32  1.79380059
    #> 9        33  1.78517425
    #> 10       34  1.51873469
    #> 11       35  1.83588862
    #> 12       36  1.81811321
    #> 13       37  1.89403594
    #> 14       38  1.94740176
    #> 15       39  1.67138922
    #> 16       40  1.55992007
    #> 17       41  1.66846383
    #> 18       42  1.62409902
    #> 19       43  1.54238105
    #> 20       44  1.46230042
    #> 21       45  1.41134238
    #> 22       46  1.27757096
    #> 23       47  1.02153277
    #> 24       48  1.01221049
    #> 25       49  0.93556392
    #> 26       50  1.00608754
    #> 27       51  1.09152961
    #> 28       52  1.32879055
    #> 29       53  1.25343060
    #> 30       54  1.34268773
    #> 31       55  1.13988316
    #> 32       56  1.00737357
    #> 33       57  1.13837850
    #> 34       58  1.11469972
    #> 35       59  0.96556205
    #> 36       60  0.85989273
    #> 37       61  0.91100574
    #> 38       62  0.87353730
    #> 39       63  0.55437249
    #> 40       64  0.60366714
    #> 41       65  0.41495007
    #> 42       66  0.22158630
    #> 43       67  0.14434634
    #> 44       68  0.08841632
    #> 45       69 -0.09656693
    #> 46       70 -0.19039944
    #> 47       71 -0.34424984
    #> 48       72 -0.41227320
    #> 49       73 -0.38969812
    #> 50       74 -0.33039850
    #> 51       75 -0.44366425

Next, we will consider the .txt format:

    ex3_txt_file <- system.file(
        "extdata",
        "ex3_single_seq.txt",
        package = "DNAcycP2"
    )
    ex3_txt <- read.csv(ex3_txt_file, header = FALSE)
    ex3_txt_smooth <- DNAcycP2::cycle(ex3_txt$V1, smooth=TRUE)
    #> Reading sequences...
    #> Not all sequences are length 50, predicting every subsequence...
    ex3_txt_original <- DNAcycP2::cycle(ex3_txt$V1, smooth=FALSE)
    #> Reading sequences...
    #> Not all sequences are length 50, predicting every subsequence...

The output (`ex3_txt_smooth` or `ex3_txt_original`) is a list with 1 entry (unnamed).

Note, that `ex3_fasta_smooth` and `ex3_txt_smooth` are essentially equivalent. The only exceptions are perhaps slight rounding differences that come from the computation, and that the list `ex3_fasta_smooth` has named entries (‘cycle_1’) while `ex3_txt_smooth` does not. The same applies for `ex3_fasta_original` and `ex3_txt_original`.

Therefore, we can use a similar command to access the outputs for our subsequence of interest:

    ex3_txt_smooth[[1]][1:51,c("position", "C0S_norm")]

If there is a sequence (or group of sequences) we want to make predictions on, we can also input them directly as strings. For example:

    input_seq1 =
        "CATGACTGCAGCTAAAACGTTGACCTAGTCGTCAGTCTACGTACTAGCGTAGCTATATCGAGTCTAGCGTCTAG"
    input_seq2 = "ATCTTTTGTATATCAAAAGACTAGATCGATTAGCGTACGCCCCTGACTAGATAGATCG"
    seq1_smooth = DNAcycP2::cycle(c(input_seq1), smooth=TRUE)
    both_seqs_smooth = DNAcycP2::cycle(c(input_seq1, input_seq2), smooth=TRUE)

### Example 4: `DNAStringSet` object input

    library(Biostrings)
    #> Loading required package: BiocGenerics
    #> Loading required package: generics
    #>
    #> Attaching package: 'generics'
    #> The following objects are masked from 'package:base':
    #>
    #>     as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
    #>     setequal, union
    #>
    #> Attaching package: 'BiocGenerics'
    #> The following objects are masked from 'package:stats':
    #>
    #>     IQR, mad, sd, var, xtabs
    #> The following objects are masked from 'package:base':
    #>
    #>     anyDuplicated, aperm, append, as.data.frame, basename, cbind,
    #>     colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find,
    #>     get, grep, grepl, is.unsorted, lapply, Map, mapply, match, mget,
    #>     order, paste, pmax, pmax.int, pmin, pmin.int, Position, rank,
    #>     rbind, Reduce, rownames, sapply, saveRDS, table, tapply, unique,
    #>     unsplit, which.max, which.min
    #> Loading required package: S4Vectors
    #> Loading required package: stats4
    #>
    #> Attaching package: 'S4Vectors'
    #> The following object is masked from 'package:utils':
    #>
    #>     findMatches
    #> The following objects are masked from 'package:base':
    #>
    #>     expand.grid, I, unname
    #> Loading required package: IRanges
    #> Loading required package: XVector
    #> Loading required package: Seqinfo
    #>
    #> Attaching package: 'Biostrings'
    #> The following object is masked from 'package:base':
    #>
    #>     strsplit
    ex4_string_set <- readDNAStringSet(system.file("extdata", "ex1.fasta", package="DNAcycP2"))
    ex4_smooth_output <- DNAcycP2::cycle(ex4_string_set, smooth=TRUE)
    #> Reading sequences...
    #> Not all sequences are length 50, predicting every subsequence...
    #>   1/389 [..............................] - ETA: 2:02 16/389 [>.............................] - ETA: 1s   32/389 [=>............................] - ETA: 1s 48/389 [==>...........................] - ETA: 1s 65/389 [====>.........................] - ETA: 1s 83/389 [=====>........................] - ETA: 0s100/389 [======>.......................] - ETA: 0s117/389 [========>.....................] - ETA: 0s133/389 [=========>....................] - ETA: 0s150/389 [==========>...................] - ETA: 0s166/389 [===========>..................] - ETA: 0s183/389 [=============>................] - ETA: 0s199/389 [==============>...............] - ETA: 0s216/389 [===============>..............] - ETA: 0s233/389 [================>.............] - ETA: 0s249/389 [==================>...........] - ETA: 0s266/389 [===================>..........] - ETA: 0s283/389 [====================>.........] - ETA: 0s299/389 [======================>.......] - ETA: 0s315/389 [=======================>......] - ETA: 0s331/389 [========================>.....] - ETA: 0s347/389 [=========================>....] - ETA: 0s362/389 [==========================>...] - ETA: 0s376/389 [===========================>..] - ETA: 0s389/389 [==============================] - 2s 3ms/step
    #>   1/389 [..............................] - ETA: 6s 17/389 [>.............................] - ETA: 1s 33/389 [=>............................] - ETA: 1s 49/389 [==>...........................] - ETA: 1s 65/389 [====>.........................] - ETA: 1s 82/389 [=====>........................] - ETA: 0s 99/389 [======>.......................] - ETA: 0s116/389 [=======>......................] - ETA: 0s132/389 [=========>....................] - ETA: 0s148/389 [==========>...................] - ETA: 0s164/389 [===========>..................] - ETA: 0s181/389 [============>.................] - ETA: 0s198/389 [==============>...............] - ETA: 0s215/389 [===============>..............] - ETA: 0s232/389 [================>.............] - ETA: 0s249/389 [==================>...........] - ETA: 0s266/389 [===================>..........] - ETA: 0s282/389 [====================>.........] - ETA: 0s299/389 [======================>.......] - ETA: 0s316/389 [=======================>......] - ETA: 0s334/389 [========================>.....] - ETA: 0s351/389 [==========================>...] - ETA: 0s368/389 [===========================>..] - ETA: 0s386/389 [============================>.] - ETA: 0s389/389 [==============================] - 1s 3ms/step
    #>   1/226 [..............................] - ETA: 3s 17/226 [=>............................] - ETA: 0s 33/226 [===>..........................] - ETA: 0s 49/226 [=====>........................] - ETA: 0s 65/226 [=======>......................] - ETA: 0s 82/226 [=========>....................] - ETA: 0s 99/226 [============>.................] - ETA: 0s117/226 [==============>...............] - ETA: 0s135/226 [================>.............] - ETA: 0s151/226 [===================>..........] - ETA: 0s168/226 [=====================>........] - ETA: 0s185/226 [=======================>......] - ETA: 0s202/226 [=========================>....] - ETA: 0s220/226 [============================>.] - ETA: 0s226/226 [==============================] - 1s 3ms/step
    #>   1/226 [..............................] - ETA: 3s 18/226 [=>............................] - ETA: 0s 35/226 [===>..........................] - ETA: 0s 52/226 [=====>........................] - ETA: 0s 69/226 [========>.....................] - ETA: 0s 86/226 [==========>...................] - ETA: 0s103/226 [============>.................] - ETA: 0s120/226 [==============>...............] - ETA: 0s137/226 [=================>............] - ETA: 0s153/226 [===================>..........] - ETA: 0s170/226 [=====================>........] - ETA: 0s187/226 [=======================>......] - ETA: 0s204/226 [==========================>...] - ETA: 0s221/226 [============================>.] - ETA: 0s226/226 [==============================] - 1s 3ms/step

`ex4_string_set` here is a `DNAStringSet` object using `readDNAStringSet` function from `Biostrings` package.

## References

- Li, K., Carroll, M., Vafabakhsh, R., Wang, X.A. and Wang, J.-P., DNAcycP: A Deep Learning Tool for DNA Cyclizability Prediction, Nucleic Acids Research, 2021
- Basu, A., Bobrovnikov, D.G., Qureshi, Z., Kayikcioglu, T., Ngo, T.T.M., Ranjan, A., Eustermann, S., Cieza, B., Morgan, M.T., Hejna, M. et al. (2021) Measuring DNA mechanics on the genome scale. Nature, 589, 462-467.

# Session info

    sessionInfo()
    #> R version 4.6.0 (2026-04-24)
    #> Platform: x86_64-pc-linux-gnu
    #> Running under: Ubuntu 24.04.4 LTS
    #>
    #> Matrix products: default
    #> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
    #> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
    #>
    #> locale:
    #>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C
    #>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8
    #>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8
    #>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C
    #>  [9] LC_ADDRESS=C               LC_TELEPHONE=C
    #> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
    #>
    #> time zone: Etc/UTC
    #> tzcode source: system (glibc)
    #>
    #> attached base packages:
    #> [1] stats4    stats     graphics  grDevices utils     datasets  methods
    #> [8] base
    #>
    #> other attached packages:
    #> [1] Biostrings_2.80.0   Seqinfo_1.2.0       XVector_0.52.0
    #> [4] IRanges_2.46.0      S4Vectors_0.50.0    BiocGenerics_0.58.0
    #> [7] generics_0.1.4      DNAcycP2_1.4.1      BiocStyle_2.40.0
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] Matrix_1.7-5        jsonlite_2.0.0      crayon_1.5.3
    #>  [4] compiler_4.6.0      BiocManager_1.30.27 filelock_1.0.3
    #>  [7] Rcpp_1.1.1-1.1      parallel_4.6.0      jquerylib_0.1.4
    #> [10] png_0.1-9           yaml_2.3.12         fastmap_1.2.0
    #> [13] reticulate_1.46.0   lattice_0.22-9      R6_2.6.1
    #> [16] knitr_1.51          maketools_1.3.2     bslib_0.10.0
    #> [19] rlang_1.2.0         cachem_1.1.0        dir.expiry_1.20.0
    #> [22] xfun_0.57           sass_0.4.10         sys_3.4.3
    #> [25] cli_3.6.6           withr_3.0.2         digest_0.6.39
    #> [28] grid_4.6.0          basilisk_1.24.0     rappdirs_0.3.4
    #> [31] lifecycle_1.0.5     evaluate_1.0.5      buildtools_1.0.0
    #> [34] rmarkdown_2.31      tools_4.6.0         htmltools_0.5.9