Skip to content

Commit 22154a5

Browse files
committed
Fix #202 (again again)
The shift was due to EmpiricalNoiseModel failing to follow the rules and return a spectrum of the configured size. Instead, it returns a size based on "best fft size" near the requested. The IncoherentAddNoise was too trusting and so effectively stretched the spectrum by the difference. IncoherentAddNoise now accepts the wrong size spectrum and truncates the generated noise waveform to the appropriate size.
1 parent 66afd44 commit 22154a5

File tree

6 files changed

+191
-49
lines changed

6 files changed

+191
-49
lines changed

gen/docs/noise.org

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,53 @@
22

33
* Overview
44

5-
WCT provides a single "noise model" which perhaps is best labeled a "sampled spectrum" model.
6-
It is rather simple but covers all known forms of noise.
5+
WCT provides a "sampled spectrum noise model" which is parametererized by two types of information:
76

8-
The model is parameterized by three pieces of information, all of which are provided by the user:
7+
- A set of "median noise spectra".
98

10-
- one or more "mean noise spectra"
9+
- An associative mapping between channel and spectrum.
1110

12-
- a mapping from channel to spectrum
11+
The WCT implementation is structured into two layers.
1312

14-
- a mapping strategy
13+
1. A "model" component provides a median noise spectrum given a key (channel or group).
1514

16-
The kernel of the model performs a sampling of a spectrum to generate a voltage-level waveform.
17-
The sampling has two steps. First, each frequency bin of the mean spectrum is fluctuated according to the Rayleigh distribution. To this fluctuated spectrum an inverse discrete Fourier transform is applied to produce a novel voltage-level waveform.
15+
2. An "adder" component generates a noise waveform and associates it to one or more channels.
1816

19-
The resulting waveform is then added to one or more channels via a mapping and tha mapping strategy.
17+
* Noise generation
2018

21-
There are two mapping strategies:
19+
The kernel of the noise generation returns a fluctuated noise waveform given a real-valued median amplitude spectrum. Median and not mean is the proper statistic to supply.
20+
The sampling of the distribution characterized by the median spectrum has two steps.
2221

23-
- the "incoherent" strategy adds a single waveform to a single channel via the mapping. A channel is mapped directly to a spectrum from which the waveform was generated.
22+
1. Each frequency bin of the median spectrum is fluctuated to produce a complex amplitude sample.
2423

25-
- the "coherent" strategy adds a single waveform to each in a group of channels. A channel is mapped to a "group" and the "group" is mapped to a spectrum.
24+
2. An inverse discrete Fourier transform is applied to the spectrum of samples to produce the noise waveform.
2625

27-
There is a WCT graph component that implements each strategy: ~IncoherentAddNoise~ and ~CoherentAddNoise~, respectively.[fn:old]
26+
The fluctuation at each frequency is produced by twice sampling a Gaussian distribution of mean zero and sigma equal to the median. These samples form the real and imaginary part of a complex valued spectral sample. Equivalently, such samples have Rayleigh-distributed complex amplitude and uniform-distributed complex phase. This sampling motivates supplying a median and not a mean spectrum.
2827

29-
A WCT graph may, of course, apply multiple instances of either or both components. This allows both types of noises to be added and, for example, interleaving of different types of coherent noise.
28+
* Noise adders
3029

31-
Note, it is left to the user to provide properly normalized spectra so that when combined there is no "double counting" of noise power.
30+
There are two types of noise adders:
3231

32+
- ~IncoherentAddNoise~ :: adds an independent noise waveform to each channel. The ~AddNoise~ type is an alias for this type.
33+
- ~CoherentAddNoise~ :: adds an independent noise waveform to each in a group of channels.
34+
35+
* Noise spectrum models
36+
37+
There are two types of noise spectrum models:
38+
39+
** ~EmpiricalNoiseModel~
40+
41+
This spectral model associates an initial median spectrum and a set of post-hoc transformations of that spectrum to a channel. It expects as input a set of initial median spectra each of which are identified by a plane number and a wire length and summary information. The post-hoc transformations are the following:
42+
43+
- The initial spectrum for a given channel is formed as the interpolation between provided spectra based on their total wire lengths and that of the channel.
44+
- An additional white-noise component may be specified for each input spectrum, this too is subject to wire-length interpolation.
45+
- A pair of gain and shaping time values may be associated with an input spectrum and a different pair with each channel and the spectrum will be transformed accordingly.
46+
47+
The ~EmpiricalNoiseModel~ spectrum model may only be used with the ~IncoherentAddNoise~ noise adder.
48+
49+
** ~GroupNoiseModel~
50+
51+
This spectral model associates a channel to a group and a group to a spectrum. No post-hoc wire-length interpolations nor any transformations are performed on the input spectra.
52+
53+
This spectrum model may be used with ~IncoherentAddNoise~ and ~CoherentAddNoise~
3354

34-
[fn:old] An older ~AddNoise~ was used. This component type is aliased to ~IncoherentAddNoise~.

gen/src/AddNoise.cxx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ bool Gen::IncoherentAddNoise::operator()(const input_pointer& inframe, output_po
5959

6060
// Limit number of warnings below
6161
static bool warned = false;
62+
static bool warned2 = false;
6263

6364
// Make waveforms of size nsample from each model, adding only
6465
// ncharge of their element to the trace charge. This
@@ -76,27 +77,37 @@ bool Gen::IncoherentAddNoise::operator()(const input_pointer& inframe, output_po
7677

7778
for (auto& [mtn, model] : m_models) {
7879
const auto& spec = model->channel_spectrum(chid);
80+
const size_t nspec = spec.size();
7981

80-
if (spec.empty()) {
82+
if (! nspec) {
8183
continue; // channel not in model
8284
}
85+
8386
// The model spec size may differ than expected nsamples.
8487
// We could interpolate to correct for that which would
8588
// slow things down. Better to correct the model(s) code
8689
// and configuration.
87-
if (not warned and spec.size() != m_nsamples) {
90+
if (not warned and nspec != m_nsamples) {
8891
log->warn("model {} produced {} samples instead of expected {}, future warnings muted",
89-
mtn, spec.size(), m_nsamples);
92+
mtn, nspec, m_nsamples);
9093
warned = true;
9194
}
9295

93-
real_vector_t sigmas(m_nsamples);
94-
for (size_t ind=0; ind<m_nsamples; ++ind) {
96+
const size_t nsigmas = nspec;
97+
// const nsigmas = m_nsamples;
98+
real_vector_t sigmas(nsigmas);
99+
for (size_t ind=0; ind < nsigmas; ++ind) {
95100
sigmas[ind] = spec[ind]*sqrt2opi;
96101
}
102+
97103
auto wave = rwgen.wave(sigmas);
104+
if (not warned2 and wave.size() < ncharge) {
105+
log->warn("undersized noise {} for input waveform {}, future warnings muted", wave.size(), ncharge);
106+
warned2 = true;
107+
}
98108
wave.resize(ncharge);
99109
Waveform::increase(charge, wave);
110+
100111
}
101112

102113
auto trace = make_shared<SimpleTrace>(chid, intrace->tbin(), charge);

gen/src/GroupNoiseModel.cxx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,18 @@ void Gen::GroupNoiseModel::configure(const WireCell::Configuration& cfg)
9696

9797
// Read in, resample and intern the spectra.
9898
m_grp2amp.clear();
99+
int count=0;
99100
for (const auto& jso : jspectra) {
100-
auto jgroup = jso["group"].isNull() ? jso["groupID"] : jso["group"];
101-
const int group = jgroup.asInt();
101+
// Some noise files lack explicit group ID so simply count groups.
102+
int group = count;
103+
auto jgroup = jso["group"];
104+
if (jgroup.isNull()) {
105+
jgroup = jso["groupID"];
106+
}
107+
if (jgroup.isInt()) {
108+
group = jgroup.asInt();
109+
}
110+
++count;
102111

103112
// The original waveform size and sampling period
104113
const size_t norig = jso["nsamples"].asInt();
@@ -107,11 +116,13 @@ void Gen::GroupNoiseModel::configure(const WireCell::Configuration& cfg)
107116
++errors;
108117
continue;
109118
}
110-
const double porig = jso["period"].asInt();
119+
const double porig = jso["period"].asDouble();
111120

112121
const auto jfreqs = jso["freqs"];
113122
const auto jamps = jso["amps"];
114123
const size_t npts = jfreqs.size();
124+
log->debug("loaded group:{} nsamples:{} period:{} nfreqs:{} namps:{}",
125+
group, norig, porig, jfreqs.size(), jamps.size());
115126

116127
const double Fnyquist = 1.0/(2*porig);
117128
const double Frayleigh = 1.0/(norig*porig);

gen/test/test-addnoise.bats

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,85 @@
11

2-
# The intention is to run this test in multiple releases and compare across releases.
3-
@test "generate simple noise for comparison with older releases" {
2+
file_base () {
3+
local noise="$1" ; shift
4+
local nsamples="${1:-6000}"
45

56
local ver="$(wire-cell --version)"
6-
local outfile="test-addnoise-${ver}.tar.bz2"
7+
local base="$(basename ${BATS_TEST_FILENAME} .bats)"
8+
echo "test-addnoise-${ver}-${noise}-${nsamples}"
9+
}
10+
11+
make_noise () {
12+
local noise="$1" ; shift
13+
local nsamples="${1:-6000}"
14+
15+
local base="$(file_base $noise $nsamples)"
716

8-
run wire-cell -l test-addnoise.log -L debug \
9-
-A outfile="$outfile" -c gen/test/test-addnoise.jsonnet
17+
local cfgfile="${BATS_TEST_FILENAME%.bats}.jsonnet"
1018

19+
local outfile="${base}.tar.gz"
20+
local logfile="${base}.log"
21+
22+
rm -f "$logfile" # appends otherwise
23+
if [ -s "$outfile" ] ; then
24+
echo "already have $outfile" 1>&3
25+
return
26+
fi
27+
run wire-cell -l "$logfile" -L debug \
28+
-A nsamples=$nsamples -A noise=$noise -A outfile="$outfile" \
29+
-c "$cfgfile"
1130
echo "$output"
1231
[[ "$status" -eq 0 ]]
1332
[[ -s "$outfile" ]]
1433
}
1534

1635

36+
# The intention is to run this test in multiple releases and compare across releases.
37+
@test "generate simple noise for comparison with older releases" {
38+
39+
40+
41+
for nsamples in 6000
42+
do
43+
for noise in empno
44+
do
45+
make_noise $noise $nsamples
46+
local base="$(file_base $noise $nsamples)"
47+
48+
wirecell-plot comp1d -o comp1d-addnoise-${ver}-${noise}-${nsamples}.pdf \
49+
--markers 'o + x .' -t '*' -n spec \
50+
--chmin 0 --chmax 800 -s --transform ac \
51+
"${base}.tar.gz"
52+
done
53+
done
54+
# wirecell-plot comp1d -o comp1d-addnoise-${ver}-all-all.pdf \
55+
# --markers 'o + x .' -t '*' -n spec \
56+
# --chmin 0 --chmax 800 -s --transform ac \
57+
# "test-addnoise-${ver}-empno-4096.tar.gz" \
58+
# "test-addnoise-${ver}-inco-4096.tar.gz" \
59+
# "test-addnoise-${ver}-empno-6000.tar.gz" \
60+
# "test-addnoise-${ver}-inco-6000.tar.gz"
61+
62+
}
63+
64+
1765

66+
@test "inco and empno with external spectra file is identical" {
67+
68+
for nsamples in 4095 4096 6000
69+
do
70+
for noise in empno inco
71+
do
72+
make_noise $noise $nsamples
73+
done
74+
75+
wirecell-plot comp1d \
76+
-o "$(file_base inco+empno $nsamples)-comp1d.pdf" \
77+
--markers 'o .' -t '*' -n spec \
78+
--chmin 0 --chmax 800 -s --transform ac \
79+
"$(file_base inco $nsamples).tar.gz" \
80+
"$(file_base empno $nsamples).tar.gz"
81+
82+
done
83+
84+
}
1885

gen/test/test-addnoise.jsonnet

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
local wc = import "wirecell.jsonnet";
44
local pg = import "pgraph.jsonnet";
55

6-
local nsamples_generate = 4096;
76
local tick = 0.5*wc.us;
87

98
// services
@@ -42,36 +41,53 @@ local absurd = pg.pnode({
4241
nchannels: 2560,
4342
}}, nin=0, nout=1);
4443

45-
local reframer = pg.pnode({
44+
local reframer(nsamples) = pg.pnode({
4645
type: 'Reframer',
4746
data: {
48-
nticks: nsamples_generate,
47+
nticks: nsamples,
4948
anode: wc.tn(anode),
5049
}}, nin=1, nout=1, uses=[anode]);
5150

52-
local empno = {
53-
type: "EmpiricalNoiseModel",
54-
name: "empno",
55-
data: {
56-
anode: wc.tn(anode),
57-
chanstat: "",
58-
spectra_file: "protodune-noise-spectra-v1.json.bz2",
59-
nsamples: nsamples_generate,
60-
period: tick,
61-
wire_length_scale: 1*wc.cm,
62-
}, uses: [anode]
51+
local pdsp_noise_file = "protodune-noise-spectra-v1.json.bz2";
52+
53+
local noise_models(nsamples) = {
54+
55+
empno: {
56+
type: "EmpiricalNoiseModel",
57+
name: "empno",
58+
data: {
59+
anode: wc.tn(anode),
60+
chanstat: "",
61+
spectra_file: pdsp_noise_file,
62+
nsamples: nsamples,
63+
period: tick,
64+
wire_length_scale: 1*wc.cm,
65+
}, uses: [anode]
66+
},
67+
inco: {
68+
type: "GroupNoiseModel",
69+
name: "inco",
70+
data: {
71+
// This can also be given as a JSON/Jsonnet file
72+
spectra: pdsp_noise_file,
73+
groups: import "test-noise-groups-pdsp.jsonnet",
74+
nsamples: nsamples,
75+
tick: tick,
76+
}
77+
}
6378
};
6479

65-
local addnoise =
80+
81+
local addnoise(model, nsamples) =
6682
pg.pnode({
6783
type: 'AddNoise',
6884
name: "",
6985
data: {
7086
dft: wc.tn(svcs.dft),
7187
rng: wc.tn(svcs.rng),
72-
model: wc.tn(empno),
73-
nsamples: nsamples_generate,
74-
}}, nin=1, nout=1, uses=[empno, svcs.dft, svcs.rng]);
88+
model: wc.tn(model),
89+
nsamples: nsamples,
90+
}}, nin=1, nout=1, uses=[model, svcs.dft, svcs.rng]);
7591

7692
local digi = pg.pnode({
7793
type: "Digitizer",
@@ -86,7 +102,10 @@ local digi = pg.pnode({
86102
}
87103
}, nin=1, nout=1, uses=[anode]);
88104

89-
function(outfile="test-addnoise.tar.bz2")
105+
function(outfile="test-addnoise.tar.bz2", noise="empno", nsamples=4096)
106+
local int_nsamples = if std.type(nsamples) == "number" then nsamples else std.parseInt(nsamples);
107+
108+
local nmodel = noise_models(int_nsamples)[noise];
90109

91110
local sink = pg.pnode({
92111
type: "FrameFileSink",
@@ -96,7 +115,7 @@ function(outfile="test-addnoise.tar.bz2")
96115
digitize: true,
97116
},
98117
}, nin=1, nout=0);
99-
local graph = pg.pipeline([absurd, reframer, addnoise, digi, sink]);
118+
local graph = pg.pipeline([absurd, reframer(int_nsamples), addnoise(nmodel, int_nsamples), digi, sink]);
100119
local app = {
101120
type: 'Pgrapher',
102121
data: {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// coherent group noise for PDSP
2+
local one(gid, start, num) = {
3+
plane: "p" + std.toString(gid),
4+
groupID: gid,
5+
channels: std.range(start, start+num-1),
6+
};
7+
[
8+
one(1, 0, 400),
9+
one(2, 400, 400),
10+
one(3, 800, 400),
11+
one(4, 1200, 400),
12+
one(5, 1600, 480),
13+
one(6, 2080, 480),
14+
]

0 commit comments

Comments
 (0)