summaryrefslogtreecommitdiff
path: root/src/photon.h
blob: f4718665d293ca6e1e997a3be7ed4dd6eced3352 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#ifndef __PHOTON_H__
#define __PHOTON_H__

#include "linalg.h"
#include "materials.h"
#include "rotate.h"
#include "random.h"
#include "physical_constants.h"
#include "mesh.h"

struct Photon
{
	float3 position;
	float3 direction;
	float3 polarization;
	float wavelength;
	float time;

	unsigned int history;

	int last_hit_triangle;
};

struct State
{
	bool inside_to_outside;

	float3 surface_normal;

	float refractive_index1, refractive_index2;
	float absorption_length;
	float scattering_length;

	int surface_index;

	float distance_to_boundary;
};

enum
{
	NO_HIT           = 0x1 << 0,
	BULK_ABSORB      = 0x1 << 1,
	SURFACE_DETECT   = 0x1 << 2,
	SURFACE_ABSORB   = 0x1 << 3,
	RAYLEIGH_SCATTER = 0x1 << 4,
	REFLECT_DIFFUSE  = 0x1 << 5,
	REFLECT_SPECULAR = 0x1 << 6
}; // processes

enum {BREAK, CONTINUE, PASS}; // return value from propagate_to_boundary

__device__ void fill_state(State &s, Photon &p)
{
	p.last_hit_triangle = intersect_mesh(p.position, p.direction, s.distance_to_boundary, p.last_hit_triangle);

	if (p.last_hit_triangle == -1)
	{
		p.history |= NO_HIT;
		return;
	}

	uint4 triangle_data = g_triangles[p.last_hit_triangle];

	float3 v0 = g_vertices[triangle_data.x];
	float3 v1 = g_vertices[triangle_data.y];
	float3 v2 = g_vertices[triangle_data.z];

	int inner_material_index = convert(0xFF & (triangle_data.w >> 24));
	int outer_material_index = convert(0xFF & (triangle_data.w >> 16));
	s.surface_index = convert(0xFF & (triangle_data.w >> 8));

	s.surface_normal = cross(v1-v0, v2-v1);
	s.surface_normal /= norm(s.surface_normal);

	Material material1, material2;
	if (dot(s.surface_normal,-p.direction) > 0.0f)
	{
		// outside to inside
		material1 = materials[outer_material_index];
		material2 = materials[inner_material_index];

		s.inside_to_outside = false;
	}
	else
	{
		// inside to outside
		material1 = materials[inner_material_index];
		material2 = materials[outer_material_index];
		s.surface_normal = -s.surface_normal;

		s.inside_to_outside = true;
	}

	s.refractive_index1 = interp_property(p.wavelength, material1.refractive_index);
	s.refractive_index2 = interp_property(p.wavelength, material2.refractive_index);
	s.absorption_length = interp_property(p.wavelength, material1.absorption_length);
	s.scattering_length = interp_property(p.wavelength, material1.scattering_length);

} // fill_state

__device__ void rayleigh_scatter(Photon &p, curandState &rng)
{
	float theta, y;

	while (true)
	{
		y = curand_uniform(&rng);
		theta = uniform(&rng, 0, 2*PI);

		if (y < powf(cosf(theta),2))
			break;
	}

	float phi = uniform(&rng, 0, 2*PI);

	float3 b = cross(p.polarization, p.direction);
	float3 c = p.polarization;

	p.direction = rotate(p.direction, theta, b);
	p.direction = rotate(p.direction, phi, c);

	p.polarization = rotate(p.polarization, theta, b);
	p.polarization = rotate(p.polarization, phi, c);

} // scatter

__device__ int propagate_to_boundary(Photon &p, State &s, curandState &rng)
{
	float absorption_distance = -s.absorption_length*logf(curand_uniform(&rng));
	float scattering_distance = -s.scattering_length*logf(curand_uniform(&rng));

	if (absorption_distance <= scattering_distance)
	{
		if (absorption_distance <= s.distance_to_boundary)
		{
			p.time += absorption_distance/(SPEED_OF_LIGHT/s.refractive_index1);
			p.position += absorption_distance*p.direction;
			p.history |= BULK_ABSORB;

			p.last_hit_triangle = -1;

			return BREAK;
		} // photon is absorbed in material1
	}
	else
	{
		if (scattering_distance <= s.distance_to_boundary)
		{
			p.time += scattering_distance/(SPEED_OF_LIGHT/s.refractive_index1);
			p.position += scattering_distance*p.direction;

			rayleigh_scatter(p, rng);

			p.history |= RAYLEIGH_SCATTER;

			p.last_hit_triangle = -1;

			return CONTINUE;
		} // photon is scattered in material1
	} // if scattering_distance < absorption_distance

	p.position += s.distance_to_boundary*p.direction;
	p.time += s.distance_to_boundary/(SPEED_OF_LIGHT/s.refractive_index1);

	return PASS;

} // propagate_to_boundary

__device__ void propagate_at_boundary(Photon &p, State &s, curandState &rng)
{
	float incident_angle = acosf(dot(s.surface_normal, -p.direction));
	float refracted_angle = asinf(sinf(incident_angle)*s.refractive_index1/s.refractive_index2);

	float3 incident_plane_normal = cross(p.direction, s.surface_normal);
	incident_plane_normal /= norm(incident_plane_normal);

	float normal_coefficient = dot(p.polarization, incident_plane_normal);
	float normal_probability = normal_coefficient*normal_coefficient;

	float reflection_coefficient;
	if (curand_uniform(&rng) < normal_probability)
	{
		reflection_coefficient = -sinf(incident_angle-refracted_angle)/sinf(incident_angle+refracted_angle);

		if ((curand_uniform(&rng) < reflection_coefficient*reflection_coefficient) || isnan(refracted_angle))
		{
			p.direction = rotate(s.surface_normal, incident_angle, incident_plane_normal);
			
			p.history |= REFLECT_SPECULAR;
		}
		else
		{
			p.direction = rotate(s.surface_normal, PI-refracted_angle, incident_plane_normal);
		}

		p.polarization = incident_plane_normal;
	} // photon polarization normal to plane of incidence
	else
	{
		reflection_coefficient = tanf(incident_angle-refracted_angle)/tanf(incident_angle+refracted_angle);

		if ((curand_uniform(&rng) < reflection_coefficient*reflection_coefficient) || isnan(refracted_angle))
		{
			p.direction = rotate(s.surface_normal, incident_angle, incident_plane_normal);
			
			p.history |= REFLECT_SPECULAR;
		}
		else
		{
			p.direction = rotate(s.surface_normal, PI-refracted_angle, incident_plane_normal);
		}

		p.polarization = cross(incident_plane_normal, p.direction);
		p.polarization /= norm(p.polarization);
	} // photon polarization parallel to plane of incidence

} // propagate_at_boundary

__device__ int propagate_at_surface(Photon &p, State &s, curandState &rng)
{
	Surface surface = surfaces[s.surface_index];

	float detect = interp_property(p.wavelength, surface.detect);
	float absorb = interp_property(p.wavelength, surface.absorb);
	float reflect_diffuse = interp_property(p.wavelength, surface.reflect_diffuse);
	float reflect_specular = interp_property(p.wavelength, surface.reflect_specular);

	// since the surface properties are interpolated linearly, we are
	// guaranteed that they still sum to 1.0.

	float uniform_sample = curand_uniform(&rng);

	if (uniform_sample < absorb)
	{
		p.history |= SURFACE_ABSORB;
		return BREAK;
	}
	else if (uniform_sample < absorb + detect)
	{
		p.history |= SURFACE_DETECT;
		return BREAK;
	}
	else if (uniform_sample < absorb + detect + reflect_diffuse)
	{
		// diffusely reflect
		p.direction = uniform_sphere(&rng);

		if (dot(p.direction, s.surface_normal) < 0.0f)
			p.direction = -p.direction;

		// randomize polarization?
		p.polarization = cross(uniform_sphere(&rng), p.direction);
		p.polarization /= norm(p.polarization);

		p.history |= REFLECT_DIFFUSE;

		return CONTINUE;
	}
	else
	{
		// specularly reflect
		float incident_angle = acosf(dot(s.surface_normal, -p.direction));
		float3 incident_plane_normal = cross(p.direction, s.surface_normal);
		incident_plane_normal /= norm(incident_plane_normal);

		p.direction = rotate(s.surface_normal, incident_angle, incident_plane_normal);

		p.history |= REFLECT_SPECULAR;

		return CONTINUE;
	}

} // propagate_at_surface

#endif
s="w"> size_t nhit; nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (ev->pmt_hits[i].hit && (pmts[i].pmt_type == PMT_OWL || pmts[i].pmt_type == PMT_BUTT)) nhit += 1; } return nhit >= 3; } /* Returns 1 if the event fails the ESUM cut. The definition for the ESUM cut * comes from the SNOMAN companion: * * Tags an event if the ESUMLO AND ESUMHI trigger bits are set, AND NONE of * the following trigger bits are set: (NHIT100LO, NHIT100MED, * NHIT100HI,NHIT20, NHIT20LB, OWLN, OWLELO, OWLEHI, PULSE_GT, PRESCALE, * MISS). * */ int is_esum(event *ev) { return (ev->trigger_type & (TRIG_ESUM_LO | TRIG_ESUM_HI | TRIG_NHIT_100_LO | TRIG_NHIT_100_MED | TRIG_NHIT_100_HI | TRIG_NHIT_20 | TRIG_NHIT_20_LB | TRIG_OWLN | TRIG_OWLE_LO | TRIG_OWLE_HI | TRIG_PULSE_GT | TRIG_PRESCALE | TRIG_MISS)) == (TRIG_ESUM_LO | TRIG_ESUM_HI); } /* Returns the data cleaning bitmask for the event `ev`. */ uint32_t get_dc_word(event *ev, zebraFile *f, zebraBank *bmast, zebraBank *bev) { uint32_t status = 0x0; if (is_muon(ev)) status |= DC_MUON; if (junk_cut(f, bmast, bev)) status |= DC_JUNK; if (crate_isotropy(ev)) status |= DC_CRATE_ISOTROPY; if (qvnhit(ev)) status |= DC_QVNHIT; if (is_neck_event(ev)) status |= DC_NECK; if (is_flasher(ev)) status |= DC_FLASHER; if (is_esum(ev)) status |= DC_ESUM; if (is_owl(ev)) status |= DC_OWL; if (is_owl_trigger(ev)) status |= DC_OWL_TRIGGER; if (is_fts(ev)) status |= DC_FTS; if (is_itc(ev)) status |= DC_ITC; if (is_breakdown(ev)) status |= DC_BREAKDOWN; return status; } /* Returns 1 if the event is tagged as an incoming muon. The goal of this cut * is to tag external muons *without* tagging muons which start within the PSUP * and then exit. In order to achieve that we use the ECA time of the OWL * tubes. * * Ideally we would have the full calibrated time of the OWL tubes, but there * was never any PCA for the OWL tubes. I also discussed this with Josh and he * said that the cables were never changed for the OWL tubes so we should at * least be able to rely on them being consistent. * * Since the average PCA correction is of order +/- 5 ns and the round trip * time for light from the edge of the PSUP to the cavity and back should be * longer than 15 ns (4 meters), the ECA time should be enough to distinguish * incoming vs outgoing muons. * * This function tags an event as an incoming muon if the following criteria * are met: * * - calibrated nhit >= 150 * - at least 5 calibrated OWL hits * - at least 1 OWL hit which has a high charge and is early relative to nearby * normal PMTs * */ int is_muon(event *ev) { size_t i, j; double t_nearby[MAX_PMTS], q_nearby[MAX_PMTS]; int n_nearby; size_t nhit_owl, nhit_owl_early; double pmt_dir[3]; double distance; double t_median, q_median; /* Require at least 150 normal hits. */ if (ev->nhit < MUON_MIN_NHIT) return 0; nhit_owl = 0; nhit_owl_early = 0; for (i = 0; i < MAX_PMTS; i++) { /* Require good calibrations. * * Note: Typically I would do this with the following check: * * ev->pmt_hits[i].pf & (KPF_NO_CAL | KPF_BAD_CAL) * * However, Javi mentioned in an email that for very early hits which * are in the TAC curl region, the calibration processor will flag * them, set the time to -9999.0, and set the KPF_BAD_CAL bit. However, * in this case for some reason the ECA time still looks reasonable. * Therefore, since we only use the ECA time here we instead just make * sure that the ECA time is something reasonable. */ if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_OWL || ev->pmt_hits[i].ept <= -100 || ev->pmt_hits[i].best_uncal_q < -100) continue; n_nearby = 0; for (j = 0; j < MAX_PMTS; j++) { if (!ev->pmt_hits[j].hit || pmts[j].pmt_type != PMT_NORMAL || ev->pmt_hits[j].ept <= -100) continue; SUB(pmt_dir,pmts[i].pos,pmts[j].pos); distance = NORM(pmt_dir); if (distance < MUON_OWL_NEARBY_DISTANCE) { t_nearby[n_nearby] = ev->pmt_hits[j].ept; q_nearby[n_nearby] = ev->pmt_hits[j].best_uncal_q; n_nearby += 1; } } /* Not sure what to do if there are no nearby PMTs. */ if (n_nearby == 0) continue; gsl_sort(t_nearby,1,n_nearby); gsl_sort(q_nearby,1,n_nearby); t_median = gsl_stats_median_from_sorted_data(t_nearby,1,n_nearby); q_median = gsl_stats_median_from_sorted_data(q_nearby,1,n_nearby); if (ev->pmt_hits[i].best_uncal_q/q_median > 1.0 && ev->pmt_hits[i].ept < t_median) nhit_owl_early += 1; nhit_owl += 1; } /* Require at least 5 OWL hits. */ if (nhit_owl < MUON_MIN_OWL_NHIT) return 0; return nhit_owl_early >= 1; } /* Returns 1 if the event fails the junk cut. The definition for the junk cut * comes from the SNOMAN companion: * * This will remove orphans, ECA data and events containing the same PMT * more than once. * * I also added a check that will flag the event if it contains more than * MAX_PMTS PMT hits (which shouldn't be possible). * * Note: This function may return -1 if there is an error traversing the PMT * banks. */ int junk_cut(zebraFile *f, zebraBank *bmast, zebraBank *bev) { int i; int ids[MAX_PMTS]; int nhit; static int pmt_links[] = {KEV_PMT,KEV_OWL,KEV_LG,KEV_FECD,KEV_BUTT,KEV_NECK}; static char *pmt_names[] = {"PMT","OWL","LG","FECD","BUTT","NECK"}; size_t index[MAX_PMTS]; PMTBank bpmt; zebraBank b; int rv; /* Fail orphans. */ if (bev->status & KEV_ORPHAN) return 1; /* Fail any events with the ECA bit set. */ if (bmast->status & KMAST_ECA) return 1; /* Next we check to see if any PMTs got hit more than once. To do this we * construct an array of the PMT PIN numbers, sort it, and then check for * duplicates. */ nhit = 0; for (i = 0; i < LEN(pmt_links); i++) { if (bev->links[pmt_links[i]-1] == 0) continue; rv = zebra_get_bank(f,&b,bev->links[pmt_links[i]-1]); if (rv) { fprintf(stderr, "error getting %s bank: %s\n", pmt_names[i], zebra_err); return -1; } while (1) { unpack_pmt(b.data, &bpmt); ids[nhit++] = bpmt.pin; if (nhit >= MAX_PMTS) { fprintf(stderr, "event with more than %i PMT hits\n", MAX_PMTS); return 1; } if (!b.next) break; rv = zebra_get_bank(f,&b,b.next); if (rv) { fprintf(stderr, "error getting %s bank: %s\n", pmt_names[i], zebra_err); return -1; } } } argsort(ids,nhit,index); for (i = 0; i < nhit - 1; i++) { /* Check if we have duplicate PMT hits. */ if (ids[i] == ids[i+1]) return 1; } return 0; } /* Returns 1 if the event fails the crate isotropy test. The definition of * this cut comes from the SNOMAN companion: * * This tags events where more than 70% of the hits occur on one crate and * more than 80% of those hits occur on two adjacent cards, including a * wrap around to count cards 0 and 15 together. * */ int crate_isotropy(event *ev) { size_t i; int crate_nhit[20]; int card_nhit[16]; int nhit; int crate, card; int max_crate; for (i = 0; i < LEN(crate_nhit); i++) crate_nhit[i] = 0; /* Loop over all the hits and add one to the crate_nhit array for each hit * in a crate. */ nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit) continue; crate = i/512; crate_nhit[crate] += 1; nhit += 1; } /* Check if any crate has more than 70% of the hits. */ max_crate = -1; for (i = 0; i < LEN(crate_nhit); i++) { if (crate_nhit[i] > 7*nhit/10) max_crate = i; } /* If no crates had more than 70% of the hits, return 0. */ if (max_crate == -1) return 0; for (i = 0; i < LEN(card_nhit); i++) card_nhit[i] = 0; /* Now we count up the number of hits in adjacent cards. We store the * number of hits in the card_nhit array as follows: Hits from card 15 or * card 0 will be added to card_nhit[0], hits from cards 0 or 1 will go * into card_nhit[1], etc. */ nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit) continue; crate = i/512; if (crate != max_crate) continue; card = (i % 512)/32; card_nhit[card] += 1; card_nhit[(card + 1) % LEN(card_nhit)] += 1; nhit += 1; } /* Check to see if 80% of the hits in this crate came from two adjacent * cards. */ for (i = 0; i < LEN(card_nhit); i++) { if (card_nhit[i] > 8*nhit/10) return 1; } return 0; } /* Returns 1 if the event is tagged by the QvNHIT cut. The definition of * this cut comes from the SNOMAN companion: * * Using pedestal subtracted charge and a hardwired gain of 32.3 first * throw away the 10% of the tubes that have the largest charge and then * divide this by 0.9* NHIT. Tag the event if this ratio is below 0.25. * * I copied the logic from the SNOMAN file flt_q_nhit_cut.for. * * Update: I changed the logic from the SNOMAN code to not require good * calibrations. The reason is that for electrical pickup events, the charges * on most of the hits can be very small or negative and so we wouldn't tag * them with this cut. */ int qvnhit(event *ev) { size_t i; double ehl[MAX_PMTS]; int nhit, max; double sum, qhl_ratio; nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_NORMAL) continue; /* Remove unphysical charge hits. */ if (ev->pmt_hits[i].ehl <= -100) continue; /* FIXME: SNOMAN code checks for pmt terminator. I don't know where * that info is stored. */ /* Note: We use QHL instead of QHS here because that's how SNOMAN did * it. In the SNOMAN code there is a comment which says: * * we use QHL since it is a better measure of pickup (long integrate) * */ ehl[nhit++] = ev->pmt_hits[i].ehl/32.3; } gsl_sort(ehl,1,nhit); max = nhit*9/10; /* Check that we have enough calibrated tubes. */ if (max < 5) return 0; sum = 0.0; for (i = 0; i < max; i++) { sum += ehl[i]; } qhl_ratio = sum/max; return qhl_ratio < QRATIO_THRESHOLD; } /* Returns the time difference between the neck PMT hit and the average ECA * time of the PSUP PMTs required to tag an event as a neck event. * * These values come from the neck tube cut table in prod/filter_flth.dat. */ double get_neck_tube_cut_time_diff(event *ev) { if (ev->dte < 19991216 || (ev->dte == 19991216 && ev->hmsc < 20370000)) return 70.0; else if (ev->dte < 20040923 || (ev->dte < 20040923 && ev->hmsc < 100222)) return 15.0; else return -85.0; } /* Returns 1 if the event is classified as a neck event. The definition of * a neck event is a combination of the neck flag in the DAMN word: * * This cuts events containing neck tubes. It requires that either both * tubes in the neck fire, or that one of those tubes fires and it has a * high charge and is early. High charge is defined by a neck tube having a * pedestal subtracted charge greater than 70 or less than -110. Early if * defined by the neck tube having an ECA time 70ns or more before the * average ECA time of the PSUP PMTS with z les than 0. After the cable * changes to the neck tubes this time difference changes to 15ns. * * and the added requirement that 50% of the normal PMTs have z <= -425.0 or * 50% of the ECA calibrated charge for normal PMTs have z <= -425.0. * * I added the requirement that 50% of the normal PMTs be near the bottom since * for the high energy events I'm interested in, it's entirely possible that a * high energy electron or muon travelling upwards could cause multiple neck * PMTs to fire. * */ int is_neck_event(event *ev) { size_t i; int n; int high_charge; double avg_ept_psup; int nhit, nhit_bottom; double time_diff; double ehs, ehs_bottom; /* First, we compute the average ECA time for all the PSUP PMTs with z <= 0. */ nhit = 0; avg_ept_psup = 0.0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || ev->pmt_hits[i].ept <= -100) continue; /* Require good calibrations. */ if (ev->pmt_hits[i].pf & (KPF_NO_CAL | KPF_BAD_CAL)) continue; if (ev->pmt_hits[i].hit && pmts[i].pos[2] < 0) { avg_ept_psup += ev->pmt_hits[i].ept; nhit += 1; } } if (nhit < 1) return 0; avg_ept_psup /= nhit; time_diff = get_neck_tube_cut_time_diff(ev); /* Now, we check if two or more neck tubes fired *or* a single neck tube * fired with a high charge and 70 ns before the average time of the normal * PMTs. */ n = 0; high_charge = 0; nhit = 0; nhit_bottom = 0; ehs = 0; ehs_bottom = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit) continue; switch (pmts[i].pmt_type) { case PMT_NECK: n += 1; /* Require good calibrations. */ if (ev->pmt_hits[i].ept <= -100) break; /* FIXME: The SNOMAN companion says "Early if defined by the neck * tube having an ECA time 70ns or more before the average ECA time * of the PSUP PMTS with z les than 0." When does this change take * place? */ if ((ev->pmt_hits[i].ehs > 70 || ev->pmt_hits[i].ehs < -110) && (ev->pmt_hits[i].ept < avg_ept_psup - time_diff)) high_charge = 1; break; case PMT_NORMAL: if (pmts[i].pos[2] < NECK_BOTTOM_HEIGHT) { nhit_bottom += 1; ehs_bottom += ev->pmt_hits[i].ehs; } nhit += 1; ehs += ev->pmt_hits[i].ehs; break; } } if ((n >= 2 || high_charge) && ((nhit_bottom >= nhit*NECK_BOTTOM_FRACTION) || (ehs_bottom >= ehs*NECK_BOTTOM_FRACTION))) return 1; return 0; } /* Returns 1 if the event is classified as a breakdown event. The definition * for a breakdown event is: * * - nhit > 1000 * * - a single crate has at least 256 hits and the median TAC for that crate * is more than 500 from the median TAC for all the other crates with at * least 20 hits * */ int is_breakdown(event *ev) { int i; double tac[20][512]; int nhit[20] = {0}; int nhit_cal[20] = {0}; int nhit_cal_sum; double median_tac[20]; size_t index[20]; /* Breakdown event must have an nhit greater than 1000. */ if (ev->nhit < 1000) return 0; nhit_cal_sum = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_NORMAL) continue; nhit[i/512] += 1; /* Sometimes during a breakdown, a lot of the channels have TAC values * that are outside of the normal window. I'm not sure exactly what * causes this, but we don't want to include these when calculating the * median. */ if (ev->pmt_hits[i].tac < 400) continue; tac[i/512][nhit_cal[i/512]++] = ev->pmt_hits[i].tac; nhit_cal_sum += 1; } /* Tag any event in which less than 70% of the PMT hit times are calibrated * correctly. */ if (nhit_cal_sum < ev->nhit*BREAKDOWN_CAL_FRAC) return 1; for (i = 0; i < 19; i++) { if (nhit_cal[i] <= MIN_NHIT_CRATE) { median_tac[i] = 0.0; continue; } gsl_sort(&tac[i][0],1,nhit_cal[i]); median_tac[i] = gsl_stats_median_from_sorted_data(&tac[i][0],1,nhit_cal[i]); } fargsort(median_tac,19,index); return (nhit[index[18]] >= MIN_NHIT_BREAKDOWN && median_tac[index[18]] > median_tac[index[17]] + 500); } /* Returns 1 if the average time for the hits in the same card as * `flasher_pmt_id` is at least 10 ns earlier than all the hits within 4 * meters of the potential flasher or if there are more hit PMTs in the slot * than within 4 meters. * * This cut is designed to help flag flashers in the is_flasher() function for * events in which the flashing channel may be missing from the event, so there * is no high charge channel to flag it. */ int is_slot_early(event *ev, int flasher_pmt_id) { int i; double t_slot[MAX_PMTS], t_nearby[MAX_PMTS]; double t_slot_median, t_nearby_median; int n_slot, n_nearby; double flasher_pos[3]; double pmt_dir[3]; double distance; int crate, card, channel; int flasher_crate, flasher_card, flasher_channel; flasher_crate = flasher_pmt_id/512; flasher_card = (flasher_pmt_id % 512)/32; flasher_channel = flasher_pmt_id % 32; COPY(flasher_pos,pmts[flasher_pmt_id].pos); n_slot = 0; n_nearby = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_NORMAL) continue; /* Require good calibrations. * * Note: For some reason PMT hits in the flasher slot sometimes are * marked as having bad calibrations and get a time of -9999. I still * don't understand why this is, so for now we don't check the * calibration bits and instead just check to see that the ECA time is * reasonable. * * Here is what Javi said via email: * * If SNO calibrations are the same than SNO+ (and I'm pretty sure they * are, at last for this matter) there is a hit-level time validation, * i.e., if the TAC value is determined to be in the curl region of the * time slope (high TAC = very early hits), they get flagged and a * calibration is not provided (which is compatible with a flashing * channel). If this is the case, you can fall back to raw TACs and QHS * when calibration is not available. * * Another update: * * I looked into this further and there are cases when the TAC is too * high, but in these cases *both* the ECA time and the ECA+PCA time * are set to -9999.0. However, there are still cases where *only* the * ECA+PCA time is set to -9999.0 but the ECA time is OK. I sent * another email to Javi and he figured it out: * * I just recall that in SNO+ we have a similar hit-level test for PCA: * if a channel has a very low QHS, having a good time-walk calibration * is hard so we just set the time to invalid. There might have been * something similar in SNO, although I didn't find anything after a * quick look in the SNO docs. * * I looked into it and *all* of the hits I saw were indeed caused by a * very low QHS value. * * Here, we use the pt1 value which is the ECA+PCA time except without * walk correction which is what was causing the issue with the bad * regular time. * * Another update: * * When testing out the data cleaning cuts I found an event (run 20664 * GTID 573780) where an obvious flasher was missed because all the * channels in the slot had pt1 = -9999. So now we just use ept which * should be good enough. */ if (ev->pmt_hits[i].ept < -100) continue; if (i/32 == flasher_pmt_id/32) { /* This hit is in the same slot as the potential flasher. */ t_slot[n_slot++] = ev->pmt_hits[i].ept; continue; } crate = i/512; card = (i % 512)/32; channel = i % 32; /* Skip PMTs in the same crate that are to the left or right of the * flasher PC since those can often be cross-talk hits. */ if (crate == flasher_crate && abs(card - flasher_card) == 1 && (channel/4 == flasher_channel/4)) continue; /* Calculate the distance from the current channel to the high charge * channel. */ SUB(pmt_dir,pmts[i].pos,flasher_pos); distance = NORM(pmt_dir); if (distance < 400.0) { t_nearby[n_nearby++] = ev->pmt_hits[i].ept; continue; } } if (n_slot == 0) return 0; /* If there are more PMTs in the slot than within 4 meters of the slot, * return it's likely to be a flasher. */ if (n_slot >= n_nearby) return 1; gsl_sort(t_slot,1,n_slot); t_slot_median = gsl_stats_median_from_sorted_data(t_slot,1,n_slot); gsl_sort(t_nearby,1,n_nearby); t_nearby_median = gsl_stats_median_from_sorted_data(t_nearby,1,n_nearby); return t_slot_median < t_nearby_median - 10.0; } /* Returns 1 if both: * * - 70% of the normal PMT hits are 50 ns later than the median time in t_pc * * - 70% of the normal PMT hits are at least 12 meters away from the PMT with * id flasher_pmt_id. * * This is used as the final check in the flasher cut. */ static int is_flasher_channel(event *ev, int flasher_pmt_id, double *t_pc, int nhit) { int i; size_t crate, card, channel, id; double flasher_pos[3]; double pmt_dir[3]; double distance; int nhit_late, nhit_far; double t; int flasher_pc; flasher_pc = (flasher_pmt_id/512)*64 + ((flasher_pmt_id % 512)/32)*4 + (flasher_pmt_id % 32)/8; COPY(flasher_pos,pmts[flasher_pmt_id].pos); if (NORM(flasher_pos) > 10000.0) { fprintf(stderr, "is_flasher_channel: potential flasher PMT pos = %.2f, %.2f %.2f! Skipping...\n", flasher_pos[0], flasher_pos[1], flasher_pos[2]); return 0; } /* Calculate the median time for PMT hits in the paddle card. * * Note: Initially this algorithm just used the time of the highest * charge channel. However, in looking at events in run 10,000 I * noticed two flashers which were not flagged by the cut because the * flasher channel had a normal time. Therefore, we now use the median * time for all channels in the paddle card. */ gsl_sort(t_pc,1,nhit); t = gsl_stats_median_from_sorted_data(t_pc,1,nhit); /* Check that 70% of the regular PMTs in the event which are not in the * flasher slot fired more than 50 ns after the high charge channel and are * more than 12 meters away from the high charge channel. */ nhit = 0; /* Number of PMTs which fired 50 ns after the high charge channel. */ nhit_late = 0; /* Number of PMTs which are 12 meters or more away from the high charge * channel. */ nhit_far = 0; for (i = 0; i < MAX_PMTS; i++) { /* Skip PMTs which weren't hit. */ if (!ev->pmt_hits[i].hit) continue; /* Only count regular PMTs without any flags. */ if (ev->pmt_hits[i].flags || pmts[i].pmt_type != PMT_NORMAL) continue; crate = i/512; card = (i % 512)/32; channel = i % 32; id = crate*64 + card*4 + channel/8; /* Skip PMTs in the same card as the high charge channel. */ if (id/4 == flasher_pc/4) continue; /* Require good calibrations. */ if (ev->pmt_hits[i].ept < -100) continue; nhit += 1; /* Calculate the distance from the current channel to the high charge * channel. */ SUB(pmt_dir,pmts[i].pos,flasher_pos); distance = NORM(pmt_dir); /* If this channel is 12 meters or more away, increment nhit_far. */ if (distance >= 1200.0) nhit_far += 1; /* If this channel fired more than 50 ns after the high charge channel, * increment nhit_late. */ if (ev->pmt_hits[i].ept > t + 50.0) nhit_late += 1; } /* If at least 70% of the regular PMTs fired within 50 ns and were at * least 12 meters away from the high charge channel, then it's a * flasher! */ if (nhit_late >= 7*nhit/10 && nhit_far >= 7*nhit/10) return 1; return 0; } /* Returns 1 if the event is a flasher and 0 if it isn't. This cut is based * loosely on the definition of a flasher event from * http://detector.sno.laurentian.ca/~detector/private/snoop/doc/eventIDs.html, * with a few small changes. * * Here we define a flasher as an event with the following criteria: * * - nhit >= 31 * * - Find the channel with the highest QLX charge. If this charge is more than * 80 pedestal subtracted counts higher than the next highest QLX charge, * skip the next three steps. Otherwise, continue. * * - Loop over all paddle cards with at least 4 hits. * * - Look for one channel in this paddle card which has an uncalibrated QHS or * QHL value 1000 counts above the next highest charge, or a QLX value more * than 80 counts above the next highest QLX charge. Note that QHS, QHL, and * QLX values below 300 are sent to 4095. If such a channel is found skip the * next step. * * - If no such high charge channel exists and all the QLX values in the PC are * less than 800, check to see if either there are more hits within the slot * than within 4 meters or the median ECA calibrated hit time within the slot * is earlier by at least 10 ns than the other PMTs within 4 meters. If * neither of these is true, go to the next paddle card. * * - At least 70% of the (regular) pmts in the event which are not in the * flasher slot should have fired more than 50 ns after the high charge * channel. * * - At least 70% of the (regular) pmts in the event which are not in the * flasher slot should be at a distance >= 12.0 m from the high charge pmt. * * The one criteria we *don't* use that is on the webpage is the AMB cut: * * - AMB Int >= 200 * * since I can't seem to figure out where that is stored in the data structure. */ int is_flasher(event *ev) { int hits_pc[1280] = {0}; int qhs, qhl, qlx; int qhl_pc[8] = {0}; int qhs_pc[8] = {0}; int qlx_all[MAX_PMTS] = {0}; int qlx_pc[8] = {0}; double t_pc[32] = {0}; int channel_pc[8] = {0}; size_t crate, card, channel, id; int i, j; size_t index[MAX_PMTS], index_qhs[8], index_qhl[8], index_qlx[8]; int nhit; size_t id_all[MAX_PMTS]; /* Flasher event must have an nhit greater than 1000. */ if (ev->nhit < 31) return 0; nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_NORMAL) continue; crate = i/512; card = (i % 512)/32; channel = i % 32; id = crate*64 + card*4 + channel/8; qlx = ev->pmt_hits[i].qilx; if (qlx < 300) { /* QLX values below 300 are set to 4095. */ qlx_all[nhit] = 4095; } else { qlx_all[nhit] = qlx; } id_all[nhit] = i; nhit += 1; hits_pc[id] += 1; } argsort(qlx_all, nhit, index); id = id_all[index[nhit-1]]; /* If the highest charge PMT in QLX is 80 pedestal subtracted counts higher * than the next highest QLX charge, check for a flasher. To do this, we * first check that there are at least 4 hits in the slot and then call * is_flasher_channel() with the times from all the hits in the slot. * * This check here was motivated by run 20062 GTID 818162. This flasher * event only has 3 hits in the PC with the flasher channel. Rather than * lower the limit on the number of hits per PC, I instead chose to do a * cut similar to the SNO QvT cut. */ if (qlx_all[index[nhit-1]] > qlx_all[index[nhit-2]] + 80) { nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_NORMAL) continue; /* If this hit isn't in the same slot as the highest charge * channel, continue. */ if (id/32 != i/32) continue; if (ev->pmt_hits[i].ept < -100) { /* This can happen if the channel has bad calibration or is in * the TAC curl region. * * It's not really obvious what to do in this situation. To be * conservative, we will just set the time to a negative value. * This means that if this channel ends up being the highest * charge channel, it will basically be guaranteed to be * earlier than the rest of the PMTs, and therefore this part * of the next check will always pass. However it will still * need to pass the cut that 70% of the other PMTs need to be * at least 12 meters away. */ t_pc[nhit] = -100; } else { t_pc[nhit] = ev->pmt_hits[i].ept; } nhit += 1; } /* Require at least 4 hits in the slot. */ if (nhit >= 4 && is_flasher_channel(ev,id,t_pc,nhit)) return 1; } argsort(hits_pc, LEN(hits_pc), index); /* The paddle card with the most hits must have >= 4 hits. */ if (hits_pc[index[LEN(hits_pc)-1]] < 4) return 0; /* Loop over the most hit paddle cards in order from most hits to least * hits. We do this because it's possible that more than one paddle card * has the same number of channels hit, and so we need to test all of them. */ for (j = LEN(hits_pc)-1; j >= 0; j--) { id = index[j]; /* Make sure this paddle card has at least 4 hits. * * Note: Since we loop over the PCs in order from most to least hits, * we are guaranteed that once we reach a PC with less than 4 hits, * there are no more PCs with 4 or more hits. */ if (hits_pc[id] < 4) return 0; nhit = 0; for (i = 0; i < MAX_PMTS; i++) { if (!ev->pmt_hits[i].hit || pmts[i].pmt_type != PMT_NORMAL) continue; crate = i/512; card = (i % 512)/32; channel = i % 32; id = crate*64 + card*4 + channel/8; if (id != index[j]) continue; /* Get the uncalibrated QHS and QHL charges. */ qhs = ev->pmt_hits[i].qihs; qhl = ev->pmt_hits[i].qihl; qlx = ev->pmt_hits[i].qilx; if (qhs < 300) { /* QHS values below 300 are set to 4095. */ qhs_pc[nhit] = 4095; } else { qhs_pc[nhit] = qhs; } if (qhl < 300) { /* QHL values below 300 are set to 4095. */ qhl_pc[nhit] = 4095; } else { qhl_pc[nhit] = qhl; } if (qlx < 300) { /* QLX values below 300 are set to 4095. */ qlx_pc[nhit] = 4095; } else { qlx_pc[nhit] = qlx; } if (ev->pmt_hits[i].ept < -100) { /* This can happen if the channel has bad calibration or is in * the TAC curl region. * * It's not really obvious what to do in this situation. To be * conservative, we will just set the time to a negative value. * This means that if this channel ends up being the highest * charge channel, it will basically be guaranteed to be * earlier than the rest of the PMTs, and therefore this part * of the next check will always pass. However it will still * need to pass the cut that 70% of the other PMTs need to be * at least 12 meters away. */ t_pc[nhit] = -100; } else { t_pc[nhit] = ev->pmt_hits[i].ept; } channel_pc[nhit] = channel; nhit += 1; } if (nhit < 2) { fprintf(stderr, "only 2 hits on %zu/%zu/PC%zu but expected at least 4!\n", id/64, (id % 64)/4, id % 4); continue; } /* Sort the QHS and QHL values by size. */ argsort(qhs_pc, nhit, index_qhs); argsort(qhl_pc, nhit, index_qhl); argsort(qlx_pc, nhit, index_qlx); crate = index[j]/64; card = (index[j] % 64)/4; /* Check if this paddle card has a QHS or QHL value which is 1000 * counts above the next highest QHS or QHL value, or a QLX value which * is 80 counts above the next highest QLX value. These values come * from the QvT cut in SNOMAN. * * Alternatively, if there isn't an obvious charge outlier, it's still * possible that the flasher PMT hit is missing from the event or that * the QHS/QHL/QLX values were just outside of the threshold. In this * case, we look to see if the TAC for the proposed slot is early with * respect to neighboring PMTs. */ if (qhs_pc[index_qhs[nhit-1]] > qhs_pc[index_qhs[nhit-2]] + 1000) { channel = channel_pc[index_qhs[nhit-1]]; } else if (qhl_pc[index_qhl[nhit-1]] > qhl_pc[index_qhl[nhit-2]] + 1000) { channel = channel_pc[index_qhl[nhit-1]]; } else if (qlx_pc[index_qlx[nhit-1]] > qlx_pc[index_qlx[nhit-2]] + 80) { channel = channel_pc[index_qlx[nhit-1]]; } else if (qlx_pc[index_qlx[nhit-1]] < 800) { /* Check to see if the missing channel in the PC is the flasher. */ for (i = 0; i < 8; i++) { id = crate*512 + card*32 + (id % 4)*8 + i; /* Skip channels which were hit or are not a normal PMT. */ if (ev->pmt_hits[id].hit || pmts[id].pmt_type != PMT_NORMAL) continue; if (is_slot_early(ev, id) && is_flasher_channel(ev,id,t_pc,nhit)) return 1; } continue; } else { /* No single high charge channel. */ continue; } if (is_flasher_channel(ev,crate*512 + card*32 + channel,t_pc,nhit)) return 1; } return 0; }