GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
120 / 133
90.2%
Branches:
121 / 202
59.9%
Functions:
14 / 14
100.0%

io/ies.cpp
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 /// @file ies.cpp
3 /// @brief IES LM-63 parsing, sampling and tilt.
4 #include "ernie/io/ies.h"
5
6 #include "ernie/util/file.h"
7 #include "ernie/util/math.h"
8 #include "ernie/util/text.h"
9
10 #include <algorithm>
11 #include <cmath>
12 #include <functional>
13 #include <limits>
14 #include <stdexcept>
15 #include <string>
16
17 namespace ernie {
18
19 namespace {
20
21 constexpr double kAngleSlack = 1e-9;
22
23 56 bool Near(double a, double b) {
24 56 return std::abs(a - b) < 1e-3;
25 }
26
27 /// Whether `x` lies within the angles `angles` span.
28 259250 bool Within(const std::vector<double>& angles, double x) {
29
3/4
✓ Branch 0 taken 259250 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 129603 times.
✓ Branch 3 taken 129647 times.
259250 return x >= angles.front() - kAngleSlack && x <= angles.back() + kAngleSlack;
30 }
31
32 /// Whether `angles` strictly increase, within [`low`, `high`].
33 20 bool Increasing(const std::vector<double>& angles, double low, double high) {
34
2/2
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 1 time.
20 return std::adjacent_find(angles.begin(), angles.end(), std::greater_equal<>()) ==
35 20 angles.end() &&
36
5/6
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 18 times.
20 angles.front() >= low - kAngleSlack && angles.back() <= high + kAngleSlack;
37 }
38
39 /// Type C's horizontal angle `phi` in the planes a file gives: mirrored into a quadrant or a half
40 /// of a symmetric luminaire; else a whole turn, past the last plane across the gap to the first.
41 12 double FoldPlane(const std::vector<double>& planes, double phi) {
42
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 phi = Mod(phi, 360.0);
43
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 const double first = planes.front();
44 12 const double last = planes.back();
45
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 if (Near(first, 0.0) && Near(last, 90.0)) {
46
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 8 times.
12 phi = phi > 180.0 ? 360.0 - phi : phi;
47
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
12 return phi > 90.0 ? 180.0 - phi : phi;
48 }
49
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (Near(first, 0.0) && Near(last, 180.0)) {
50
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return phi > 180.0 ? 360.0 - phi : phi;
51 }
52
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (Near(first, 90.0) && Near(last, 270.0)) {
53
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ return phi < 90.0 ? 180.0 - phi : phi > 270.0 ? 540.0 - phi : phi;
54 }
55
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return phi < first ? phi + 360.0 : phi;
56 }
57
58 constexpr const char* kFormat = "IES LM-63 file";
59
60 /// Tilt data: the lamp-to-luminaire geometry, the number of pairs, the angles, the factors.
61 5 IesTilt ReadTilt(NumberReader& numbers) {
62 5 IesTilt tilt;
63
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 tilt.geometry = static_cast<int>(numbers.Next());
64
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 const std::size_t pairs = numbers.Count();
65
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 tilt.angles = numbers.Values(pairs);
66
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 tilt.factors = numbers.Values(pairs);
67
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
10 if (tilt.geometry < 1 || tilt.geometry > 3 || pairs == 0 ||
68 5 !std::is_sorted(tilt.angles.begin(), tilt.angles.end())) {
69
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::invalid_argument("IES LM-63 file has bad tilt data");
70 }
71 5 return tilt;
72 ✗ }
73
74 12 IesType ReadType(double type) {
75
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
12 if (type != 1.0 && type != 2.0 && type != 3.0) {
76
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::invalid_argument("IES photometric type must be 1 (C), 2 (B) or 3 (A)");
77 }
78 11 return static_cast<IesType>(static_cast<int>(type));
79 }
80
81 } // namespace
82
83 4 double IesTilt::Angle(const glm::dvec3& x, const glm::dvec3& y, const glm::dvec3& z) const {
84
2/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
4 switch (geometry) {
85 1 case 1:
86
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
2 return glm::degrees(std::acos(std::clamp(z.z, -1.0, 1.0)));
87 3 case 2:
88
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 return glm::degrees(std::asin(std::min(std::abs(x.z), 1.0)));
89 ✗ case 3:
90
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return glm::degrees(std::asin(std::min(std::abs(y.z), 1.0)));
91 default:
92 return 0.0;
93 }
94 }
95
96 8 double IesTilt::Factor(double degrees) const {
97
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 time.
8 if (angles.empty()) {
98 return 1.0;
99 }
100 // Data to 90 degrees mirrors about it.
101
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 5 times.
7 if (angles.back() <= 90.0 + kAngleSlack && degrees > 90.0) {
102 2 degrees = 180.0 - degrees;
103 }
104 7 return Interpolate<double>(angles, factors, degrees);
105 }
106
107 259234 double IesProfile::Sample(double theta, double phi) const {
108
2/2
✓ Branch 0 taken 259218 times.
✓ Branch 1 taken 16 times.
259234 if (type == IesType::C) {
109 259218 return Table(theta, phi);
110 }
111 // The direction in the luminaire's frame, then the file's angles for it.
112
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 3 times.
16 const double t = glm::radians(theta);
113 16 const double p = glm::radians(phi);
114
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 3 times.
16 const glm::dvec3 e(std::sin(t) * std::cos(p), std::sin(t) * std::sin(p), -std::cos(t));
115
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 3 times.
16 if (type == IesType::B) {
116 26 return Table(glm::degrees(std::atan2(e.x, -e.z)),
117
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
26 glm::degrees(std::asin(std::clamp(-e.y, -1.0, 1.0))));
118 }
119 6 return Table(glm::degrees(std::atan2(e.y, -e.z)),
120
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
6 glm::degrees(std::asin(std::clamp(e.x, -1.0, 1.0))));
121 }
122
123 259234 double IesProfile::Table(double v, double h) const {
124
2/4
✓ Branch 0 taken 259234 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 259234 times.
✗ Branch 3 not taken.
259234 if (vertical.empty() || horizontal.empty()) {
125 return 0.0;
126 }
127
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 259218 times.
259234 if (type != IesType::C) {
128 // Types A and B give one side of a symmetric luminaire from 0 degrees.
129
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 v = Near(vertical.front(), 0.0) ? std::abs(v) : v;
130
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 6 times.
16 h = Near(horizontal.front(), 0.0) ? std::abs(h) : h;
131
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 if (horizontal.size() > 1 && !Within(horizontal, h)) {
132 return 0.0;
133 }
134 }
135
2/2
✓ Branch 0 taken 129631 times.
✓ Branch 1 taken 129603 times.
259234 if (!Within(vertical, v)) {
136 return 0.0;
137 }
138
2/2
✓ Branch 0 taken 129604 times.
✓ Branch 1 taken 27 times.
129631 const std::size_t columns = vertical.size();
139 259289 const auto row = [&](std::size_t plane) {
140
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 129658 times.
129658 if (columns == 1) {
141 ✗ return candela[plane];
142 }
143 129658 const auto [i, t] = Bracket(vertical, v);
144 129658 return std::lerp(candela[plane * columns + i], candela[plane * columns + i + 1], t);
145 129631 };
146 // One plane: rotationally symmetric, or a single plane of data.
147
2/2
✓ Branch 0 taken 129604 times.
✓ Branch 1 taken 27 times.
129631 if (horizontal.size() == 1) {
148 129604 return row(0);
149 }
150
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 15 times.
27 const double phi = type == IesType::C ? FoldPlane(horizontal, h) : h;
151 27 const double last = horizontal.back();
152
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (phi > last) {
153
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const double gap = horizontal.front() + 360.0 - last;
154
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return std::lerp(row(horizontal.size() - 1), row(0), gap > 0.0 ? (phi - last) / gap : 0.0);
155 }
156 27 const auto [plane, t] = Bracket(horizontal, phi);
157 27 return std::lerp(row(plane), row(plane + 1), t);
158 }
159
160 1 double IesProfile::Flux() const {
161 1 constexpr int kTheta = 720;
162 1 constexpr int kPhi = 360;
163 1 const double dTheta = 180.0 / kTheta;
164 1 const double dPhi = 360.0 / kPhi;
165 1 double sum = 0.0;
166
2/2
✓ Branch 0 taken 720 times.
✓ Branch 1 taken 1 time.
721 for (int i = 0; i < kTheta; ++i) {
167 720 const double theta = (i + 0.5) * dTheta;
168 720 double ring = 0.0;
169
2/2
✓ Branch 0 taken 259200 times.
✓ Branch 1 taken 720 times.
259920 for (int j = 0; j < kPhi; ++j) {
170 259200 ring += Sample(theta, (j + 0.5) * dPhi);
171 }
172 720 sum += ring * std::sin(glm::radians(theta));
173 }
174 1 return sum * glm::radians(dTheta) * glm::radians(dPhi);
175 }
176
177 14 IesProfile ParseIes(std::string_view text, const std::filesystem::path& directory) {
178 // Keywords up to TILT=; numbers after its line, separated by blanks, line breaks or commas.
179 14 const std::size_t tiltAt = text.find("TILT=");
180
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 13 times.
14 if (tiltAt == std::string_view::npos) {
181
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 throw std::invalid_argument("Not an IES LM-63 file: no TILT= line");
182 }
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 const std::size_t lineEnd = std::min(text.find('\n', tiltAt), text.size());
184 13 const std::string tilt = Trim(text.substr(tiltAt + 5, lineEnd - tiltAt - 5));
185
3/6
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 13 times.
✗ Branch 8 not taken.
18 NumberReader numbers(text.substr(lineEnd), kFormat);
186
187 13 IesProfile profile;
188
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 10 times.
13 if (tilt == "INCLUDE") {
189
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
6 profile.tilt = ReadTilt(numbers);
190
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
10 } else if (tilt != "NONE" && !tilt.empty()) {
191
6/10
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 1 time.
✓ Branch 13 taken 2 times.
✗ Branch 14 not taken.
11 NumberReader file(ReadTextFile(directory / tilt), "IES tilt file");
192
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
4 profile.tilt = ReadTilt(file);
193 2 }
194
195
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 numbers.Skip(2); // lamps, lumens per lamp (-1: absolute photometry)
196
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 const double multiplier = numbers.Next();
197
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 const std::size_t verticals = numbers.Count();
198
1/2
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
12 const std::size_t horizontals = numbers.Count();
199
3/4
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 1 time.
12 profile.type = ReadType(numbers.Next());
200
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 numbers.Skip(4); // units, width, length, height
201
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 const double ballast = numbers.Next();
202
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 numbers.Skip(2); // file generation type (formerly the ballast-lamp factor), input watts
203
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (verticals == 0 || horizontals == 0) {
204
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw std::invalid_argument("IES LM-63 file has no angles");
205 }
206
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 profile.vertical = numbers.Values(verticals);
207
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 profile.horizontal = numbers.Values(horizontals);
208
209 // Type C's vertical angles run from the nadir to the zenith; types A and B's both run from -90
210 // to 90 degrees about the beam's axis.
211 11 constexpr double kAny = std::numeric_limits<double>::infinity();
212 11 const bool c = profile.type == IesType::C;
213
5/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7 times.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 9 times.
✗ Branch 6 not taken.
24 if (!Increasing(profile.vertical, c ? 0.0 : -90.0, c ? 180.0 : 90.0) ||
214
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 6 times.
12 !Increasing(profile.horizontal, c ? -kAny : -90.0, c ? kAny : 90.0)) {
215
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 throw std::invalid_argument("IES LM-63 file's angles are out of order or range");
216 }
217
218
2/2
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 1 time.
9 profile.candela = numbers.Values(verticals * horizontals);
219
2/2
✓ Branch 0 taken 68 times.
✓ Branch 1 taken 8 times.
76 for (double& value : profile.candela) {
220
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 68 times.
68 value = std::max(value, 0.0) * multiplier * ballast;
221 }
222 8 return profile;
223 18 }
224
225 4 IesProfile ReadIes(const std::filesystem::path& path) {
226
3/4
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 time.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
10 return ParseIes(ReadTextFile(path), path.parent_path());
227 }
228
229 } // namespace ernie
230