lzcomplexity 1.0.2__tar.gz → 2.0.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.
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/Cargo.lock +3 -3
- lzcomplexity-2.0.0/Cargo.toml +50 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/PKG-INFO +1 -1
- lzcomplexity-2.0.0/crates/lzcomplexity-core/src/shuffle.rs +358 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-py/src/lib.rs +28 -3
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/pyproject.toml +1 -1
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/python/lzcomplexity/__init__.pyi +6 -2
- lzcomplexity-1.0.2/Cargo.toml +0 -30
- lzcomplexity-1.0.2/crates/lzcomplexity-core/src/shuffle.rs +0 -216
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/LICENSE +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/README.md +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/Cargo.toml +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/README.md +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/src/lib.rs +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/src/lpf.rs +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/src/lz76.rs +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/src/metrics.rs +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/src/sequence.rs +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-core/src/suffix_array.rs +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/crates/lzcomplexity-py/Cargo.toml +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/python/lzcomplexity/__init__.py +0 -0
- {lzcomplexity-1.0.2 → lzcomplexity-2.0.0}/python/lzcomplexity/py.typed +0 -0
|
@@ -238,7 +238,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
|
238
238
|
|
|
239
239
|
[[package]]
|
|
240
240
|
name = "lzcomplexity-cli"
|
|
241
|
-
version = "
|
|
241
|
+
version = "2.0.0"
|
|
242
242
|
dependencies = [
|
|
243
243
|
"clap",
|
|
244
244
|
"flate2",
|
|
@@ -250,7 +250,7 @@ dependencies = [
|
|
|
250
250
|
|
|
251
251
|
[[package]]
|
|
252
252
|
name = "lzcomplexity-core"
|
|
253
|
-
version = "
|
|
253
|
+
version = "2.0.0"
|
|
254
254
|
dependencies = [
|
|
255
255
|
"cdivsufsort",
|
|
256
256
|
"rand",
|
|
@@ -260,7 +260,7 @@ dependencies = [
|
|
|
260
260
|
|
|
261
261
|
[[package]]
|
|
262
262
|
name = "lzcomplexity-py"
|
|
263
|
-
version = "
|
|
263
|
+
version = "2.0.0"
|
|
264
264
|
dependencies = [
|
|
265
265
|
"lzcomplexity-core",
|
|
266
266
|
"pyo3",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
resolver = "2"
|
|
3
|
+
members = ["crates/lzcomplexity-core", "crates/lzcomplexity-py"]
|
|
4
|
+
|
|
5
|
+
[workspace.package]
|
|
6
|
+
version = "2.0.0" # x-release-please-version
|
|
7
|
+
edition = "2021"
|
|
8
|
+
rust-version = "1.75"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
authors = [
|
|
11
|
+
"Efrén Aragón Pérez <efrenaragon96@gmail.com>",
|
|
12
|
+
"Daniel Estévez Moya <kemossabee@gmail.com>",
|
|
13
|
+
"Ernesto Estévez Rams <estevez@fisica.uh.cu>",
|
|
14
|
+
]
|
|
15
|
+
repository = "https://github.com/pleros-ai/lzcomplexity"
|
|
16
|
+
|
|
17
|
+
[workspace.dependencies]
|
|
18
|
+
rayon = "1.10"
|
|
19
|
+
# PINNED EXACTLY, AND NOT AS A SUPPLY-CHAIN MEASURE.
|
|
20
|
+
#
|
|
21
|
+
# The block shuffle behind `emc` draws from `ChaCha8Rng::seed_from_u64` and
|
|
22
|
+
# `Rng::gen_range`, so the exact byte stream these two crates produce is part of
|
|
23
|
+
# this library's observable output: it decides which surrogates are drawn, and
|
|
24
|
+
# therefore the value `emc` returns. Two things depend on it.
|
|
25
|
+
#
|
|
26
|
+
# 1. `emc` is documented as reproducible for a given input on any machine at any
|
|
27
|
+
# thread count. See docs/project/determinism.md.
|
|
28
|
+
# 2. The C++ backend on `main` reproduces these crates' *internal* algorithms by
|
|
29
|
+
# hand -- rand_core's PCG32 seed expansion, ChaCha8 with a 64-word buffer, and
|
|
30
|
+
# gen_range's `(range << leading_zeros) - 1` rejection zone -- so that both
|
|
31
|
+
# backends return identical values. See modules/core/inc/lz/rng.h there, and
|
|
32
|
+
# docs/project/cpp-parity.md.
|
|
33
|
+
#
|
|
34
|
+
# A minor-version bump of either crate may change that stream without any API
|
|
35
|
+
# change and without any warning, which would silently break both properties. Do
|
|
36
|
+
# not relax these to caret requirements. To upgrade deliberately: re-run the C++
|
|
37
|
+
# `rng_parity` test (ctest) against the new stream, regenerate its vectors if it
|
|
38
|
+
# fails, and treat the resulting `emc` change as a breaking change.
|
|
39
|
+
rand = "=0.8.6"
|
|
40
|
+
rand_chacha = "=0.3.1"
|
|
41
|
+
cdivsufsort = "2.0"
|
|
42
|
+
clap = { version = "4.5", features = ["derive"] }
|
|
43
|
+
serde = { version = "1", features = ["derive"] }
|
|
44
|
+
serde_json = "1"
|
|
45
|
+
flate2 = "1.0"
|
|
46
|
+
|
|
47
|
+
[profile.release]
|
|
48
|
+
lto = "thin"
|
|
49
|
+
codegen-units = 1
|
|
50
|
+
opt-level = 3
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
//! Shuffle-based complexity measures.
|
|
2
|
+
|
|
3
|
+
use rayon::prelude::*;
|
|
4
|
+
|
|
5
|
+
use crate::lz76::{lz76_factorization, LzArgs, LzShuffle};
|
|
6
|
+
use crate::sequence::{shuffle_copy_seeded, Sequence};
|
|
7
|
+
use crate::NO_ALPHABET;
|
|
8
|
+
|
|
9
|
+
/// Estimate the "naive M" satisfying `N = M * 2^M`, mirroring `max_block_size` in C++.
|
|
10
|
+
pub fn max_block_size(size: usize) -> usize {
|
|
11
|
+
if size < 10 {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
let mut m: usize = 10;
|
|
15
|
+
let mut m_old: usize = 1;
|
|
16
|
+
let mut count: u32 = 0;
|
|
17
|
+
while count < 100 {
|
|
18
|
+
let est = ((size as f64) / (m_old as f64)).ln() / 2f64.ln();
|
|
19
|
+
m = est.round() as usize;
|
|
20
|
+
count += 1;
|
|
21
|
+
if m == m_old {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
m_old = m;
|
|
25
|
+
}
|
|
26
|
+
m
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Compute the random-shuffle factorization vector and `mm`. Mirrors
|
|
30
|
+
/// `ShuffleFactorization` from the C++.
|
|
31
|
+
pub fn shuffle_factorization(seq: &Sequence, args: &LzArgs) -> (Vec<i32>, usize) {
|
|
32
|
+
let mut mm: i64 = args.block_size as i64;
|
|
33
|
+
if mm <= 0 {
|
|
34
|
+
mm = max_block_size(seq.len()) as i64;
|
|
35
|
+
if seq.len() > 50 {
|
|
36
|
+
mm += 10;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let mm = mm as usize;
|
|
40
|
+
|
|
41
|
+
// Index 0 unused, indices 1..=mm hold the per-block-size complexity.
|
|
42
|
+
let mut res: Vec<i32> = vec![0; mm + 3];
|
|
43
|
+
|
|
44
|
+
// Hash the sequence content ONCE; each per-block seed only differs by the
|
|
45
|
+
// `idx` term (bit-identical seeds → identical shuffles → identical EMC).
|
|
46
|
+
let base = fnv1a(seq.as_bytes());
|
|
47
|
+
let computed: Vec<(usize, i32)> = (1..=mm)
|
|
48
|
+
.into_par_iter()
|
|
49
|
+
.map(|idx| {
|
|
50
|
+
let seed = seed_from_base(base, idx);
|
|
51
|
+
let rand_seq = shuffle_copy_seeded(seq, idx as u32, (seq.len() / 2) as u32, seed);
|
|
52
|
+
let c = lz76_factorization(&rand_seq, args);
|
|
53
|
+
(idx, c as i32)
|
|
54
|
+
})
|
|
55
|
+
.collect();
|
|
56
|
+
for (idx, c) in computed {
|
|
57
|
+
res[idx] = c;
|
|
58
|
+
}
|
|
59
|
+
(res, mm)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Effective measure complexity via the block-entropy estimator.
|
|
63
|
+
///
|
|
64
|
+
/// For each block size `l`, `h_rand[l]` is `C_LZ(u^{RS(l)})`, the LZ complexity
|
|
65
|
+
/// of `u` block-shuffled at scale `l`. The surrogate is (idealised) an i.i.d.
|
|
66
|
+
/// stream of `l`-blocks whose entropy rate is `H(l)/l`, so multiplying back
|
|
67
|
+
/// recovers a block-entropy estimate `H_l = l · C_LZ(u^{RS(l)}) · g`, with
|
|
68
|
+
/// `g = log_k(N)/N` and `ĥ = C_LZ(u) · g` the entropy density of `u`. That
|
|
69
|
+
/// gives one *finite-scale excess entropy* per block size — the subextensive
|
|
70
|
+
/// part of the block entropy at scale `l`:
|
|
71
|
+
///
|
|
72
|
+
/// ```text
|
|
73
|
+
/// Ê(l) = H_l − l·ĥ = l · g · ( C_LZ(u^{RS(l)}) − C_LZ(u) )
|
|
74
|
+
/// ```
|
|
75
|
+
///
|
|
76
|
+
/// Theory pins down the shape of that ladder: `E(l) = Σ_{j≤l} [h(j) − h]` with
|
|
77
|
+
/// every increment `h(j) − h ≥ 0`, so `E(l)` is non-negative and non-decreasing
|
|
78
|
+
/// in `l` and rises to `E` (Crutchfield & Feldman, *Chaos* 13:25 (2003),
|
|
79
|
+
/// Lemma 1 and Eq. 52). The raw ladder violates both properties, because each
|
|
80
|
+
/// `Ê(l)` is an *independent* surrogate draw rather than a slice of one
|
|
81
|
+
/// consistent block-entropy curve. We therefore project it onto that cone —
|
|
82
|
+
/// non-negative isotonic regression, see [`non_negative_isotonic`] — and read
|
|
83
|
+
/// the answer off the projection:
|
|
84
|
+
///
|
|
85
|
+
/// ```text
|
|
86
|
+
/// summands[l−1] = Ê_fit(l) − Ê_fit(l−1) ≥ 0 (the estimate of h(l) − h)
|
|
87
|
+
/// Ê = Ê_fit(mm) = Σ summands ≥ 0
|
|
88
|
+
/// ```
|
|
89
|
+
///
|
|
90
|
+
/// Every block size informs the fit, and the returned scalar can no longer go
|
|
91
|
+
/// negative — a mutual information never does.
|
|
92
|
+
///
|
|
93
|
+
/// # Why not sum the raw differences
|
|
94
|
+
///
|
|
95
|
+
/// Versions ≤ 1.0.1 summed `(H_l − H_{l−1}) − ĥ` directly. That is the same
|
|
96
|
+
/// textbook identity written the other way round, `Σ_l [h(l) − h] = H(L) − L·h`,
|
|
97
|
+
/// so the `H_l` telescoped and the total collapsed *exactly* to
|
|
98
|
+
/// `mm · g · (C_LZ(u^{RS(mm)}) − C_LZ(u))`: only the largest block size reached
|
|
99
|
+
/// the scalar, the other `mm − 1` factorizations cancelled algebraically, and a
|
|
100
|
+
/// single unlucky surrogate draw could push the result below zero. Projecting
|
|
101
|
+
/// first is what couples the scales together.
|
|
102
|
+
///
|
|
103
|
+
/// `multi_information` stays the *raw* `l = 1` contrast `H_1 − ĥ`, unchanged
|
|
104
|
+
/// from earlier versions: it is a separate quantity (the multi-information rate
|
|
105
|
+
/// of the source), not a term of the sum, and it is the best-calibrated number
|
|
106
|
+
/// this estimator produces.
|
|
107
|
+
pub fn shuffle_entropy_calculation(
|
|
108
|
+
seq: &Sequence,
|
|
109
|
+
args: &LzArgs,
|
|
110
|
+
complexity: i32,
|
|
111
|
+
h_rand: &[i32],
|
|
112
|
+
mm: usize,
|
|
113
|
+
) -> LzShuffle {
|
|
114
|
+
let mut result = LzShuffle {
|
|
115
|
+
max_block_size: mm as i32,
|
|
116
|
+
..Default::default()
|
|
117
|
+
};
|
|
118
|
+
if args.get_shuffle_terms {
|
|
119
|
+
result.summands = vec![0.0; mm];
|
|
120
|
+
}
|
|
121
|
+
let log_base = if args.log_base == NO_ALPHABET {
|
|
122
|
+
seq.alphabet_size().max(2)
|
|
123
|
+
} else {
|
|
124
|
+
args.log_base.max(2)
|
|
125
|
+
} as f64;
|
|
126
|
+
|
|
127
|
+
let n = seq.len() as f64;
|
|
128
|
+
if n <= 1.0 || mm == 0 {
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
// g = log_k(N) / N, so ĥ = C_LZ(u)·g and H_l = l·C_LZ(u^{RS(l)})·g.
|
|
132
|
+
let g = (n.ln() / log_base.ln()) / n;
|
|
133
|
+
let h_hat = complexity as f64 * g; // entropy density of the original sequence
|
|
134
|
+
|
|
135
|
+
// The raw ladder Ê(l) = H_l − l·ĥ, one finite-scale excess entropy per
|
|
136
|
+
// block size. `h_rand` is 1-indexed, so `len` is how many scales it holds.
|
|
137
|
+
let len = mm.min(h_rand.len().saturating_sub(1));
|
|
138
|
+
if len == 0 {
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
let raw: Vec<f64> = (1..=len)
|
|
142
|
+
.map(|l| l as f64 * (h_rand[l] as f64 * g - h_hat))
|
|
143
|
+
.collect();
|
|
144
|
+
|
|
145
|
+
// The l = 1 contrast, before any projection.
|
|
146
|
+
result.multi_information = raw[0];
|
|
147
|
+
|
|
148
|
+
// Project onto {non-negative, non-decreasing} and read off the increments.
|
|
149
|
+
let fitted = non_negative_isotonic(&raw);
|
|
150
|
+
let mut prev = 0.0f64; // Ê_fit(0) = 0
|
|
151
|
+
for l in 1..=len {
|
|
152
|
+
let term = fitted[l - 1] - prev;
|
|
153
|
+
if args.get_shuffle_terms {
|
|
154
|
+
result.summands[l - 1] = term;
|
|
155
|
+
}
|
|
156
|
+
prev = fitted[l - 1];
|
|
157
|
+
}
|
|
158
|
+
result.emc_value = prev; // = Ê_fit(len)
|
|
159
|
+
result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// L2 projection of `y` onto the cone of non-negative, non-decreasing sequences.
|
|
163
|
+
///
|
|
164
|
+
/// The isotonic part is the pool-adjacent-violators algorithm (Ayer et al.,
|
|
165
|
+
/// *Ann. Math. Statist.* 26:641 (1955)): walk left to right, and whenever the
|
|
166
|
+
/// newest block averages below its predecessor, merge the two and re-check.
|
|
167
|
+
/// Blocks are stored as `(sum, count)` pairs, so this is O(n) overall. The
|
|
168
|
+
/// non-negative projection is then the pointwise positive part, which preserves
|
|
169
|
+
/// monotonicity.
|
|
170
|
+
fn non_negative_isotonic(y: &[f64]) -> Vec<f64> {
|
|
171
|
+
let mut sums: Vec<f64> = Vec::with_capacity(y.len());
|
|
172
|
+
let mut counts: Vec<usize> = Vec::with_capacity(y.len());
|
|
173
|
+
for &v in y {
|
|
174
|
+
sums.push(v);
|
|
175
|
+
counts.push(1);
|
|
176
|
+
while sums.len() > 1 {
|
|
177
|
+
let k = sums.len();
|
|
178
|
+
let last = sums[k - 1] / counts[k - 1] as f64;
|
|
179
|
+
let prev = sums[k - 2] / counts[k - 2] as f64;
|
|
180
|
+
if prev <= last {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
sums[k - 2] += sums[k - 1];
|
|
184
|
+
counts[k - 2] += counts[k - 1];
|
|
185
|
+
sums.pop();
|
|
186
|
+
counts.pop();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let mut out = Vec::with_capacity(y.len());
|
|
190
|
+
for (s, c) in sums.iter().zip(counts.iter()) {
|
|
191
|
+
let level = (s / *c as f64).max(0.0);
|
|
192
|
+
out.extend(std::iter::repeat(level).take(*c));
|
|
193
|
+
}
|
|
194
|
+
out
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// `lz76RandomShuffleComplexity` — full pipeline.
|
|
198
|
+
pub fn lz76_random_shuffle_complexity(seq: &Sequence, args: &LzArgs) -> LzShuffle {
|
|
199
|
+
lz76_random_shuffle_complexity_with(seq, args, lz76_factorization(seq, args) as i32)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Same, but with an already-computed original-sequence complexity (so callers
|
|
203
|
+
/// that already have it — e.g. the `lz76` driver — skip one factorization).
|
|
204
|
+
pub fn lz76_random_shuffle_complexity_with(
|
|
205
|
+
seq: &Sequence,
|
|
206
|
+
args: &LzArgs,
|
|
207
|
+
complexity: i32,
|
|
208
|
+
) -> LzShuffle {
|
|
209
|
+
let (h_rand, mm) = shuffle_factorization(seq, args);
|
|
210
|
+
shuffle_entropy_calculation(seq, args, complexity, &h_rand, mm)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// `lz76PairedShuffleComplexity` — shuffle on the merged (Z) sequence.
|
|
214
|
+
pub fn lz76_paired_shuffle_complexity(seq: &Sequence, args: &LzArgs) -> LzShuffle {
|
|
215
|
+
let mid = seq.len() / 2;
|
|
216
|
+
let (past, future) = seq.split_at(mid);
|
|
217
|
+
let merged = merge_sequences(&past, &future);
|
|
218
|
+
let complexity = lz76_factorization(&merged, args) as i32;
|
|
219
|
+
let (h_rand, mm) = shuffle_factorization(&merged, args);
|
|
220
|
+
shuffle_entropy_calculation(&merged, args, complexity, &h_rand, mm)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Mirror of `internal::MergeSequences` — element-wise pairing into a new alphabet.
|
|
224
|
+
pub fn merge_sequences(s1: &Sequence, s2: &Sequence) -> Sequence {
|
|
225
|
+
let max_iter = s1.len().min(s2.len());
|
|
226
|
+
let mut buf = Vec::with_capacity(max_iter);
|
|
227
|
+
let mut lookup = vec![0u8; 65536];
|
|
228
|
+
let mut next_symbol: u8 = b'0';
|
|
229
|
+
let a = s1.as_bytes();
|
|
230
|
+
let b = s2.as_bytes();
|
|
231
|
+
for i in 0..max_iter {
|
|
232
|
+
let key = ((a[i] as u16) << 8) | (b[i] as u16);
|
|
233
|
+
let entry = &mut lookup[key as usize];
|
|
234
|
+
if *entry == 0 {
|
|
235
|
+
*entry = next_symbol;
|
|
236
|
+
next_symbol = next_symbol.wrapping_add(1);
|
|
237
|
+
}
|
|
238
|
+
buf.push(*entry);
|
|
239
|
+
}
|
|
240
|
+
Sequence::from_bytes_with_alphabet(buf, s1.alphabet_size().saturating_mul(s2.alphabet_size()))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// FNV-1a hash of the sequence bytes (the content-dependent part of the seed).
|
|
244
|
+
fn fnv1a(bytes: &[u8]) -> u64 {
|
|
245
|
+
let mut h: u64 = 14695981039346656037; // FNV offset basis (64-bit)
|
|
246
|
+
for &c in bytes {
|
|
247
|
+
h ^= c as u64;
|
|
248
|
+
h = h.wrapping_mul(1099511628211);
|
|
249
|
+
}
|
|
250
|
+
h
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Derive the per-block-size seed from the (once-computed) content hash.
|
|
254
|
+
fn seed_from_base(base: u64, idx: usize) -> u64 {
|
|
255
|
+
base ^ ((idx as u64).wrapping_mul(0x9E3779B97F4A7C15))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[cfg(test)]
|
|
259
|
+
mod tests {
|
|
260
|
+
use super::*;
|
|
261
|
+
|
|
262
|
+
/// A binary sequence (alphabet 2 -> log_base auto = 2) of length 100, plus
|
|
263
|
+
/// `g = log_2(100)/100`. The block-entropy arithmetic only reads the length
|
|
264
|
+
/// and the alphabet, so the content is irrelevant to these tests.
|
|
265
|
+
fn fixture() -> (Sequence, LzArgs, f64) {
|
|
266
|
+
let mut data = vec![b'0'; 50];
|
|
267
|
+
data.extend(std::iter::repeat(b'1').take(50));
|
|
268
|
+
let seq = Sequence::from_bytes(data);
|
|
269
|
+
let mut args = LzArgs::new();
|
|
270
|
+
args.get_shuffle_terms = true;
|
|
271
|
+
let n = 100.0f64;
|
|
272
|
+
(seq, args, (n.ln() / 2f64.ln()) / n)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn emc_projects_the_excess_ladder() {
|
|
277
|
+
let (seq, args, g) = fixture();
|
|
278
|
+
// c_1 = 20, c_2 = 15, c_3 = 12 against C_LZ(u) = 10, so the raw ladder
|
|
279
|
+
// Ê(l) = l·g·(c_l − 10) is [10g, 10g, 6g] — non-monotone at l = 3.
|
|
280
|
+
let r = shuffle_entropy_calculation(&seq, &args, 10, &[0, 20, 15, 12], 3);
|
|
281
|
+
|
|
282
|
+
// Pooling all three violators gives one block at (10 + 10 + 6)/3 g.
|
|
283
|
+
let level = (26.0 / 3.0) * g;
|
|
284
|
+
assert!((r.emc_value - level).abs() < 1e-12, "{}", r.emc_value);
|
|
285
|
+
assert!((r.summands[0] - level).abs() < 1e-12);
|
|
286
|
+
assert_eq!(r.summands[1], 0.0);
|
|
287
|
+
assert_eq!(r.summands[2], 0.0);
|
|
288
|
+
// multi_information is the *raw* l = 1 contrast, not the fitted one.
|
|
289
|
+
assert!((r.multi_information - 10.0 * g).abs() < 1e-12);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[test]
|
|
293
|
+
fn emc_keeps_the_closed_form_when_the_ladder_is_monotone() {
|
|
294
|
+
let (seq, args, g) = fixture();
|
|
295
|
+
// c_l = 11 for every l gives Ê(l) = l·g, already strictly increasing,
|
|
296
|
+
// so the projection is the identity and the total is the old
|
|
297
|
+
// closed form mm·g·(c_mm − C_LZ(u)).
|
|
298
|
+
let r = shuffle_entropy_calculation(&seq, &args, 10, &[0, 11, 11, 11], 3);
|
|
299
|
+
|
|
300
|
+
assert!((r.emc_value - 3.0 * g).abs() < 1e-12, "{}", r.emc_value);
|
|
301
|
+
for s in &r.summands {
|
|
302
|
+
assert!((s - g).abs() < 1e-12, "{s}");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn emc_is_never_negative() {
|
|
308
|
+
let (seq, args, g) = fixture();
|
|
309
|
+
// Every surrogate less complex than the original: the raw ladder is
|
|
310
|
+
// [−10g, −20g, −30g], entirely below zero.
|
|
311
|
+
let r = shuffle_entropy_calculation(&seq, &args, 20, &[0, 10, 10, 10], 3);
|
|
312
|
+
|
|
313
|
+
assert_eq!(r.emc_value, 0.0);
|
|
314
|
+
assert!(r.summands.iter().all(|&s| s == 0.0), "{:?}", r.summands);
|
|
315
|
+
// The diagnostic still shows the negative contrast.
|
|
316
|
+
assert!((r.multi_information + 10.0 * g).abs() < 1e-12);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn isotonic_pools_adjacent_violators() {
|
|
321
|
+
// 3.0 then 2.0 violates monotonicity; they pool to 2.5.
|
|
322
|
+
assert_eq!(
|
|
323
|
+
non_negative_isotonic(&[1.0, 3.0, 2.0, 4.0]),
|
|
324
|
+
vec![1.0, 2.5, 2.5, 4.0]
|
|
325
|
+
);
|
|
326
|
+
// Already isotonic: unchanged apart from the non-negativity clamp.
|
|
327
|
+
assert_eq!(non_negative_isotonic(&[-1.0, 0.5]), vec![0.0, 0.5]);
|
|
328
|
+
// A strictly decreasing input pools into one block.
|
|
329
|
+
assert_eq!(non_negative_isotonic(&[6.0, 3.0, 0.0]), vec![3.0, 3.0, 3.0]);
|
|
330
|
+
assert_eq!(non_negative_isotonic(&[]), Vec::<f64>::new());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn summands_are_non_negative_and_sum_to_the_value() {
|
|
335
|
+
// A real end-to-end run: period-8 motif, n = 2000.
|
|
336
|
+
let data: Vec<u8> = b"00010111".iter().copied().cycle().take(2000).collect();
|
|
337
|
+
let seq = Sequence::from_bytes(data);
|
|
338
|
+
let mut args = LzArgs::new();
|
|
339
|
+
args.get_shuffle_terms = true;
|
|
340
|
+
let r = lz76_random_shuffle_complexity(&seq, &args);
|
|
341
|
+
|
|
342
|
+
assert_eq!(r.summands.len(), r.max_block_size as usize);
|
|
343
|
+
assert!(r.emc_value > 0.0, "expected structure, got {}", r.emc_value);
|
|
344
|
+
assert!(r.summands.iter().all(|&s| s >= 0.0), "{:?}", r.summands);
|
|
345
|
+
let total: f64 = r.summands.iter().sum();
|
|
346
|
+
assert!(
|
|
347
|
+
(total - r.emc_value).abs() < 1e-12,
|
|
348
|
+
"{total} vs {}",
|
|
349
|
+
r.emc_value
|
|
350
|
+
);
|
|
351
|
+
// The ladder is non-decreasing, so partial sums climb to the total.
|
|
352
|
+
let mut acc = 0.0;
|
|
353
|
+
for &s in &r.summands {
|
|
354
|
+
acc += s;
|
|
355
|
+
assert!(acc <= r.emc_value + 1e-12);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -177,9 +177,28 @@ fn h(
|
|
|
177
177
|
/// content. Higher values mean more structure than random noise of the same
|
|
178
178
|
/// length and alphabet.
|
|
179
179
|
///
|
|
180
|
+
/// For each block size ``l = 1 ... max_block_size`` the sequence is shuffled in
|
|
181
|
+
/// blocks of length ``l`` and re-factorized, which gives one estimate of the
|
|
182
|
+
/// *excess entropy accumulated up to scale l*. Excess entropy is a mutual
|
|
183
|
+
/// information between the past and the future, so that ladder must be
|
|
184
|
+
/// non-negative and non-decreasing in ``l``; the raw estimates are not, because
|
|
185
|
+
/// each rests on its own surrogate draw. They are projected onto that shape
|
|
186
|
+
/// (non-negative isotonic regression) before the terms are read off, so
|
|
187
|
+
/// **every returned value is non-negative** and every block size informs the
|
|
188
|
+
/// total.
|
|
189
|
+
///
|
|
180
190
|
/// The shuffle is seeded deterministically from the sequence content, so
|
|
181
191
|
/// repeated calls on the same input produce identical results.
|
|
182
192
|
///
|
|
193
|
+
/// .. warning::
|
|
194
|
+
///
|
|
195
|
+
/// Treat the value as an **ordinal index at fixed sequence length**, not as
|
|
196
|
+
/// a bit count. It is biased upward (roughly 2-3x the analytic excess
|
|
197
|
+
/// entropy for order-1 Markov sources) and it scales with
|
|
198
|
+
/// ``max_block_size``, which itself grows with length -- so never compare
|
|
199
|
+
/// values across sequences of different length. Report it alongside
|
|
200
|
+
/// :func:`h`.
|
|
201
|
+
///
|
|
183
202
|
/// Parameters
|
|
184
203
|
/// ----------
|
|
185
204
|
/// seq : str | bytes | list[int] | list[str] | Iterable[int]
|
|
@@ -198,9 +217,15 @@ fn h(
|
|
|
198
217
|
/// Returns
|
|
199
218
|
/// -------
|
|
200
219
|
/// tuple[float, list[float]]
|
|
201
|
-
/// ``(emc_value, summands)
|
|
202
|
-
/// complexity
|
|
203
|
-
///
|
|
220
|
+
/// ``(emc_value, summands)``. ``emc_value`` is the effective measure
|
|
221
|
+
/// complexity, always ``>= 0.0``. ``summands`` has one entry per block
|
|
222
|
+
/// size, ``summands[l-1]`` estimating the scale-``l`` conditional-entropy
|
|
223
|
+
/// excess ``h(l) - h``; every entry is ``>= 0.0`` and they sum to
|
|
224
|
+
/// ``emc_value``. Their running sum is the excess entropy captured up to
|
|
225
|
+
/// each scale, so a profile that keeps climbing past the first few entries
|
|
226
|
+
/// indicates long-range or hierarchical correlation. Zero entries mean
|
|
227
|
+
/// neighbouring scales were pooled by the projection, not that the scale
|
|
228
|
+
/// was skipped.
|
|
204
229
|
///
|
|
205
230
|
/// Examples
|
|
206
231
|
/// --------
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lzcomplexity"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0" # x-release-please-version
|
|
8
8
|
description = "LempelZiv-76 complexity engine. Suited for complexity analysis of time series."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Efrén Aragón Pérez", email = "efrenaragon96@gmail.com" },
|
|
@@ -54,8 +54,12 @@ def emc(
|
|
|
54
54
|
) -> Tuple[float, List[float]]:
|
|
55
55
|
"""Effective measure complexity via random block shuffling.
|
|
56
56
|
|
|
57
|
-
Returns ``(emc_value, summands)
|
|
58
|
-
|
|
57
|
+
Returns ``(emc_value, summands)``. Both the value and every summand are
|
|
58
|
+
non-negative, and the summands sum to the value; ``summands[l-1]``
|
|
59
|
+
estimates the scale-``l`` conditional-entropy excess ``h(l) - h``.
|
|
60
|
+
|
|
61
|
+
Read the value as an ordinal index at fixed sequence length, not as a bit
|
|
62
|
+
count, and never compare it across lengths.
|
|
59
63
|
"""
|
|
60
64
|
...
|
|
61
65
|
|
lzcomplexity-1.0.2/Cargo.toml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
[workspace]
|
|
2
|
-
resolver = "2"
|
|
3
|
-
members = ["crates/lzcomplexity-core", "crates/lzcomplexity-py"]
|
|
4
|
-
|
|
5
|
-
[workspace.package]
|
|
6
|
-
version = "1.0.2" # x-release-please-version
|
|
7
|
-
edition = "2021"
|
|
8
|
-
rust-version = "1.75"
|
|
9
|
-
license = "MIT"
|
|
10
|
-
authors = [
|
|
11
|
-
"Efrén Aragón Pérez <efrenaragon96@gmail.com>",
|
|
12
|
-
"Daniel Estévez Moya <kemossabee@gmail.com>",
|
|
13
|
-
"Ernesto Estévez Rams <estevez@fisica.uh.cu>",
|
|
14
|
-
]
|
|
15
|
-
repository = "https://github.com/pleros-ai/lzcomplexity"
|
|
16
|
-
|
|
17
|
-
[workspace.dependencies]
|
|
18
|
-
rayon = "1.10"
|
|
19
|
-
rand = "0.8"
|
|
20
|
-
rand_chacha = "0.3"
|
|
21
|
-
cdivsufsort = "2.0"
|
|
22
|
-
clap = { version = "4.5", features = ["derive"] }
|
|
23
|
-
serde = { version = "1", features = ["derive"] }
|
|
24
|
-
serde_json = "1"
|
|
25
|
-
flate2 = "1.0"
|
|
26
|
-
|
|
27
|
-
[profile.release]
|
|
28
|
-
lto = "thin"
|
|
29
|
-
codegen-units = 1
|
|
30
|
-
opt-level = 3
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
//! Shuffle-based complexity measures.
|
|
2
|
-
|
|
3
|
-
use rayon::prelude::*;
|
|
4
|
-
|
|
5
|
-
use crate::lz76::{lz76_factorization, LzArgs, LzShuffle};
|
|
6
|
-
use crate::sequence::{shuffle_copy_seeded, Sequence};
|
|
7
|
-
use crate::NO_ALPHABET;
|
|
8
|
-
|
|
9
|
-
/// Estimate the "naive M" satisfying `N = M * 2^M`, mirroring `max_block_size` in C++.
|
|
10
|
-
pub fn max_block_size(size: usize) -> usize {
|
|
11
|
-
if size < 10 {
|
|
12
|
-
return 1;
|
|
13
|
-
}
|
|
14
|
-
let mut m: usize = 10;
|
|
15
|
-
let mut m_old: usize = 1;
|
|
16
|
-
let mut count: u32 = 0;
|
|
17
|
-
while count < 100 {
|
|
18
|
-
let est = ((size as f64) / (m_old as f64)).ln() / 2f64.ln();
|
|
19
|
-
m = est.round() as usize;
|
|
20
|
-
count += 1;
|
|
21
|
-
if m == m_old {
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
m_old = m;
|
|
25
|
-
}
|
|
26
|
-
m
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/// Compute the random-shuffle factorization vector and `mm`. Mirrors
|
|
30
|
-
/// `ShuffleFactorization` from the C++.
|
|
31
|
-
pub fn shuffle_factorization(seq: &Sequence, args: &LzArgs) -> (Vec<i32>, usize) {
|
|
32
|
-
let mut mm: i64 = args.block_size as i64;
|
|
33
|
-
if mm <= 0 {
|
|
34
|
-
mm = max_block_size(seq.len()) as i64;
|
|
35
|
-
if seq.len() > 50 {
|
|
36
|
-
mm += 10;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
let mm = mm as usize;
|
|
40
|
-
|
|
41
|
-
// Index 0 unused, indices 1..=mm hold the per-block-size complexity.
|
|
42
|
-
let mut res: Vec<i32> = vec![0; mm + 3];
|
|
43
|
-
|
|
44
|
-
// Hash the sequence content ONCE; each per-block seed only differs by the
|
|
45
|
-
// `idx` term (bit-identical seeds → identical shuffles → identical EMC).
|
|
46
|
-
let base = fnv1a(seq.as_bytes());
|
|
47
|
-
let computed: Vec<(usize, i32)> = (1..=mm)
|
|
48
|
-
.into_par_iter()
|
|
49
|
-
.map(|idx| {
|
|
50
|
-
let seed = seed_from_base(base, idx);
|
|
51
|
-
let rand_seq = shuffle_copy_seeded(seq, idx as u32, (seq.len() / 2) as u32, seed);
|
|
52
|
-
let c = lz76_factorization(&rand_seq, args);
|
|
53
|
-
(idx, c as i32)
|
|
54
|
-
})
|
|
55
|
-
.collect();
|
|
56
|
-
for (idx, c) in computed {
|
|
57
|
-
res[idx] = c;
|
|
58
|
-
}
|
|
59
|
-
(res, mm)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/// Effective measure complexity via the block-entropy estimator.
|
|
63
|
-
///
|
|
64
|
-
/// For each block size `l`, `h_rand[l]` is `C_LZ(u^{RS(l)})`, the LZ complexity
|
|
65
|
-
/// of `u` block-shuffled at scale `l`. From it we form a block-entropy estimate
|
|
66
|
-
/// `H_l = l * C_LZ(u^{RS(l)}) * log_k(N) / N` (with `H_0 = 0`), take the entropy
|
|
67
|
-
/// gain `ΔH_l = H_l - H_{l-1}`, and sum each gain's excess over the original
|
|
68
|
-
/// sequence's entropy density `ĥ = C_LZ(u) * log_k(N) / N`:
|
|
69
|
-
///
|
|
70
|
-
/// ```text
|
|
71
|
-
/// Ê = Σ_{l=1}^{mm} (ΔH_l - ĥ)
|
|
72
|
-
/// ```
|
|
73
|
-
pub fn shuffle_entropy_calculation(
|
|
74
|
-
seq: &Sequence,
|
|
75
|
-
args: &LzArgs,
|
|
76
|
-
complexity: i32,
|
|
77
|
-
h_rand: &[i32],
|
|
78
|
-
mm: usize,
|
|
79
|
-
) -> LzShuffle {
|
|
80
|
-
let mut result = LzShuffle {
|
|
81
|
-
max_block_size: mm as i32,
|
|
82
|
-
..Default::default()
|
|
83
|
-
};
|
|
84
|
-
if args.get_shuffle_terms {
|
|
85
|
-
result.summands = vec![0.0; mm];
|
|
86
|
-
}
|
|
87
|
-
let log_base = if args.log_base == NO_ALPHABET {
|
|
88
|
-
seq.alphabet_size().max(2)
|
|
89
|
-
} else {
|
|
90
|
-
args.log_base.max(2)
|
|
91
|
-
} as f64;
|
|
92
|
-
|
|
93
|
-
let n = seq.len() as f64;
|
|
94
|
-
if n <= 1.0 || mm == 0 {
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
// g = log_k(N) / N, so ĥ = C_LZ(u)·g and H_l = l·C_LZ(u^{RS(l)})·g.
|
|
98
|
-
let g = (n.ln() / log_base.ln()) / n;
|
|
99
|
-
let h_hat = complexity as f64 * g; // entropy density of the original sequence
|
|
100
|
-
|
|
101
|
-
let mut emc = 0.0f64;
|
|
102
|
-
let mut h_prev = 0.0f64; // H_0 = 0
|
|
103
|
-
for l in 1..=mm {
|
|
104
|
-
if l >= h_rand.len() {
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
let h_l = l as f64 * h_rand[l] as f64 * g;
|
|
108
|
-
let term = (h_l - h_prev) - h_hat; // ΔH_l - ĥ
|
|
109
|
-
emc += term;
|
|
110
|
-
if args.get_shuffle_terms {
|
|
111
|
-
result.summands[l - 1] = term;
|
|
112
|
-
}
|
|
113
|
-
if l == 1 {
|
|
114
|
-
result.multi_information = term;
|
|
115
|
-
}
|
|
116
|
-
h_prev = h_l;
|
|
117
|
-
}
|
|
118
|
-
result.emc_value = emc;
|
|
119
|
-
result
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/// `lz76RandomShuffleComplexity` — full pipeline.
|
|
123
|
-
pub fn lz76_random_shuffle_complexity(seq: &Sequence, args: &LzArgs) -> LzShuffle {
|
|
124
|
-
lz76_random_shuffle_complexity_with(seq, args, lz76_factorization(seq, args) as i32)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/// Same, but with an already-computed original-sequence complexity (so callers
|
|
128
|
-
/// that already have it — e.g. the `lz76` driver — skip one factorization).
|
|
129
|
-
pub fn lz76_random_shuffle_complexity_with(
|
|
130
|
-
seq: &Sequence,
|
|
131
|
-
args: &LzArgs,
|
|
132
|
-
complexity: i32,
|
|
133
|
-
) -> LzShuffle {
|
|
134
|
-
let (h_rand, mm) = shuffle_factorization(seq, args);
|
|
135
|
-
shuffle_entropy_calculation(seq, args, complexity, &h_rand, mm)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/// `lz76PairedShuffleComplexity` — shuffle on the merged (Z) sequence.
|
|
139
|
-
pub fn lz76_paired_shuffle_complexity(seq: &Sequence, args: &LzArgs) -> LzShuffle {
|
|
140
|
-
let mid = seq.len() / 2;
|
|
141
|
-
let (past, future) = seq.split_at(mid);
|
|
142
|
-
let merged = merge_sequences(&past, &future);
|
|
143
|
-
let complexity = lz76_factorization(&merged, args) as i32;
|
|
144
|
-
let (h_rand, mm) = shuffle_factorization(&merged, args);
|
|
145
|
-
shuffle_entropy_calculation(&merged, args, complexity, &h_rand, mm)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/// Mirror of `internal::MergeSequences` — element-wise pairing into a new alphabet.
|
|
149
|
-
pub fn merge_sequences(s1: &Sequence, s2: &Sequence) -> Sequence {
|
|
150
|
-
let max_iter = s1.len().min(s2.len());
|
|
151
|
-
let mut buf = Vec::with_capacity(max_iter);
|
|
152
|
-
let mut lookup = vec![0u8; 65536];
|
|
153
|
-
let mut next_symbol: u8 = b'0';
|
|
154
|
-
let a = s1.as_bytes();
|
|
155
|
-
let b = s2.as_bytes();
|
|
156
|
-
for i in 0..max_iter {
|
|
157
|
-
let key = ((a[i] as u16) << 8) | (b[i] as u16);
|
|
158
|
-
let entry = &mut lookup[key as usize];
|
|
159
|
-
if *entry == 0 {
|
|
160
|
-
*entry = next_symbol;
|
|
161
|
-
next_symbol = next_symbol.wrapping_add(1);
|
|
162
|
-
}
|
|
163
|
-
buf.push(*entry);
|
|
164
|
-
}
|
|
165
|
-
Sequence::from_bytes_with_alphabet(buf, s1.alphabet_size().saturating_mul(s2.alphabet_size()))
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/// FNV-1a hash of the sequence bytes (the content-dependent part of the seed).
|
|
169
|
-
fn fnv1a(bytes: &[u8]) -> u64 {
|
|
170
|
-
let mut h: u64 = 14695981039346656037; // FNV offset basis (64-bit)
|
|
171
|
-
for &c in bytes {
|
|
172
|
-
h ^= c as u64;
|
|
173
|
-
h = h.wrapping_mul(1099511628211);
|
|
174
|
-
}
|
|
175
|
-
h
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/// Derive the per-block-size seed from the (once-computed) content hash.
|
|
179
|
-
fn seed_from_base(base: u64, idx: usize) -> u64 {
|
|
180
|
-
base ^ ((idx as u64).wrapping_mul(0x9E3779B97F4A7C15))
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
#[cfg(test)]
|
|
184
|
-
mod tests {
|
|
185
|
-
use super::*;
|
|
186
|
-
|
|
187
|
-
#[test]
|
|
188
|
-
fn emc_block_entropy_formula() {
|
|
189
|
-
// Binary sequence (alphabet 2 -> log_base auto = 2), n = 100.
|
|
190
|
-
let mut data = vec![b'0'; 50];
|
|
191
|
-
data.extend(std::iter::repeat(b'1').take(50));
|
|
192
|
-
let seq = Sequence::from_bytes(data);
|
|
193
|
-
let mut args = LzArgs::new();
|
|
194
|
-
args.get_shuffle_terms = true;
|
|
195
|
-
|
|
196
|
-
let complexity = 10i32; // C_LZ(u)
|
|
197
|
-
let h_rand = vec![0, 20, 15, 12]; // index 0 unused; c_1=20, c_2=15, c_3=12
|
|
198
|
-
let mm = 3usize;
|
|
199
|
-
let r = shuffle_entropy_calculation(&seq, &args, complexity, &h_rand, mm);
|
|
200
|
-
|
|
201
|
-
// Hand-computed: g = log_2(100)/100, ĥ = 10·g, H_l = l·c_l·g (H_0 = 0).
|
|
202
|
-
let n = 100.0f64;
|
|
203
|
-
let g = (n.ln() / 2f64.ln()) / n;
|
|
204
|
-
let h_hat = 10.0 * g;
|
|
205
|
-
let (h1, h2, h3) = (1.0 * 20.0 * g, 2.0 * 15.0 * g, 3.0 * 12.0 * g);
|
|
206
|
-
let expected = (h1 - 0.0 - h_hat) + (h2 - h1 - h_hat) + (h3 - h2 - h_hat);
|
|
207
|
-
assert!(
|
|
208
|
-
(r.emc_value - expected).abs() < 1e-12,
|
|
209
|
-
"{} vs {}",
|
|
210
|
-
r.emc_value,
|
|
211
|
-
expected
|
|
212
|
-
);
|
|
213
|
-
assert!((r.summands[0] - (h1 - h_hat)).abs() < 1e-12);
|
|
214
|
-
assert!((r.multi_information - (h1 - h_hat)).abs() < 1e-12);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|