Skip to content

Commit 113146c

Browse files
authored
Quantization (#10)
* add stable noise sources and tutorial * add quantization * add stats * add quantization tutorial * add quantization mode midtread
1 parent 070a375 commit 113146c

File tree

5 files changed

+219
-8
lines changed

5 files changed

+219
-8
lines changed

Project.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
1414

1515
[compat]
1616
DiffEqCallbacks = "~3.8"
17-
julia = "1.10"
18-
StableRNGs = "1"
1917
JuliaSimCompiler = "0.1.19"
2018
ModelingToolkit = "9"
2119
ModelingToolkitStandardLibrary = "2"
2220
OrdinaryDiffEq = "6.89"
2321
Random = "1"
24-
22+
StableRNGs = "1"
23+
julia = "1.10"
2524

2625
[extras]
2726
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
27+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
2828
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2929

3030
[targets]
31-
test = ["ControlSystemsBase", "Test"]
31+
test = ["ControlSystemsBase", "Test", "Statistics"]

docs/src/tutorials/noise.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,50 @@ Internally, a random number generator from [StableRNGs.jl](https://github.com/Ju
119119
2. Multiple calls to the random number generator at the same time step all return the same number.
120120

121121
## Quantization
122-
Not yet available.
122+
123+
A signal may be quantized to a fixed number of levels (e.g., 8-bit) using the [`Quantization`](@ref) block. This may be used to simulate, e.g., the quantization that occurs in a AD converter. Below, we have a simple example where a sine wave is quantized to 2 bits (4 levels), limited between -1 and 1:
124+
```@example QUANT
125+
using ModelingToolkit, ModelingToolkitSampledData, OrdinaryDiffEq, Plots
126+
using ModelingToolkit: t_nounits as t, D_nounits as D
127+
z = ShiftIndex(Clock(0.1))
128+
@mtkmodel QuantizationModel begin
129+
@components begin
130+
input = Sine(amplitude=1.5, frequency=1)
131+
quant = Quantization(; z, bits=2, y_min = -1, y_max = 1)
132+
end
133+
@variables begin
134+
x(t) = 0 # Dummy variable to work around a bug for models without continuous-time state
135+
end
136+
@equations begin
137+
connect(input.output, quant.input)
138+
D(x) ~ 0 # Dummy equation
139+
end
140+
end
141+
@named m = QuantizationModel()
142+
m = complete(m)
143+
ssys = structural_simplify(IRSystem(m))
144+
prob = ODEProblem(ssys, [], (0.0, 2.0))
145+
sol = solve(prob, Tsit5())
146+
plot(sol, idxs=m.input.output.u)
147+
plot!(sol, idxs=m.quant.y, label="Quantized output")
148+
```
149+
150+
151+
152+
### Different quantization modes
153+
With the default option `midrise = true`, the output of the quantizer is always between `y_min` and `y_max` inclusive, and the number of distinct levels it can take is `2^bits`. The possible values are given by
154+
```@example
155+
bits = 2; y_min = -1; y_max = 1
156+
collect(range(y_min, stop=y_max, length=2^bits))
157+
```
158+
Notably, these possible levels _do not include 0_. If `midrise = false`, a mid-tread quantizer is used instead. The two options are visualized below:
159+
```@example QUANT
160+
y_min = -1; y_max = 1; bits = 2
161+
u = y_min:0.01:y_max
162+
y_mr = ModelingToolkitSampledData.quantize_midrise.(u, bits, y_min, y_max)
163+
y_mt = ModelingToolkitSampledData.quantize_midtread.(u, bits, y_min, y_max)
164+
plot(u, [y_mr y_mt], label=["Midrise" "Midtread"], xlabel="Input", ylabel="Output", framestyle=:zerolines, l=2, seriestype=:step)
165+
```
166+
Note how the default mid-rise quantizer mode has a rise at the middle of the interval, while the mid-tread mode has a flat region (a tread) centered around the middle of the interval.
167+
168+
The default option `midrise = true` includes both end points as possible output values, while `midrise = false` does not include the upper limit.

src/ModelingToolkitSampledData.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export get_clock
77
export DiscreteIntegrator, DiscreteDerivative, Delay, Difference, ZeroOrderHold, Sampler,
88
ClockChanger,
99
DiscretePIDParallel, DiscretePIDStandard, DiscreteStateSpace,
10-
DiscreteTransferFunction, NormalNoise, UniformNoise
10+
DiscreteTransferFunction, NormalNoise, UniformNoise, Quantization
1111
include("discrete_blocks.jl")
1212

1313
end

src/discrete_blocks.jl

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,4 +876,67 @@ See also [ControlSystemsMTK.jl](https://juliacontrol.github.io/ControlSystemsMTK
876876
# push!(eqs, input.u ~ u)
877877
# push!(eqs, output.u ~ y)
878878
# compose(ODESystem(eqs, t, sts, pars; name = name), input, output)
879-
# end
879+
# end
880+
881+
"""
882+
Quantization
883+
884+
A quantization block that quantizes the input signal to a specified number of bits.
885+
886+
# Parameters:
887+
- `y_max`: Upper limit of output
888+
- `y_min`: Lower limit of output
889+
- `bits`: Number of bits of quantization
890+
- `quantized`: If quantization effects shall be computed. If false, the output is equal to the input, which may be useful for, e.g., linearization.
891+
892+
# Connectors:
893+
- `input`
894+
- `output`
895+
896+
# Variables
897+
- `y`: Output signal, equal to `output.u`
898+
- `u`: Input signal, equal to `input.u`
899+
"""
900+
@mtkmodel Quantization begin
901+
@extend u, y = siso = SISO()
902+
@structural_parameters begin
903+
z = ShiftIndex()
904+
midrise = true
905+
end
906+
@parameters begin
907+
y_max = 1, [description = "Upper limit of output"]
908+
y_min = -1, [description = "Lower limit of output"]
909+
bits::Int = 8, [description = "Number of bits of quantization"]
910+
quantized::Bool = true, [description = "If quantization effects shall be computed."]
911+
end
912+
begin
913+
end
914+
@equations begin
915+
y(z) ~ ifelse(quantized == true, quantize(u(z), bits, y_min, y_max, midrise), u(z))
916+
end
917+
end
918+
919+
function quantize_midrise(u, bits, y_min, y_max)
920+
d = y_max - y_min
921+
y1 = clamp(u, y_min, y_max)
922+
y2 = (y1 - y_min) / d # between 0 and 1
923+
Δ = 2^Int(bits)-1
924+
y3 = round(y2 * Δ) / Δ # quantized between 0 and 1
925+
y4 = y3*d + y_min
926+
return y4
927+
end
928+
929+
function quantize_midtread(u, bits, y_min, y_max)
930+
Δ = (y_max - y_min) / (2^Int(bits)-1)
931+
# clamp(Δ * floor(u / Δ + 0.5), y_min, y_max)
932+
k = sign(u) * max(0, floor((abs(u) - Δ/2) / Δ + 1))
933+
y0 = sign(k) */2 + Δ*(abs(k)-1/2))
934+
y1 = iszero(y0) ? zero(y0) : y0 # remove -0.0
935+
return clamp(y1, y_min, y_max - Δ/2)
936+
end
937+
938+
function quantize(u, bits, y_min, y_max, midrise)
939+
midrise ? quantize_midrise(u, bits, y_min, y_max) : quantize_midtread(u, bits, y_min, y_max)
940+
end
941+
942+
@register_symbolic quantize(u::Real, bits::Real, y_min::Real, y_max::Real, midrise::Bool)

test/test_discrete_blocks.jl

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,33 @@ using Statistics
324324
@named m = NoiseModel()
325325
m = complete(m)
326326
ssys = structural_simplify(IRSystem(m))
327-
prob = ODEProblem(ssys, [], (0.0, 10.0))
327+
prob = ODEProblem(ssys, [m.noise.y(k-1) => 0], (0.0, 10.0))
328+
sol = solve(prob, Tsit5())
329+
@test !all(iszero, sol.u)
330+
tv = 0:k.clock.dt:sol.t[end]
331+
@test std(sol(tv, idxs = m.plant.u)) 1 rtol=0.1
332+
@test mean(sol(tv, idxs = m.plant.u)) 0 atol=0.08
333+
end
334+
335+
@testset "UniformNoise" begin
336+
k = ShiftIndex(Clock(0.01))
337+
338+
@mtkmodel NoiseModel begin
339+
@components begin
340+
noise = UniformNoise(z = k)
341+
zoh = ZeroOrderHold(z = k)
342+
plant = FirstOrder(T = 1e-4) # Included due to bug with only discrete-time systems
343+
end
344+
@equations begin
345+
connect(noise.output, zoh.input)
346+
connect(zoh.output, plant.input)
347+
end
348+
end
349+
350+
@named m = NoiseModel()
351+
m = complete(m)
352+
ssys = structural_simplify(IRSystem(m))
353+
prob = ODEProblem(ssys, [m.noise.y(k-1) => 0], (0.0, 10.0))
328354
sol = solve(prob, Tsit5())
329355
@test !all(iszero, sol.u)
330356
tv = 0:k.clock.dt:sol.t[end]
@@ -385,3 +411,79 @@ end
385411
# @test reduce(vcat, sol((0:10) .+ 1e-2))[:]≈[zeros(2); 1; zeros(8)] atol=1e-2
386412
# end*
387413

414+
415+
@testset "quantization" begin
416+
@info "Testing quantization"
417+
418+
function test_quant(y_min, y_max, bits)
419+
u = y_min:(1/2^bits):y_max
420+
y = ModelingToolkitSampledData.quantize_midrise.(u, bits, y_min, y_max)
421+
uy = unique(y)
422+
@test uy range(y_min, stop=y_max, length=2^bits)
423+
end
424+
425+
test_quant(-1, 1, 2) # Symmetric
426+
test_quant(-1, 2, 2) # Not symmetric
427+
test_quant(-1, 1, 3) # Symmetric, uneven number of bits
428+
test_quant(-5, -2, 2) # Only negative
429+
test_quant(5, 12, 2) # Only positive
430+
test_quant(-5, 12, 20) # Large number of bits
431+
432+
433+
434+
function test_quant2(y_min, y_max, bits)
435+
u = y_min:(1/2^bits):y_max
436+
y = ModelingToolkitSampledData.quantize_midtread.(u, bits, y_min, y_max)
437+
uy = unique(y)
438+
# @test (2^bits - 2 <= length(uy) <= 2^bits) # This check is not reliable since there might be one ulp difference when the last clamp is applied
439+
@test maximum(y) <= y_max
440+
@test minimum(y) >= y_min
441+
end
442+
443+
test_quant2(-1, 1, 2) # Symmetric
444+
test_quant2(-1, 2, 2) # Not symmetric
445+
test_quant2(-1, 1, 3) # Symmetric, uneven number of bits
446+
test_quant2(-5, -2, 2) # Only negative
447+
test_quant2(5, 12, 2) # Only positive
448+
test_quant2(-5, 12, 20) # Large number of bits
449+
450+
z = ShiftIndex(Clock(0.1))
451+
@mtkmodel QuantizationModel begin
452+
@components begin
453+
input = Sine(amplitude=1, frequency=1)
454+
quant = Quantization(; z, bits=2)
455+
end
456+
@equations begin
457+
connect(input.output, quant.input)
458+
end
459+
end
460+
@named m = QuantizationModel()
461+
m = complete(m)
462+
ssys = structural_simplify(IRSystem(m))
463+
prob = ODEProblem(ssys, [], (0.0, 10.0))
464+
sol = solve(prob, Tsit5(), dtmax=0.01)
465+
y = sol[m.quant.y]
466+
uy = unique(y)
467+
@test length(uy) == 4
468+
469+
470+
@mtkmodel QuantizationModel2 begin
471+
@components begin
472+
input = Sine(amplitude=1, frequency=1)
473+
quant = Quantization(; z, bits=2, midrise=false)
474+
end
475+
@equations begin
476+
connect(input.output, quant.input)
477+
end
478+
end
479+
@named m = QuantizationModel2()
480+
m = complete(m)
481+
ssys = structural_simplify(IRSystem(m))
482+
prob = ODEProblem(ssys, [], (0.0, 10.0))
483+
sol = solve(prob, Tsit5(), dtmax=0.01)
484+
y = sol[m.quant.y]
485+
uy = unique(y)
486+
@test length(uy) == 4
487+
end
488+
489+

0 commit comments

Comments
 (0)