energorna 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- energorna-0.1.0/LICENSE +30 -0
- energorna-0.1.0/PKG-INFO +179 -0
- energorna-0.1.0/README.md +154 -0
- energorna-0.1.0/energorna/__init__.py +23 -0
- energorna-0.1.0/energorna/cli.py +88 -0
- energorna-0.1.0/energorna/engine.py +118 -0
- energorna-0.1.0/energorna.egg-info/PKG-INFO +179 -0
- energorna-0.1.0/energorna.egg-info/SOURCES.txt +13 -0
- energorna-0.1.0/energorna.egg-info/dependency_links.txt +1 -0
- energorna-0.1.0/energorna.egg-info/entry_points.txt +2 -0
- energorna-0.1.0/energorna.egg-info/requires.txt +1 -0
- energorna-0.1.0/energorna.egg-info/top_level.txt +1 -0
- energorna-0.1.0/pyproject.toml +36 -0
- energorna-0.1.0/setup.cfg +4 -0
- energorna-0.1.0/tests/test_ground_truth.py +68 -0
energorna-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jose Antonio Vilar Sánchez (QMetrika Labs)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
NOTICE — The MIT license above covers this SOURCE CODE. The underlying METHOD
|
|
26
|
+
(mRNA nearest-neighbor thermodynamic fingerprinting for variant scoring) is
|
|
27
|
+
covered by patent P202630522 (OEPM, Spain). Commercial use of the patented
|
|
28
|
+
method may require a separate license. Research and academic use is unaffected.
|
|
29
|
+
|
|
30
|
+
This software is for RESEARCH USE ONLY and is not a medical device.
|
energorna-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: energorna
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Glass-box local periodicity-loss observable of the mRNA stacking profile for repeat-expansion disorders (DM1, Huntington, SCA1)
|
|
5
|
+
Author-email: Jose Antonio Vilar Sanchez <qmetrika@proton.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/josevilar-qbioai/energorna
|
|
8
|
+
Project-URL: Repository, https://github.com/josevilar-qbioai/energorna
|
|
9
|
+
Project-URL: Issues, https://github.com/josevilar-qbioai/energorna/issues
|
|
10
|
+
Keywords: repeat expansion,myotonic dystrophy,huntington,SCA1,trinucleotide,age at onset,mRNA,thermodynamics,interruptions
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: numpy>=1.20
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# energorna
|
|
27
|
+
|
|
28
|
+
[](https://github.com/josevilar-qbioai/energorna/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/energorna/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
**Glass-box local periodicity-loss observable of the mRNA stacking-energy profile, for
|
|
33
|
+
repeat-expansion disorders.** Numpy-only, no ViennaRNA / GPU / API. Part of the QMetrika
|
|
34
|
+
Bio-IA thermodynamic engine (same Turner nearest-neighbor parameters as `ef-synonymous`).
|
|
35
|
+
|
|
36
|
+
*[English](#english) · [Español](#español)*
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## English
|
|
41
|
+
|
|
42
|
+
### The idea
|
|
43
|
+
|
|
44
|
+
In repeat-expansion diseases (myotonic dystrophy type 1, Huntington, SCA1, DM2…), age at
|
|
45
|
+
onset is not set only by *how many* repeats there are, but by whether the repeat tract is
|
|
46
|
+
**interrupted**. Interruptions (e.g. CCG in the CTG tract of DM1, CAA in the CAG tract of
|
|
47
|
+
Huntington, CAT in SCA1) break the **periodicity** of the nearest-neighbor stacking energy
|
|
48
|
+
profile Φ, disrupt the slipped-strand substrate that drives somatic instability, and
|
|
49
|
+
**delay onset**.
|
|
50
|
+
|
|
51
|
+
`energorna` computes a single glass-box observable — `periodicity_loss = 1 − ACF_period(Φ)`,
|
|
52
|
+
the loss of autocorrelation (at the motif period) of the Turner stacking profile — that
|
|
53
|
+
captures this. It **auto-calibrates the period** per motif (3 for trinucleotides, 4 for the
|
|
54
|
+
CCTG tetranucleotide) and, unlike simply counting interruptions, is sensitive to their
|
|
55
|
+
**type** and **position**.
|
|
56
|
+
|
|
57
|
+
### Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install energorna
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Use — CLI
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# pure (CTG)40 vs an interrupted allele
|
|
67
|
+
energorna score --repeat CTG --n 40
|
|
68
|
+
energorna score --seq CTGCTGCTG...CCGCTG... --motif CTG --json
|
|
69
|
+
energorna score --seq-file allele.fasta --motif CAG
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output shows the observable next to the traditional baselines:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
motif CTG (period 3) · 62 units · longest uninterrupted run 51
|
|
76
|
+
periodicity_loss = 0.18xx <- observable
|
|
77
|
+
periodicity_loss (3') = 0.19xx
|
|
78
|
+
n_interruptions = 4 (count baseline)
|
|
79
|
+
fragmentation_index = 0.09xx (fragmentation baseline)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Use — Python
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import energorna as ef
|
|
86
|
+
|
|
87
|
+
ef.calibrate_lag("CCTG") # -> 4 (auto-detects the motif period)
|
|
88
|
+
ef.periodicity_loss(seq, unit="CTG") # global observable
|
|
89
|
+
ef.periodicity_profile(seq, unit="CTG") # (pos, loss) resolved 5'->3'
|
|
90
|
+
ef.periodicity_loss_3prime(seq, unit="CTG") # 3'-weighted
|
|
91
|
+
ef.n_interruptions(seq, "CTG") # count baseline (to compare)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Why it matters (validation)
|
|
95
|
+
|
|
96
|
+
Across three diseases, `periodicity_loss` (or its axis, the longest uninterrupted stretch)
|
|
97
|
+
predicts age at onset **better than counting repeats**:
|
|
98
|
+
|
|
99
|
+
| Disease | Data | Result |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| **DM1** | Pešović 2018, n=7 | periodicity vs onset residual **r=+0.81** (LOO robust, bootstrap CI [+0.59,+0.99], perm p=0.027) vs count +0.53; captures type (CCG≫CTC) & position |
|
|
102
|
+
| **Huntington** | GeM-HD 2019 (published directions) | LOI < canonical < duplication — matches onset ordering |
|
|
103
|
+
| **SCA1** | Menon 2013 Table S3, n=35 | longest uninterrupted stretch predicts onset **R²=0.64** vs 0.24 for total size (r=−0.80, p=5×10⁻⁵) |
|
|
104
|
+
|
|
105
|
+
### Honest scope
|
|
106
|
+
|
|
107
|
+
A **biologically motivated correlate**, not proven causality. Individual, interruption-typed
|
|
108
|
+
cohorts at scale (OPTIMISTIC, Enroll-HD) are access-controlled; the claim is "better
|
|
109
|
+
predictor than count on the available data, with a mechanistic hypothesis and falsifiable
|
|
110
|
+
predictions" (correlation with measured somatic instability, UV melting, MSH3 binding).
|
|
111
|
+
Research use only.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Español
|
|
116
|
+
|
|
117
|
+
### La idea
|
|
118
|
+
|
|
119
|
+
En enfermedades de expansión de repeticiones (distrofia miotónica tipo 1, Huntington,
|
|
120
|
+
SCA1, DM2…), la edad de inicio no la fija solo *cuántas* repeticiones hay, sino si el
|
|
121
|
+
tracto está **interrumpido**. Las interrupciones (p. ej. CCG en el tracto CTG de DM1, CAA
|
|
122
|
+
en el CAG de Huntington, CAT en SCA1) rompen la **periodicidad** del perfil de energía de
|
|
123
|
+
apilamiento nearest-neighbor Φ, alteran el sustrato slipped-strand que dirige la
|
|
124
|
+
inestabilidad somática y **retrasan el inicio**.
|
|
125
|
+
|
|
126
|
+
`energorna` calcula un único observable glass-box — `periodicity_loss = 1 − ACF_periodo(Φ)`,
|
|
127
|
+
la pérdida de autocorrelación (al periodo del motivo) del perfil de apilamiento Turner —
|
|
128
|
+
que lo captura. **Auto-calibra el periodo** por motivo (3 en trinucleótidos, 4 en el
|
|
129
|
+
tetranucleótido CCTG) y, a diferencia de contar interrupciones, es sensible a su **tipo**
|
|
130
|
+
y **posición**.
|
|
131
|
+
|
|
132
|
+
### Instalación
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install energorna
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Uso — CLI
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
energorna score --repeat CTG --n 40
|
|
142
|
+
energorna score --seq CTGCTG...CCGCTG... --motif CTG --json
|
|
143
|
+
energorna score --seq-file alelo.fasta --motif CAG
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Uso — Python
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import energorna as ef
|
|
150
|
+
ef.calibrate_lag("CCTG") # -> 4 (auto-detecta el periodo)
|
|
151
|
+
ef.periodicity_loss(seq, unit="CTG") # observable global
|
|
152
|
+
ef.periodicity_profile(seq, unit="CTG") # perfil (pos, pérdida) 5'->3'
|
|
153
|
+
ef.periodicity_loss_3prime(seq, unit="CTG") # ponderado 3'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Por qué importa (validación)
|
|
157
|
+
|
|
158
|
+
En tres enfermedades, `periodicity_loss` (o su eje, el tramo ininterrumpido más largo)
|
|
159
|
+
predice la edad de inicio **mejor que contar repeticiones**: DM1 (Pešović n=7, r=+0,81),
|
|
160
|
+
Huntington (direcciones publicadas, LOI<canónico<duplicación) y SCA1 (Menon 2013 n=35,
|
|
161
|
+
R²=0,64 vs 0,24 del tamaño total). Ver la tabla en la sección en inglés.
|
|
162
|
+
|
|
163
|
+
### Alcance honesto
|
|
164
|
+
|
|
165
|
+
Es un **correlato biológicamente motivado**, no una causalidad demostrada. Los datos
|
|
166
|
+
individuales tipados a escala son de acceso controlado; el claim es "mejor predictor que
|
|
167
|
+
el conteo en los datos disponibles, con hipótesis mecanística y predicciones falsables".
|
|
168
|
+
Uso solo para investigación (RUO).
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Citation / Cita
|
|
173
|
+
|
|
174
|
+
Vilar Sánchez JA. *energoRNA: local periodicity loss of the mRNA stacking profile predicts
|
|
175
|
+
age at onset in repeat-expansion disorders.* QMetrika Labs, 2026. Method covered by patent
|
|
176
|
+
P202630522 (OEPM). Code under MIT.
|
|
177
|
+
|
|
178
|
+
- Repo: https://github.com/josevilar-qbioai/energorna
|
|
179
|
+
- ORCID: https://orcid.org/0009-0008-1057-4223
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# energorna
|
|
2
|
+
|
|
3
|
+
[](https://github.com/josevilar-qbioai/energorna/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/energorna/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Glass-box local periodicity-loss observable of the mRNA stacking-energy profile, for
|
|
8
|
+
repeat-expansion disorders.** Numpy-only, no ViennaRNA / GPU / API. Part of the QMetrika
|
|
9
|
+
Bio-IA thermodynamic engine (same Turner nearest-neighbor parameters as `ef-synonymous`).
|
|
10
|
+
|
|
11
|
+
*[English](#english) · [Español](#español)*
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## English
|
|
16
|
+
|
|
17
|
+
### The idea
|
|
18
|
+
|
|
19
|
+
In repeat-expansion diseases (myotonic dystrophy type 1, Huntington, SCA1, DM2…), age at
|
|
20
|
+
onset is not set only by *how many* repeats there are, but by whether the repeat tract is
|
|
21
|
+
**interrupted**. Interruptions (e.g. CCG in the CTG tract of DM1, CAA in the CAG tract of
|
|
22
|
+
Huntington, CAT in SCA1) break the **periodicity** of the nearest-neighbor stacking energy
|
|
23
|
+
profile Φ, disrupt the slipped-strand substrate that drives somatic instability, and
|
|
24
|
+
**delay onset**.
|
|
25
|
+
|
|
26
|
+
`energorna` computes a single glass-box observable — `periodicity_loss = 1 − ACF_period(Φ)`,
|
|
27
|
+
the loss of autocorrelation (at the motif period) of the Turner stacking profile — that
|
|
28
|
+
captures this. It **auto-calibrates the period** per motif (3 for trinucleotides, 4 for the
|
|
29
|
+
CCTG tetranucleotide) and, unlike simply counting interruptions, is sensitive to their
|
|
30
|
+
**type** and **position**.
|
|
31
|
+
|
|
32
|
+
### Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install energorna
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Use — CLI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# pure (CTG)40 vs an interrupted allele
|
|
42
|
+
energorna score --repeat CTG --n 40
|
|
43
|
+
energorna score --seq CTGCTGCTG...CCGCTG... --motif CTG --json
|
|
44
|
+
energorna score --seq-file allele.fasta --motif CAG
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Output shows the observable next to the traditional baselines:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
motif CTG (period 3) · 62 units · longest uninterrupted run 51
|
|
51
|
+
periodicity_loss = 0.18xx <- observable
|
|
52
|
+
periodicity_loss (3') = 0.19xx
|
|
53
|
+
n_interruptions = 4 (count baseline)
|
|
54
|
+
fragmentation_index = 0.09xx (fragmentation baseline)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Use — Python
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import energorna as ef
|
|
61
|
+
|
|
62
|
+
ef.calibrate_lag("CCTG") # -> 4 (auto-detects the motif period)
|
|
63
|
+
ef.periodicity_loss(seq, unit="CTG") # global observable
|
|
64
|
+
ef.periodicity_profile(seq, unit="CTG") # (pos, loss) resolved 5'->3'
|
|
65
|
+
ef.periodicity_loss_3prime(seq, unit="CTG") # 3'-weighted
|
|
66
|
+
ef.n_interruptions(seq, "CTG") # count baseline (to compare)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Why it matters (validation)
|
|
70
|
+
|
|
71
|
+
Across three diseases, `periodicity_loss` (or its axis, the longest uninterrupted stretch)
|
|
72
|
+
predicts age at onset **better than counting repeats**:
|
|
73
|
+
|
|
74
|
+
| Disease | Data | Result |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| **DM1** | Pešović 2018, n=7 | periodicity vs onset residual **r=+0.81** (LOO robust, bootstrap CI [+0.59,+0.99], perm p=0.027) vs count +0.53; captures type (CCG≫CTC) & position |
|
|
77
|
+
| **Huntington** | GeM-HD 2019 (published directions) | LOI < canonical < duplication — matches onset ordering |
|
|
78
|
+
| **SCA1** | Menon 2013 Table S3, n=35 | longest uninterrupted stretch predicts onset **R²=0.64** vs 0.24 for total size (r=−0.80, p=5×10⁻⁵) |
|
|
79
|
+
|
|
80
|
+
### Honest scope
|
|
81
|
+
|
|
82
|
+
A **biologically motivated correlate**, not proven causality. Individual, interruption-typed
|
|
83
|
+
cohorts at scale (OPTIMISTIC, Enroll-HD) are access-controlled; the claim is "better
|
|
84
|
+
predictor than count on the available data, with a mechanistic hypothesis and falsifiable
|
|
85
|
+
predictions" (correlation with measured somatic instability, UV melting, MSH3 binding).
|
|
86
|
+
Research use only.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Español
|
|
91
|
+
|
|
92
|
+
### La idea
|
|
93
|
+
|
|
94
|
+
En enfermedades de expansión de repeticiones (distrofia miotónica tipo 1, Huntington,
|
|
95
|
+
SCA1, DM2…), la edad de inicio no la fija solo *cuántas* repeticiones hay, sino si el
|
|
96
|
+
tracto está **interrumpido**. Las interrupciones (p. ej. CCG en el tracto CTG de DM1, CAA
|
|
97
|
+
en el CAG de Huntington, CAT en SCA1) rompen la **periodicidad** del perfil de energía de
|
|
98
|
+
apilamiento nearest-neighbor Φ, alteran el sustrato slipped-strand que dirige la
|
|
99
|
+
inestabilidad somática y **retrasan el inicio**.
|
|
100
|
+
|
|
101
|
+
`energorna` calcula un único observable glass-box — `periodicity_loss = 1 − ACF_periodo(Φ)`,
|
|
102
|
+
la pérdida de autocorrelación (al periodo del motivo) del perfil de apilamiento Turner —
|
|
103
|
+
que lo captura. **Auto-calibra el periodo** por motivo (3 en trinucleótidos, 4 en el
|
|
104
|
+
tetranucleótido CCTG) y, a diferencia de contar interrupciones, es sensible a su **tipo**
|
|
105
|
+
y **posición**.
|
|
106
|
+
|
|
107
|
+
### Instalación
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install energorna
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Uso — CLI
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
energorna score --repeat CTG --n 40
|
|
117
|
+
energorna score --seq CTGCTG...CCGCTG... --motif CTG --json
|
|
118
|
+
energorna score --seq-file alelo.fasta --motif CAG
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Uso — Python
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import energorna as ef
|
|
125
|
+
ef.calibrate_lag("CCTG") # -> 4 (auto-detecta el periodo)
|
|
126
|
+
ef.periodicity_loss(seq, unit="CTG") # observable global
|
|
127
|
+
ef.periodicity_profile(seq, unit="CTG") # perfil (pos, pérdida) 5'->3'
|
|
128
|
+
ef.periodicity_loss_3prime(seq, unit="CTG") # ponderado 3'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Por qué importa (validación)
|
|
132
|
+
|
|
133
|
+
En tres enfermedades, `periodicity_loss` (o su eje, el tramo ininterrumpido más largo)
|
|
134
|
+
predice la edad de inicio **mejor que contar repeticiones**: DM1 (Pešović n=7, r=+0,81),
|
|
135
|
+
Huntington (direcciones publicadas, LOI<canónico<duplicación) y SCA1 (Menon 2013 n=35,
|
|
136
|
+
R²=0,64 vs 0,24 del tamaño total). Ver la tabla en la sección en inglés.
|
|
137
|
+
|
|
138
|
+
### Alcance honesto
|
|
139
|
+
|
|
140
|
+
Es un **correlato biológicamente motivado**, no una causalidad demostrada. Los datos
|
|
141
|
+
individuales tipados a escala son de acceso controlado; el claim es "mejor predictor que
|
|
142
|
+
el conteo en los datos disponibles, con hipótesis mecanística y predicciones falsables".
|
|
143
|
+
Uso solo para investigación (RUO).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### Citation / Cita
|
|
148
|
+
|
|
149
|
+
Vilar Sánchez JA. *energoRNA: local periodicity loss of the mRNA stacking profile predicts
|
|
150
|
+
age at onset in repeat-expansion disorders.* QMetrika Labs, 2026. Method covered by patent
|
|
151
|
+
P202630522 (OEPM). Code under MIT.
|
|
152
|
+
|
|
153
|
+
- Repo: https://github.com/josevilar-qbioai/energorna
|
|
154
|
+
- ORCID: https://orcid.org/0009-0008-1057-4223
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""energorna — glass-box local periodicity-loss observable for repeat-expansion disorders.
|
|
2
|
+
|
|
3
|
+
Self-contained engine (numpy-only). Turner nearest-neighbor stacking parameters.
|
|
4
|
+
The interruption that breaks the periodicity of the stacking profile Φ predicts age at
|
|
5
|
+
onset better than counting repeats — validated across DM1, Huntington and SCA1.
|
|
6
|
+
"""
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from .engine import (
|
|
10
|
+
stacking_profile,
|
|
11
|
+
calibrate_lag,
|
|
12
|
+
periodicity_loss,
|
|
13
|
+
periodicity_profile,
|
|
14
|
+
periodicity_loss_3prime,
|
|
15
|
+
n_interruptions,
|
|
16
|
+
fragmentation_index,
|
|
17
|
+
STACKING_TURNER,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"stacking_profile", "calibrate_lag", "periodicity_loss", "periodicity_profile",
|
|
22
|
+
"periodicity_loss_3prime", "n_interruptions", "fragmentation_index", "STACKING_TURNER",
|
|
23
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
energorna CLI — score the local periodicity loss of a repeat-expansion allele.
|
|
3
|
+
|
|
4
|
+
Examples:
|
|
5
|
+
energorna score --seq CTGCTGCCGCTGCTG... --motif CTG
|
|
6
|
+
energorna score --seq-file allele.fasta --motif CAG --json
|
|
7
|
+
energorna score --repeat CTG --n 40 --json # pure (CTG)40
|
|
8
|
+
|
|
9
|
+
Numpy-only, glass-box. Prints periodicity_loss (the observable), its 3'-weighted
|
|
10
|
+
variant, and the traditional count/fragmentation baselines for comparison.
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from . import (periodicity_loss, periodicity_loss_3prime, periodicity_profile,
|
|
18
|
+
calibrate_lag, n_interruptions, fragmentation_index)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read_seq(literal=None, path=None, repeat=None, n=None):
|
|
22
|
+
if repeat and n:
|
|
23
|
+
seq = repeat.upper() * int(n)
|
|
24
|
+
elif literal:
|
|
25
|
+
seq = literal
|
|
26
|
+
elif path:
|
|
27
|
+
seq = "".join(l.strip() for l in open(path) if not l.startswith(">"))
|
|
28
|
+
else:
|
|
29
|
+
return None
|
|
30
|
+
seq = re.sub(r"\s", "", seq).upper().replace("U", "T")
|
|
31
|
+
return seq if re.fullmatch(r"[ACGT]+", seq or "") else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _longest_run(seq, motif):
|
|
35
|
+
units = [seq[i:i+len(motif)] for i in range(0, len(seq) - len(motif) + 1, len(motif))]
|
|
36
|
+
best = cur = 0
|
|
37
|
+
for u in units:
|
|
38
|
+
cur = cur + 1 if u == motif else 0
|
|
39
|
+
best = max(best, cur)
|
|
40
|
+
return best
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv=None):
|
|
44
|
+
ap = argparse.ArgumentParser(prog="energorna", description=__doc__,
|
|
45
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
46
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
47
|
+
s = sub.add_parser("score", help="score a repeat allele")
|
|
48
|
+
s.add_argument("--seq"); s.add_argument("--seq-file")
|
|
49
|
+
s.add_argument("--repeat"); s.add_argument("--n", type=int)
|
|
50
|
+
s.add_argument("--motif", default="CTG", help="repeat unit (default CTG)")
|
|
51
|
+
s.add_argument("--json", action="store_true")
|
|
52
|
+
args = ap.parse_args(argv)
|
|
53
|
+
|
|
54
|
+
if args.cmd != "score":
|
|
55
|
+
ap.print_help(); return 1
|
|
56
|
+
|
|
57
|
+
seq = _read_seq(args.seq, args.seq_file, args.repeat, args.n)
|
|
58
|
+
if not seq:
|
|
59
|
+
print("error: provide a valid nucleotide sequence via --seq / --seq-file / --repeat+--n",
|
|
60
|
+
file=sys.stderr)
|
|
61
|
+
return 2
|
|
62
|
+
motif = args.motif.upper().replace("U", "T")
|
|
63
|
+
|
|
64
|
+
out = {
|
|
65
|
+
"motif": motif,
|
|
66
|
+
"motif_period": calibrate_lag(motif),
|
|
67
|
+
"n_units": len(seq) // len(motif),
|
|
68
|
+
"longest_uninterrupted_run": _longest_run(seq, motif),
|
|
69
|
+
"n_interruptions": n_interruptions(seq, motif),
|
|
70
|
+
"periodicity_loss": round(periodicity_loss(seq, unit=motif), 6),
|
|
71
|
+
"periodicity_loss_3prime": round(periodicity_loss_3prime(seq, unit=motif), 6),
|
|
72
|
+
"fragmentation_index": round(fragmentation_index(seq, motif), 6),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if args.json:
|
|
76
|
+
print(json.dumps(out, indent=2))
|
|
77
|
+
else:
|
|
78
|
+
print(f"motif {out['motif']} (period {out['motif_period']}) · "
|
|
79
|
+
f"{out['n_units']} units · longest uninterrupted run {out['longest_uninterrupted_run']}")
|
|
80
|
+
print(f" periodicity_loss = {out['periodicity_loss']:.4f} <- observable")
|
|
81
|
+
print(f" periodicity_loss (3') = {out['periodicity_loss_3prime']:.4f}")
|
|
82
|
+
print(f" n_interruptions = {out['n_interruptions']} (count baseline)")
|
|
83
|
+
print(f" fragmentation_index = {out['fragmentation_index']:.4f} (fragmentation baseline)")
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
energorna.engine — glass-box observable of local PERIODICITY LOSS in the nearest-neighbor
|
|
3
|
+
stacking energy profile, for repeat-expansion disorders.
|
|
4
|
+
|
|
5
|
+
Self-contained (numpy-only). No ViennaRNA / GPU / API / wet-lab. Every number is traceable.
|
|
6
|
+
The central observable is the loss of autocorrelation (at lag = motif period) of the
|
|
7
|
+
nearest-neighbor stacking free-energy profile (Turner RNA parameters).
|
|
8
|
+
|
|
9
|
+
Public API:
|
|
10
|
+
stacking_profile(seq) -> per-position stacking profile Φ (kcal/mol)
|
|
11
|
+
calibrate_lag(unit) -> motif period (optimal ACF lag)
|
|
12
|
+
periodicity_loss(seq, unit|lag) -> global scalar: 1 - ACF_period(Φ)
|
|
13
|
+
periodicity_profile(seq) -> (relative_pos, local_loss) per window
|
|
14
|
+
periodicity_loss_3prime(seq) -> 3'-weighted scalar
|
|
15
|
+
n_interruptions(seq, unit) -> COUNT baseline (# interruptions)
|
|
16
|
+
fragmentation_index(seq, unit) -> FRAGMENTATION baseline (1 - Σ run^α / n^α)
|
|
17
|
+
|
|
18
|
+
Turner (RNA) nearest-neighbor stacking parameters; keys stored as DNA (T≡U).
|
|
19
|
+
Source: Turner 2004 / NNDB.
|
|
20
|
+
"""
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
STACKING_TURNER = {
|
|
24
|
+
'AA': -0.93, 'AT': -1.10, 'AG': -2.08, 'AC': -2.24,
|
|
25
|
+
'TA': -1.33, 'TT': -0.93, 'TG': -2.35, 'TC': -2.11,
|
|
26
|
+
'GA': -2.35, 'GT': -2.24, 'GG': -3.26, 'GC': -3.42,
|
|
27
|
+
'CA': -2.11, 'CT': -2.08, 'CG': -2.36, 'CC': -3.26,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stacking_profile(seq):
|
|
32
|
+
"""Nearest-neighbor stacking energy profile (Turner), one value per position."""
|
|
33
|
+
s = seq.upper().replace('U', 'T')
|
|
34
|
+
n = len(s)
|
|
35
|
+
bonds = np.array([STACKING_TURNER.get(s[i:i+2], 0.0) for i in range(n - 1)])
|
|
36
|
+
prof = np.zeros(n)
|
|
37
|
+
if n == 1:
|
|
38
|
+
return prof
|
|
39
|
+
prof[0], prof[-1] = bonds[0], bonds[-1]
|
|
40
|
+
for i in range(1, n - 1):
|
|
41
|
+
prof[i] = (bonds[i-1] + bonds[i]) / 2
|
|
42
|
+
return prof
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _acf(x, lag):
|
|
46
|
+
"""Autocorrelation of x at a given lag."""
|
|
47
|
+
x = np.asarray(x, float)
|
|
48
|
+
if len(x) <= lag + 1 or x.std() == 0:
|
|
49
|
+
return 1.0
|
|
50
|
+
x = x - x.mean()
|
|
51
|
+
return float(np.corrcoef(x[:-lag], x[lag:])[0, 1])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calibrate_lag(unit, lo=2, hi=8):
|
|
55
|
+
"""Motif period = lag (lo..hi) maximizing the ACF of the pure repeat.
|
|
56
|
+
|
|
57
|
+
Trinucleotides (CTG, CAG, CGG) -> 3; tetranucleotide (CCTG) -> 4.
|
|
58
|
+
"""
|
|
59
|
+
prof = stacking_profile(unit * 40)
|
|
60
|
+
scores = {L: _acf(prof, L) for L in range(lo, hi + 1)}
|
|
61
|
+
return max(scores, key=lambda L: scores[L])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def periodicity_loss(seq, unit=None, lag=None):
|
|
65
|
+
"""Global scalar: 1 - ACF at lag=period. If `unit`, calibrate the lag; else lag=3."""
|
|
66
|
+
if lag is None:
|
|
67
|
+
lag = calibrate_lag(unit) if unit else 3
|
|
68
|
+
return 1 - _acf(stacking_profile(seq), lag)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def periodicity_profile(seq, unit=None, lag=None, win=21, step=3):
|
|
72
|
+
"""Position-resolved profile of the local periodicity loss.
|
|
73
|
+
|
|
74
|
+
Returns (pos, val): pos in [0,1] (0=5', 1=3'), val = 1 - ACF_period per window.
|
|
75
|
+
"""
|
|
76
|
+
if lag is None:
|
|
77
|
+
lag = calibrate_lag(unit) if unit else 3
|
|
78
|
+
prof = stacking_profile(seq)
|
|
79
|
+
pos, val = [], []
|
|
80
|
+
for i in range(0, len(prof) - win, step):
|
|
81
|
+
pos.append((i + win / 2) / len(prof))
|
|
82
|
+
val.append(1 - _acf(prof[i:i+win], lag))
|
|
83
|
+
return np.array(pos), np.array(val)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def periodicity_loss_3prime(seq, unit=None, lag=None, win=21, step=3):
|
|
87
|
+
"""Periodicity loss weighted toward the 3' end (linear weight 0->1 from 5'->3')."""
|
|
88
|
+
pos, val = periodicity_profile(seq, unit, lag, win, step)
|
|
89
|
+
return float(np.average(val, weights=pos)) if len(pos) else 0.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── traditional baselines (for benchmarking) ──
|
|
93
|
+
def _units(seq, k):
|
|
94
|
+
s = seq.upper().replace('U', 'T')
|
|
95
|
+
return [s[i:i+k] for i in range(0, len(s) - k + 1, k)]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def n_interruptions(seq, unit='CTG'):
|
|
99
|
+
"""Number of units that are NOT the pure motif (count baseline)."""
|
|
100
|
+
unit = unit.upper().replace('U', 'T')
|
|
101
|
+
return sum(1 for t in _units(seq, len(unit)) if t != unit)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def fragmentation_index(seq, unit='CTG', alpha=1.6):
|
|
105
|
+
"""1 - FI, where FI = Σ(perfect_run)^α / n^α (fragmentation baseline)."""
|
|
106
|
+
unit = unit.upper().replace('U', 'T')
|
|
107
|
+
trip = _units(seq, len(unit))
|
|
108
|
+
n = len(trip)
|
|
109
|
+
runs, cur = [], 0
|
|
110
|
+
for t in trip:
|
|
111
|
+
if t == unit:
|
|
112
|
+
cur += 1
|
|
113
|
+
elif cur:
|
|
114
|
+
runs.append(cur); cur = 0
|
|
115
|
+
if cur:
|
|
116
|
+
runs.append(cur)
|
|
117
|
+
fi = sum(r**alpha for r in runs) / (n**alpha) if n else 0.0
|
|
118
|
+
return 1 - fi
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: energorna
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Glass-box local periodicity-loss observable of the mRNA stacking profile for repeat-expansion disorders (DM1, Huntington, SCA1)
|
|
5
|
+
Author-email: Jose Antonio Vilar Sanchez <qmetrika@proton.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/josevilar-qbioai/energorna
|
|
8
|
+
Project-URL: Repository, https://github.com/josevilar-qbioai/energorna
|
|
9
|
+
Project-URL: Issues, https://github.com/josevilar-qbioai/energorna/issues
|
|
10
|
+
Keywords: repeat expansion,myotonic dystrophy,huntington,SCA1,trinucleotide,age at onset,mRNA,thermodynamics,interruptions
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: numpy>=1.20
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# energorna
|
|
27
|
+
|
|
28
|
+
[](https://github.com/josevilar-qbioai/energorna/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/energorna/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
**Glass-box local periodicity-loss observable of the mRNA stacking-energy profile, for
|
|
33
|
+
repeat-expansion disorders.** Numpy-only, no ViennaRNA / GPU / API. Part of the QMetrika
|
|
34
|
+
Bio-IA thermodynamic engine (same Turner nearest-neighbor parameters as `ef-synonymous`).
|
|
35
|
+
|
|
36
|
+
*[English](#english) · [Español](#español)*
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## English
|
|
41
|
+
|
|
42
|
+
### The idea
|
|
43
|
+
|
|
44
|
+
In repeat-expansion diseases (myotonic dystrophy type 1, Huntington, SCA1, DM2…), age at
|
|
45
|
+
onset is not set only by *how many* repeats there are, but by whether the repeat tract is
|
|
46
|
+
**interrupted**. Interruptions (e.g. CCG in the CTG tract of DM1, CAA in the CAG tract of
|
|
47
|
+
Huntington, CAT in SCA1) break the **periodicity** of the nearest-neighbor stacking energy
|
|
48
|
+
profile Φ, disrupt the slipped-strand substrate that drives somatic instability, and
|
|
49
|
+
**delay onset**.
|
|
50
|
+
|
|
51
|
+
`energorna` computes a single glass-box observable — `periodicity_loss = 1 − ACF_period(Φ)`,
|
|
52
|
+
the loss of autocorrelation (at the motif period) of the Turner stacking profile — that
|
|
53
|
+
captures this. It **auto-calibrates the period** per motif (3 for trinucleotides, 4 for the
|
|
54
|
+
CCTG tetranucleotide) and, unlike simply counting interruptions, is sensitive to their
|
|
55
|
+
**type** and **position**.
|
|
56
|
+
|
|
57
|
+
### Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install energorna
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Use — CLI
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# pure (CTG)40 vs an interrupted allele
|
|
67
|
+
energorna score --repeat CTG --n 40
|
|
68
|
+
energorna score --seq CTGCTGCTG...CCGCTG... --motif CTG --json
|
|
69
|
+
energorna score --seq-file allele.fasta --motif CAG
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output shows the observable next to the traditional baselines:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
motif CTG (period 3) · 62 units · longest uninterrupted run 51
|
|
76
|
+
periodicity_loss = 0.18xx <- observable
|
|
77
|
+
periodicity_loss (3') = 0.19xx
|
|
78
|
+
n_interruptions = 4 (count baseline)
|
|
79
|
+
fragmentation_index = 0.09xx (fragmentation baseline)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Use — Python
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import energorna as ef
|
|
86
|
+
|
|
87
|
+
ef.calibrate_lag("CCTG") # -> 4 (auto-detects the motif period)
|
|
88
|
+
ef.periodicity_loss(seq, unit="CTG") # global observable
|
|
89
|
+
ef.periodicity_profile(seq, unit="CTG") # (pos, loss) resolved 5'->3'
|
|
90
|
+
ef.periodicity_loss_3prime(seq, unit="CTG") # 3'-weighted
|
|
91
|
+
ef.n_interruptions(seq, "CTG") # count baseline (to compare)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Why it matters (validation)
|
|
95
|
+
|
|
96
|
+
Across three diseases, `periodicity_loss` (or its axis, the longest uninterrupted stretch)
|
|
97
|
+
predicts age at onset **better than counting repeats**:
|
|
98
|
+
|
|
99
|
+
| Disease | Data | Result |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| **DM1** | Pešović 2018, n=7 | periodicity vs onset residual **r=+0.81** (LOO robust, bootstrap CI [+0.59,+0.99], perm p=0.027) vs count +0.53; captures type (CCG≫CTC) & position |
|
|
102
|
+
| **Huntington** | GeM-HD 2019 (published directions) | LOI < canonical < duplication — matches onset ordering |
|
|
103
|
+
| **SCA1** | Menon 2013 Table S3, n=35 | longest uninterrupted stretch predicts onset **R²=0.64** vs 0.24 for total size (r=−0.80, p=5×10⁻⁵) |
|
|
104
|
+
|
|
105
|
+
### Honest scope
|
|
106
|
+
|
|
107
|
+
A **biologically motivated correlate**, not proven causality. Individual, interruption-typed
|
|
108
|
+
cohorts at scale (OPTIMISTIC, Enroll-HD) are access-controlled; the claim is "better
|
|
109
|
+
predictor than count on the available data, with a mechanistic hypothesis and falsifiable
|
|
110
|
+
predictions" (correlation with measured somatic instability, UV melting, MSH3 binding).
|
|
111
|
+
Research use only.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Español
|
|
116
|
+
|
|
117
|
+
### La idea
|
|
118
|
+
|
|
119
|
+
En enfermedades de expansión de repeticiones (distrofia miotónica tipo 1, Huntington,
|
|
120
|
+
SCA1, DM2…), la edad de inicio no la fija solo *cuántas* repeticiones hay, sino si el
|
|
121
|
+
tracto está **interrumpido**. Las interrupciones (p. ej. CCG en el tracto CTG de DM1, CAA
|
|
122
|
+
en el CAG de Huntington, CAT en SCA1) rompen la **periodicidad** del perfil de energía de
|
|
123
|
+
apilamiento nearest-neighbor Φ, alteran el sustrato slipped-strand que dirige la
|
|
124
|
+
inestabilidad somática y **retrasan el inicio**.
|
|
125
|
+
|
|
126
|
+
`energorna` calcula un único observable glass-box — `periodicity_loss = 1 − ACF_periodo(Φ)`,
|
|
127
|
+
la pérdida de autocorrelación (al periodo del motivo) del perfil de apilamiento Turner —
|
|
128
|
+
que lo captura. **Auto-calibra el periodo** por motivo (3 en trinucleótidos, 4 en el
|
|
129
|
+
tetranucleótido CCTG) y, a diferencia de contar interrupciones, es sensible a su **tipo**
|
|
130
|
+
y **posición**.
|
|
131
|
+
|
|
132
|
+
### Instalación
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install energorna
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Uso — CLI
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
energorna score --repeat CTG --n 40
|
|
142
|
+
energorna score --seq CTGCTG...CCGCTG... --motif CTG --json
|
|
143
|
+
energorna score --seq-file alelo.fasta --motif CAG
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Uso — Python
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import energorna as ef
|
|
150
|
+
ef.calibrate_lag("CCTG") # -> 4 (auto-detecta el periodo)
|
|
151
|
+
ef.periodicity_loss(seq, unit="CTG") # observable global
|
|
152
|
+
ef.periodicity_profile(seq, unit="CTG") # perfil (pos, pérdida) 5'->3'
|
|
153
|
+
ef.periodicity_loss_3prime(seq, unit="CTG") # ponderado 3'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Por qué importa (validación)
|
|
157
|
+
|
|
158
|
+
En tres enfermedades, `periodicity_loss` (o su eje, el tramo ininterrumpido más largo)
|
|
159
|
+
predice la edad de inicio **mejor que contar repeticiones**: DM1 (Pešović n=7, r=+0,81),
|
|
160
|
+
Huntington (direcciones publicadas, LOI<canónico<duplicación) y SCA1 (Menon 2013 n=35,
|
|
161
|
+
R²=0,64 vs 0,24 del tamaño total). Ver la tabla en la sección en inglés.
|
|
162
|
+
|
|
163
|
+
### Alcance honesto
|
|
164
|
+
|
|
165
|
+
Es un **correlato biológicamente motivado**, no una causalidad demostrada. Los datos
|
|
166
|
+
individuales tipados a escala son de acceso controlado; el claim es "mejor predictor que
|
|
167
|
+
el conteo en los datos disponibles, con hipótesis mecanística y predicciones falsables".
|
|
168
|
+
Uso solo para investigación (RUO).
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Citation / Cita
|
|
173
|
+
|
|
174
|
+
Vilar Sánchez JA. *energoRNA: local periodicity loss of the mRNA stacking profile predicts
|
|
175
|
+
age at onset in repeat-expansion disorders.* QMetrika Labs, 2026. Method covered by patent
|
|
176
|
+
P202630522 (OEPM). Code under MIT.
|
|
177
|
+
|
|
178
|
+
- Repo: https://github.com/josevilar-qbioai/energorna
|
|
179
|
+
- ORCID: https://orcid.org/0009-0008-1057-4223
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
energorna/__init__.py
|
|
5
|
+
energorna/cli.py
|
|
6
|
+
energorna/engine.py
|
|
7
|
+
energorna.egg-info/PKG-INFO
|
|
8
|
+
energorna.egg-info/SOURCES.txt
|
|
9
|
+
energorna.egg-info/dependency_links.txt
|
|
10
|
+
energorna.egg-info/entry_points.txt
|
|
11
|
+
energorna.egg-info/requires.txt
|
|
12
|
+
energorna.egg-info/top_level.txt
|
|
13
|
+
tests/test_ground_truth.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy>=1.20
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
energorna
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "energorna"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Glass-box local periodicity-loss observable of the mRNA stacking profile for repeat-expansion disorders (DM1, Huntington, SCA1)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Jose Antonio Vilar Sanchez", email = "qmetrika@proton.me" }]
|
|
13
|
+
keywords = ["repeat expansion", "myotonic dystrophy", "huntington", "SCA1", "trinucleotide", "age at onset", "mRNA", "thermodynamics", "interruptions"]
|
|
14
|
+
dependencies = ["numpy>=1.20"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/josevilar-qbioai/energorna"
|
|
29
|
+
Repository = "https://github.com/josevilar-qbioai/energorna"
|
|
30
|
+
Issues = "https://github.com/josevilar-qbioai/energorna/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
energorna = "energorna.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools]
|
|
36
|
+
packages = ["energorna"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ground-truth tests for energorna. Values computed from the reference engine and
|
|
3
|
+
reported in the manuscript / study (studies/energorna-dm1). numpy-only.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import energorna as ef
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_calibrate_lag_period():
|
|
10
|
+
# ACF of the pure repeat peaks at the motif period
|
|
11
|
+
assert ef.calibrate_lag("CTG") == 3
|
|
12
|
+
assert ef.calibrate_lag("CAG") == 3
|
|
13
|
+
assert ef.calibrate_lag("CGG") == 3
|
|
14
|
+
assert ef.calibrate_lag("CCTG") == 4 # tetranucleotide (DM2)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_periodicity_loss_pure_is_low():
|
|
18
|
+
assert abs(ef.periodicity_loss("CTG" * 40, unit="CTG") - 0.036937) < 1e-5
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_interruption_raises_loss():
|
|
22
|
+
pure = ef.periodicity_loss("CTG" * 60, unit="CTG")
|
|
23
|
+
intr = ef.periodicity_loss(_equispaced("CTG", "CCG", k=4), unit="CTG")
|
|
24
|
+
assert intr > pure + 0.1 # an interruption breaks the pattern
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_huntington_ordering():
|
|
28
|
+
# LOI (uninterrupted) < canonical (1 CAA) < duplication (2 CAA) — matches onset direction
|
|
29
|
+
loi = ef.periodicity_loss("CAG" * 42, unit="CAG")
|
|
30
|
+
can = ef.periodicity_loss("CAG" * 40 + "CAACAG", unit="CAG")
|
|
31
|
+
dup = ef.periodicity_loss("CAG" * 38 + "CAACAGCAACAG", unit="CAG")
|
|
32
|
+
assert loi < can < dup
|
|
33
|
+
assert abs(loi - 0.036145) < 1e-5
|
|
34
|
+
assert abs(dup - 0.229976) < 1e-5
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_type_discrimination_ccg_gt_ctc():
|
|
38
|
+
# At fixed count/position, CCG (protective in DM1) disrupts Φ more than CTC
|
|
39
|
+
ccg = ef.periodicity_loss(_equispaced("CTG", "CCG", k=4), unit="CTG")
|
|
40
|
+
ctc = ef.periodicity_loss(_equispaced("CTG", "CTC", k=4), unit="CTG")
|
|
41
|
+
assert ccg > ctc
|
|
42
|
+
assert abs(ccg - 0.193759) < 1e-5
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_baselines():
|
|
46
|
+
seq = _equispaced("CTG", "CCG", k=4)
|
|
47
|
+
assert ef.n_interruptions(seq, "CTG") == 4
|
|
48
|
+
assert 0.0 < ef.fragmentation_index(seq, "CTG") < 1.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_profile_shape():
|
|
52
|
+
pos, val = ef.periodicity_profile("CTG" * 40 + "CCGCTG" * 4, unit="CTG")
|
|
53
|
+
assert len(pos) == len(val) and pos.min() >= 0 and pos.max() <= 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _equispaced(motif, interr, k=4, n=60):
|
|
57
|
+
u = [motif] * n
|
|
58
|
+
for i in np.linspace(0, n - 1, k + 2).round().astype(int)[1:-1]:
|
|
59
|
+
u[int(i)] = interr
|
|
60
|
+
return "".join(u)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
for fn in [test_calibrate_lag_period, test_periodicity_loss_pure_is_low,
|
|
65
|
+
test_interruption_raises_loss, test_huntington_ordering,
|
|
66
|
+
test_type_discrimination_ccg_gt_ctc, test_baselines, test_profile_shape]:
|
|
67
|
+
fn(); print("ok:", fn.__name__)
|
|
68
|
+
print("ALL OK")
|