Skip to content

Commit 98d5c17

Browse files
Merge pull request #12 from DennisWayo/feature/reproducible-sine-noise-padding
Add reproducible sine noise and robust signal padding
2 parents eb682de + a3066e7 commit 98d5c17

4 files changed

Lines changed: 69 additions & 16 deletions

File tree

src/signals/SignalConverters.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ export signal_mps, signal_ztmps
1515
# Convert an input 1D array signal into ITensor MPS with the binary encoding of the indices
1616
function _array_to_tensor(x::AbstractVector; sites=undef)
1717
N = length(x)
18-
n = round(Int, log2(N)) # no. of qubits that can encodes signal
18+
N > 0 || throw(ArgumentError("_array_to_tensor: Input signal must be non-empty."))
19+
n = ceil(Int, log2(N)) # no. of qubits that can encode signal
20+
padded_length = 2^n
1921

2022
ITensors.disable_warn_order()
2123
try
22-
# If signal is not a power of 2, fill with 0s upto length 2^n with a warning
23-
if N < 2^n
24-
@warn "_array_to_tensor: Input signal length $N is not a power of 2. Filling with zeros upto length $(2^n). We recommend providing signals of length power-of-2 for best performance."
25-
x_filled = zeros(2^n)
24+
# If signal is not a power of 2, fill with 0s up to the next power of 2 with a warning.
25+
if N != padded_length
26+
@warn "_array_to_tensor: Input signal length $N is not a power of 2. Filling with zeros upto length $(padded_length). We recommend providing signals of length power-of-2 for best performance."
27+
x_filled = zeros(eltype(x), padded_length)
2628
x_filled[1:N] .= x
2729
x = x_filled
2830
end
29-
@assert length(x) == 2^n ||
31+
@assert length(x) == padded_length ||
3032
"_array_to_tensor: Length of signal vector must be a power of 2"
3133
if sites === undef
3234
sites = [Index(2; tags=@sprintf("site-%d", i)) for i in 1:n]

src/signals/Signals.jl

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ function _generate_signal(
1818
freq::Float64;
1919
phase::Float64=0.0,
2020
noise_level::Float64=0.0,
21+
seed::Union{Nothing,Int}=nothing,
2122
kwargs...,
2223
)
2324
jvals = 0:(2^n-1)
24-
return [sin(freq * dt * j + phase) + noise_level * randn() for j in jvals]
25+
rng = isnothing(seed) ? Random.default_rng() : Xoshiro(seed)
26+
return [sin(freq * dt * j + phase) + noise_level * randn(rng) for j in jvals]
2527
end
2628

2729
function _generate_signal(
@@ -50,13 +52,15 @@ function _generate_signal(
5052
freq::Vector{Float64};
5153
phase::Vector{Float64}=zeros(length(freq)),
5254
noise_level::Float64=0.0,
55+
seed::Union{Nothing,Int}=nothing,
5356
kwargs...,
5457
)
5558
jvals = 0:(2^n-1)
5659
length(freq) == length(phase) ||
5760
throw(ArgumentError("Frequency and phase vectors must be of the same length."))
61+
rng = isnothing(seed) ? Random.default_rng() : Xoshiro(seed)
5862
return [
59-
sum(sin* dt * j + φ) for (ω, φ) in zip(freq, phase)) + noise_level * randn() for
63+
sum(sin* dt * j + φ) for (ω, φ) in zip(freq, phase)) + noise_level * randn(rng) for
6064
j in jvals
6165
]
6266
end
@@ -162,14 +166,18 @@ Generate a length-`2^n` real signal of a specified type.
162166
Can be a scalar or a vector of frequencies. Defaults to `2π` if not provided.
163167
164168
# kwargs (kind-dependent)
165-
- `phase::Float64`: The phase offset of the signal in radians. Applicable to `:sin` and `:cos` kinds.
166-
Defaults to `0.0`.
167-
- `decay_rate::Float64`: The rate of exponential decay. Only applicable to the `:decay` kind.
168-
A higher value results in faster decay. Defaults to `1.0`.
169-
- `noise_level::Float64`: The amplitude of random noise added to the signal. Applicable to all kinds.
170-
Defaults to `0.0` (no noise).
171-
- `seed::Int`: The random seed for reproducibility of noise generation. Applicable when `noise_level > 0`.
172-
Defaults to `nothing` (random seed).
169+
- `phase`: Phase offset in radians.
170+
- For `:sin` with scalar frequency, use `phase::Float64` (default `0.0`).
171+
- For `:sin` with vector frequency, use `phase::Vector{Float64}` (defaults to zeros).
172+
- For `:sin_decay`, phase is optional (`Float64` or `Vector{Float64}` depending on frequency type).
173+
- `decay_rate`: Exponential damping rate. Required for `:sin_decay`
174+
(`Float64` for scalar-frequency input, `Vector{Float64}` for vector-frequency input).
175+
- `noise_level::Float64`: The amplitude of Gaussian noise added to `:sin` signals. Defaults to `0.0`
176+
(no noise).
177+
- `seed`: Random seed control:
178+
- For `:sin` with `noise_level > 0`, pass `seed::Int` for reproducible noise; default `nothing`
179+
(use current global RNG state).
180+
- For `:random`, pass `seed::Int` (default `1234`).
173181
174182
# Returns
175183
- `signal::Vector{Float64}`: A real-valued vector of length `2^n` representing the generated signal.

test/test_signal_converters.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ using QILaplace.SignalConverters:
1919
ψ_padded, norm_padded = _array_to_tensor(x_non_pow2)
2020
@test length(inds(ψ_padded)) == 2 # rounds up to 4 = 2^2
2121
@test isapprox(norm_padded, sqrt(1.0 + 4.0 + 9.0); rtol=1e-12)
22+
23+
# Test with non-power-of-2 requiring ceil(log2(N)) (5 -> 8)
24+
x_non_pow2_big = [1.0, 2.0, 3.0, 4.0, 5.0]
25+
ψ_padded_big, norm_padded_big = _array_to_tensor(x_non_pow2_big)
26+
@test length(inds(ψ_padded_big)) == 3 # rounds up to 8 = 2^3
27+
@test isapprox(norm_padded_big, sqrt(sum(abs2, x_non_pow2_big)); rtol=1e-12)
28+
29+
# Non-power-of-2 complex signal should pad without type errors.
30+
x_non_pow2_complex = ComplexF64[1.0 + 1.0im, 2.0 - 1.0im, 3.0 + 0.5im]
31+
ψ_padded_complex, norm_padded_complex = _array_to_tensor(x_non_pow2_complex)
32+
@test length(inds(ψ_padded_complex)) == 2
33+
@test isapprox(norm_padded_complex, sqrt(sum(abs2, x_non_pow2_complex)); rtol=1e-12)
34+
35+
# Through public API, padded tail coefficients should be exactly zero.
36+
ψ_non_pow2 = signal_mps(x_non_pow2_big; method=:svd, cutoff=0.0, maxdim=typemax(Int))
37+
for i in 0:4
38+
@test isapprox(coefficient(ψ_non_pow2, i), x_non_pow2_big[i+1]; atol=1e-12)
39+
end
40+
for i in 5:7
41+
@test isapprox(coefficient(ψ_non_pow2, i), 0.0; atol=1e-12)
42+
end
2243
end
2344

2445
# ==================== Test _tensor_to_mps_svd ====================

test/test_signals.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,28 @@ end
7878
r2 = generate_signal(nrand, kind=:random, seed=42)
7979
@test r1 == r2
8080

81+
# seeded noise for :sin should be reproducible
82+
s_noise_1 = generate_signal(
83+
n, kind=:sin, dt=dt, freq=freq, phase=phase, noise_level=0.1, seed=2026
84+
)
85+
s_noise_2 = generate_signal(
86+
n, kind=:sin, dt=dt, freq=freq, phase=phase, noise_level=0.1, seed=2026
87+
)
88+
s_noise_3 = generate_signal(
89+
n, kind=:sin, dt=dt, freq=freq, phase=phase, noise_level=0.1, seed=2027
90+
)
91+
@test s_noise_1 == s_noise_2
92+
@test s_noise_1 != s_noise_3
93+
94+
# seeded noise for vector-frequency :sin path should also be reproducible
95+
s_noise_vec_1 = generate_signal(
96+
n, kind=:sin, dt=dt, freq=[1.0, 2.0], phase=[0.0, 0.5], noise_level=0.1, seed=77
97+
)
98+
s_noise_vec_2 = generate_signal(
99+
n, kind=:sin, dt=dt, freq=[1.0, 2.0], phase=[0.0, 0.5], noise_level=0.1, seed=77
100+
)
101+
@test s_noise_vec_1 == s_noise_vec_2
102+
81103
# integer freq should work like float
82104
sint = generate_signal(n, kind=:sin, dt=dt, freq=1)
83105
sfloat = generate_signal(n, kind=:sin, dt=dt, freq=1.0)

0 commit comments

Comments
 (0)