plot-antenna 2.2__tar.gz → 2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot-antenna
3
- Version: 2.2
3
+ Version: 2.4
4
4
  Summary: Antenna plotting program for plotting antenna simulation results
5
5
  Home-page: https://github.com/schlatterbeck/plot-antenna
6
6
  Author: Ralf Schlatterbeck
@@ -233,6 +233,28 @@ your own data with `plot-antenna`_. The eznec program in
233
233
  ``plot_antenna/eznec.py`` might even be better in this regard. See the
234
234
  next section on documentation of the `plot-antenna`_ API.
235
235
 
236
+ Running the Tests
237
+ -----------------
238
+
239
+ For running the plotly tests, the ``kaleido`` python package needs to be
240
+ installed. For Debian Trixie and earlier version 0.2.1 is needed. This
241
+ is installed with pip as shown below, if you don't want to mess with the
242
+ ``--break-system-packages`` option, installing everything into a virtual
243
+ environment is the way to go::
244
+
245
+ python3 -m pip install kaleido==0.2.1 --break-system-packages
246
+
247
+ The tests use pictures saved in ``test/pics`` and will compare |--| depending
248
+ on matplotlib or plotly version |--| the picture matching the backend
249
+ version to the picture produced during the test. For this to work,
250
+ especially for plotly, the correct fonts need to be installed. Plotly
251
+ seems to prefer the Open Sans font family. So at least on Debian the
252
+ package ``fonts-open-sans`` needs to be installed. Otherwise the tests
253
+ fail because the pictures do not match. If a test fails a picture with
254
+ the extension ``.debug`` will be produced in the directory ``test/pics``.
255
+ If differences are not immediately visible, the ``compare`` program from
256
+ imagemagick may help.
257
+
236
258
  Plot-Antenna API
237
259
  ----------------
238
260
 
@@ -257,21 +279,41 @@ It has an internal ``pattern`` dictionary which stores the gain values
257
279
  by a tuple of ``(theta, phi)`` where ``theta`` is the elevation angle
258
280
  (measured from the zenith=0 degrees) and the azimuth angle phi measured
259
281
  from the positive X-axis. The gain values in this data structure are in
260
- dBi (Decibel over an isotropic radiator). There is currently no way to
261
- directly pass a numpy array with the gains. A simple program to
262
- construct an azimuth plot of an antenna that has the same pattern in all
263
- directions (gain=0dB) would be::
282
+ dBi (Decibel over an isotropic radiator).
283
+
284
+ A simple program to construct an azimuth plot of an antenna that has the
285
+ same pattern in all directions (gain=0dB) would be::
264
286
 
265
287
  import numpy as np
266
288
  from plot_antenna import plot_antenna
267
289
 
290
+ # Compute args, see below
268
291
  frequency = 430.0
269
292
  polarization = 'sum'
270
293
  key = (frequency, polarization)
271
- gdict = {key: plot_antenna.Gain_Data ([frequency])}
294
+ gdict = {key: plot_antenna.Gain_Data (key)}
272
295
  data = gdict [key].pattern
273
- for azi in np.arange (0, 361, 10):
274
- data [(90.0, azi)] = 0.0
296
+ for theta in np.arange (0, 181, 10):
297
+ for phi in np.arange (0, 361, 10):
298
+ data [(theta, phi)] = 0.0
299
+ gp = plot_antenna.Gain_Plot (args, gdict)
300
+ gp.compute ()
301
+ gp.plot ()
302
+
303
+ In the latest version you can also directly pass numpy arrays for gain,
304
+ theta, and phi angles, angles are in degrees::
305
+
306
+ import numpy as np
307
+ from plot_antenna import plot_antenna
308
+
309
+ # Compute args, see below
310
+ frequency = 430.0
311
+ polarization = 'sum'
312
+ key = (frequency, polarization)
313
+ thetas = np.arange (0, 181, 10)
314
+ phis = np.arange (0, 361, 10)
315
+ gains = np.zeros ((19, 37))
316
+ gdict = {key: plot_antenna.Gain_Data.from_gains (key, gains, thetas, phis)}
275
317
  gp = plot_antenna.Gain_Plot (args, gdict)
276
318
  gp.compute ()
277
319
  gp.plot ()
@@ -290,14 +332,18 @@ line arguments but can be called with an empty string list, e.g.::
290
332
  args.filename = ''
291
333
  # Title
292
334
  args.title = 'My Title'
293
- # We want an azimuth plot
294
- args.azimuth = True
295
335
  # We might want to ship result to running browser with plotly
296
336
  # args.show_in_browser = True
337
+ # If we want to do a 3d-plot we set args.plot3d, we could also set
338
+ # args.azimuth to get an azimuth plot. Both variables can be set and
339
+ # we get both plots (one after the other with matplotlib, both in
340
+ # different browser windows with plotly)
341
+ args.azimuth = False
342
+ args.plot3d = True
297
343
 
298
344
  The ``cmd`` variable is a python ``ArgumentParser`` object. So if you
299
345
  are parsing command line arguments you can add your own options before
300
- calling ``process_args``
346
+ calling ``process_args``.
301
347
 
302
348
  If not parsing argument from the command line and arguments should be
303
349
  changed this can be done by directly modifying args, e.g.::
@@ -319,6 +365,30 @@ the companion program for reading EZNEC data in
319
365
  Release Notes
320
366
  -------------
321
367
 
368
+ v2.4: Bug-fixes, coordinate transformation
369
+
370
+ - Fix max theta gain computation: We need to take *both sides of phi*
371
+ into account
372
+ - Allow to directly pass numpy arrays in API
373
+ - Coordinate transform for a contributed setup for a measurement device
374
+ - Update tests for debian trixie, in particular use better font defaults
375
+ that are more likely to be the same on multiple architectures
376
+
377
+ v2.3: Fix nec geo computation
378
+
379
+ - Fix a bug when parsing NEC geo info, in particular back-references in
380
+ geometry segments
381
+ - Update tests for recent changes, unfortunately the plotly PNG pics
382
+ seem not to be reproduceable across different installations of the
383
+ same plotly version
384
+
385
+ v2.2: Fix radial axis range of polar plots
386
+
387
+ - Polar plots were scaled differently depending on data, we now force
388
+ the polar axis range to a maximum of 1.01 (on both, matplotlib and
389
+ plotly backends) so that the trace(s) always fit without truncating
390
+ the trace at the boundary
391
+
322
392
  v2.1: Scale by angle
323
393
 
324
394
  - New option ``--scale-by-angle`` that allows to scale the azimuth or
@@ -201,6 +201,28 @@ your own data with `plot-antenna`_. The eznec program in
201
201
  ``plot_antenna/eznec.py`` might even be better in this regard. See the
202
202
  next section on documentation of the `plot-antenna`_ API.
203
203
 
204
+ Running the Tests
205
+ -----------------
206
+
207
+ For running the plotly tests, the ``kaleido`` python package needs to be
208
+ installed. For Debian Trixie and earlier version 0.2.1 is needed. This
209
+ is installed with pip as shown below, if you don't want to mess with the
210
+ ``--break-system-packages`` option, installing everything into a virtual
211
+ environment is the way to go::
212
+
213
+ python3 -m pip install kaleido==0.2.1 --break-system-packages
214
+
215
+ The tests use pictures saved in ``test/pics`` and will compare |--| depending
216
+ on matplotlib or plotly version |--| the picture matching the backend
217
+ version to the picture produced during the test. For this to work,
218
+ especially for plotly, the correct fonts need to be installed. Plotly
219
+ seems to prefer the Open Sans font family. So at least on Debian the
220
+ package ``fonts-open-sans`` needs to be installed. Otherwise the tests
221
+ fail because the pictures do not match. If a test fails a picture with
222
+ the extension ``.debug`` will be produced in the directory ``test/pics``.
223
+ If differences are not immediately visible, the ``compare`` program from
224
+ imagemagick may help.
225
+
204
226
  Plot-Antenna API
205
227
  ----------------
206
228
 
@@ -225,21 +247,41 @@ It has an internal ``pattern`` dictionary which stores the gain values
225
247
  by a tuple of ``(theta, phi)`` where ``theta`` is the elevation angle
226
248
  (measured from the zenith=0 degrees) and the azimuth angle phi measured
227
249
  from the positive X-axis. The gain values in this data structure are in
228
- dBi (Decibel over an isotropic radiator). There is currently no way to
229
- directly pass a numpy array with the gains. A simple program to
230
- construct an azimuth plot of an antenna that has the same pattern in all
231
- directions (gain=0dB) would be::
250
+ dBi (Decibel over an isotropic radiator).
251
+
252
+ A simple program to construct an azimuth plot of an antenna that has the
253
+ same pattern in all directions (gain=0dB) would be::
232
254
 
233
255
  import numpy as np
234
256
  from plot_antenna import plot_antenna
235
257
 
258
+ # Compute args, see below
236
259
  frequency = 430.0
237
260
  polarization = 'sum'
238
261
  key = (frequency, polarization)
239
- gdict = {key: plot_antenna.Gain_Data ([frequency])}
262
+ gdict = {key: plot_antenna.Gain_Data (key)}
240
263
  data = gdict [key].pattern
241
- for azi in np.arange (0, 361, 10):
242
- data [(90.0, azi)] = 0.0
264
+ for theta in np.arange (0, 181, 10):
265
+ for phi in np.arange (0, 361, 10):
266
+ data [(theta, phi)] = 0.0
267
+ gp = plot_antenna.Gain_Plot (args, gdict)
268
+ gp.compute ()
269
+ gp.plot ()
270
+
271
+ In the latest version you can also directly pass numpy arrays for gain,
272
+ theta, and phi angles, angles are in degrees::
273
+
274
+ import numpy as np
275
+ from plot_antenna import plot_antenna
276
+
277
+ # Compute args, see below
278
+ frequency = 430.0
279
+ polarization = 'sum'
280
+ key = (frequency, polarization)
281
+ thetas = np.arange (0, 181, 10)
282
+ phis = np.arange (0, 361, 10)
283
+ gains = np.zeros ((19, 37))
284
+ gdict = {key: plot_antenna.Gain_Data.from_gains (key, gains, thetas, phis)}
243
285
  gp = plot_antenna.Gain_Plot (args, gdict)
244
286
  gp.compute ()
245
287
  gp.plot ()
@@ -258,14 +300,18 @@ line arguments but can be called with an empty string list, e.g.::
258
300
  args.filename = ''
259
301
  # Title
260
302
  args.title = 'My Title'
261
- # We want an azimuth plot
262
- args.azimuth = True
263
303
  # We might want to ship result to running browser with plotly
264
304
  # args.show_in_browser = True
305
+ # If we want to do a 3d-plot we set args.plot3d, we could also set
306
+ # args.azimuth to get an azimuth plot. Both variables can be set and
307
+ # we get both plots (one after the other with matplotlib, both in
308
+ # different browser windows with plotly)
309
+ args.azimuth = False
310
+ args.plot3d = True
265
311
 
266
312
  The ``cmd`` variable is a python ``ArgumentParser`` object. So if you
267
313
  are parsing command line arguments you can add your own options before
268
- calling ``process_args``
314
+ calling ``process_args``.
269
315
 
270
316
  If not parsing argument from the command line and arguments should be
271
317
  changed this can be done by directly modifying args, e.g.::
@@ -287,6 +333,30 @@ the companion program for reading EZNEC data in
287
333
  Release Notes
288
334
  -------------
289
335
 
336
+ v2.4: Bug-fixes, coordinate transformation
337
+
338
+ - Fix max theta gain computation: We need to take *both sides of phi*
339
+ into account
340
+ - Allow to directly pass numpy arrays in API
341
+ - Coordinate transform for a contributed setup for a measurement device
342
+ - Update tests for debian trixie, in particular use better font defaults
343
+ that are more likely to be the same on multiple architectures
344
+
345
+ v2.3: Fix nec geo computation
346
+
347
+ - Fix a bug when parsing NEC geo info, in particular back-references in
348
+ geometry segments
349
+ - Update tests for recent changes, unfortunately the plotly PNG pics
350
+ seem not to be reproduceable across different installations of the
351
+ same plotly version
352
+
353
+ v2.2: Fix radial axis range of polar plots
354
+
355
+ - Polar plots were scaled differently depending on data, we now force
356
+ the polar axis range to a maximum of 1.01 (on both, matplotlib and
357
+ plotly backends) so that the trace(s) always fit without truncating
358
+ the trace at the boundary
359
+
290
360
  v2.1: Scale by angle
291
361
 
292
362
  - New option ``--scale-by-angle`` that allows to scale the azimuth or
@@ -0,0 +1 @@
1
+ 2.4
@@ -0,0 +1 @@
1
+ VERSION="2.4"
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/python3
2
+
3
+ # Parsers for contributed data structures
4
+
5
+ import sys
6
+ import numpy as np
7
+ from csv import DictReader
8
+ from . import plot_antenna as aplot
9
+
10
+ def coordinate_transform (gd):
11
+ """ Coordinate transformation on gain data: The measurement
12
+ describes a great circle for each elevation, so the 'poles' are
13
+ on the axis of the positioner. The positioner axis becomes the
14
+ new Z axis and new elevations are the azimuth values. The new
15
+ azimuths are computed from the elevations.
16
+ """
17
+ t = gd.phis_d [gd.phis_d <= 180]
18
+ p = sorted (list (set (gd.thetas_d).union (gd.thetas_d + 180)))
19
+ if p [-1] != 360.:
20
+ p.append (360.)
21
+ otl = gd.thetas_d.shape [0]
22
+ if gd.thetas_d [otl - 1] != 180:
23
+ otl += 1
24
+ p = np.array (p)
25
+ ng = np.zeros ((len (t), len (p)))
26
+ opl = gd.phis_d.shape [0]
27
+ for nth, theta in enumerate (t):
28
+ for nph, phi in enumerate (p):
29
+ if phi <= 180:
30
+ oth = nph
31
+ oph = nth
32
+ else:
33
+ oth = nph - (p.shape [0] // 2)
34
+ oph = opl - nth - 1
35
+ if oth == gd.gains.shape [0]:
36
+ # This happens only when 180° is missing for positioner
37
+ ng [nth, nph] = gd.gains [0, opl - oph - 1]
38
+ else:
39
+ ng [nth, nph] = gd.gains [oth, oph]
40
+ gd.gains = ng
41
+ gd.phis_d = p
42
+ gd.thetas_d = t
43
+ gd.phis = p * np.pi / 180
44
+ gd.thetas = t * np.pi / 180
45
+ # end def coordinate_transform
46
+
47
+ def parse_csv_measurement_data (args):
48
+ """ Parses measurement data from CSV file
49
+ This has the columns:
50
+ - Messwert: The measurement
51
+ May also be called 'eirp' or 'eirp (dBm)'
52
+ - Einheit Messwert: Unit of the measurement (e.g. dBm)
53
+ This can also be called "Einheit eirp"
54
+ Or we have a column 'eirp (dBm)' above and the column with the
55
+ unit is missing
56
+ - Position Drehscheibe: Azimuth angle
57
+ also called Position Drehscheibe (deg)
58
+ - Position Positionierer: Elevation angle
59
+ also seen as Position Positioner
60
+ or Position Positioner (deg)
61
+ - Polarisation: 'PH' for horizontal, 'PV' for vertical polarization
62
+ - Messfrequenz: frequency
63
+ For an example see test/Messdaten.csv
64
+ The format has the following peculiarities:
65
+ - The elevation angles slightly vary for a single azimuth scan.
66
+ This means we may have 10° and some values at 10.1°. Since the
67
+ elevation angle steps are 10° we round to the nearest integer.
68
+ - Azimuth is scanned continuously, so azimuth angles do not
69
+ match at all for two different elevation angles. This still
70
+ means we can plot an azimuth polarization diagram. But for
71
+ elevation or 3d plots we need to interpolate the azimuth
72
+ angles. For this the --interpolate-azimuth-step option was
73
+ added. Typically we interpolate the azimuth values to e.g. a
74
+ 2° grid.
75
+ - Some azimuth angles are greater than 360°.
76
+ """
77
+ # We always need to interpolate to do the coordinate transformation
78
+ # And we fix this to 1° for ease of coordinate transform
79
+ if args.interpolate_azimuth_step is None:
80
+ args.interpolate_azimuth_step = 1
81
+ gdata_dict = {}
82
+ with open (args.filename, 'r') as f:
83
+ dr = DictReader (f, delimiter = ';')
84
+ for rec in dr:
85
+ for m in ('Einheit Messwert', 'Einheit eirp'):
86
+ try:
87
+ args.dB_unit = rec [m]
88
+ break
89
+ except KeyError:
90
+ pass
91
+ else:
92
+ if 'eirp (dBm)' in rec:
93
+ args.dB_unit = 'dBm'
94
+ # Frequency: The internal representation is in MHz, so we
95
+ # multiply by 1e3 because the values are in GHz.
96
+ try:
97
+ f = float (rec ['Messfrequenz']) * 1e3
98
+ except KeyError:
99
+ f = float (rec ['Messfrequenz (GHz)']) * 1e3
100
+ if f not in gdata_dict:
101
+ p = rec ['Polarisation'][1:]
102
+ k = (f, p)
103
+ if k not in gdata_dict:
104
+ gdata_dict [k] = aplot.Gain_Data \
105
+ (k, transform = coordinate_transform)
106
+ gdata = gdata_dict [k]
107
+ for ds in ('Position Drehscheibe', 'Position Drehscheibe (deg)'):
108
+ try:
109
+ azi = float (rec [ds])
110
+ if args.invert_turntable:
111
+ azi = -azi
112
+ azi = (azi + args.turntable_offset) % 360
113
+ break
114
+ except KeyError:
115
+ pass
116
+ else:
117
+ raise ValueError ('No column "Position Drehscheibe"')
118
+ # Need to round elevation values: these sometimes differ
119
+ # during a scan
120
+ p = ( 'Position Positionierer', 'Position Positioner'
121
+ , 'Position Positioner (deg)'
122
+ )
123
+ for k in p:
124
+ try:
125
+ ele = float (rec [k])
126
+ if args.invert_positioner:
127
+ ele = -ele
128
+ ele %= 360
129
+ break
130
+ except KeyError:
131
+ pass
132
+ else:
133
+ raise ValueError ('No column "Position Positioner"')
134
+ if args.round_positioner:
135
+ rel = args.round_positioner
136
+ ele = round (ele / rel, 0) * rel
137
+ # Don't allow values outside angle range
138
+ if azi < 0 or azi > 360 or ele < 0 or ele > 360:
139
+ continue
140
+ # We treat the value as 'dBi' although this is dBm
141
+ # Probably needs conversion but we may get away with
142
+ # allowing a unit to be specified for the plot, it must be a
143
+ # dezibel value, though (not linear gain or so)
144
+ for k in ('Messwert', 'eirp', 'eirp (dBm)'):
145
+ try:
146
+ gain = float (rec [k])
147
+ break
148
+ except KeyError:
149
+ pass
150
+ else:
151
+ raise ValueError ('No column "eirp" or similar')
152
+ gdata.pattern [(ele, azi)] = gain
153
+ return gdata_dict
154
+ # end def parse_csv_measurement_data
155
+
156
+ def main_csv_measurement_data (argv = sys.argv [1:], pic_io = None):
157
+ """ Parse a contributed measurement format, see docstring of
158
+ parse_csv_measurement_data.
159
+ The pic_io argument is for testing.
160
+ """
161
+ cmd = aplot.options_general ()
162
+ aplot.options_gain (cmd)
163
+ cmd.add_argument ('filename', help = 'CSV File to parse and plot')
164
+ cmd.add_argument \
165
+ ( '--round-positioner'
166
+ , help = "Round positioner angle to this many degrees"
167
+ , type = int
168
+ )
169
+ cmd.add_argument \
170
+ ( '--turntable-offset'
171
+ , help = "Offset in degrees of the turntable, default=%(default)s"
172
+ , type = float
173
+ , default = 0.0
174
+ )
175
+ cmd.add_argument \
176
+ ( '--invert-turntable'
177
+ , help = "Invert turntable angles, default False"
178
+ , action = 'store_true'
179
+ )
180
+ cmd.add_argument \
181
+ ( '--invert-positioner'
182
+ , help = "Invert positioner angles, default False"
183
+ , action = 'store_true'
184
+ )
185
+ args = aplot.process_args (cmd, argv)
186
+ # Set default polarization, we need this otherwise the sum isn't computed
187
+ if not args.polarization:
188
+ args.polarization ['sum'] = True
189
+ if pic_io is not None:
190
+ args.output_file = pic_io
191
+ args.save_format = 'png'
192
+ gdata = parse_csv_measurement_data (args)
193
+ gp = aplot.Gain_Plot (args, gdata)
194
+ gp.compute ()
195
+ if 0:
196
+ # Try find (old) azimuth where only the polarization changes
197
+ # This assumes that the phi angles are the same for H and V
198
+ keys = list (gp.gdata)
199
+ key = None
200
+ for k in keys:
201
+ if len (k) == 2 and k [1] == 'sum':
202
+ key = k
203
+ break
204
+ if key is not None:
205
+ stat = {}
206
+ for oph, phi in enumerate (gp.gdata [key].phis_d):
207
+ gbp = []
208
+ for oth, theta in enumerate (gp.gdata [key].thetas_d):
209
+ gbp.append (gp.gdata [key].gains [oth, oph])
210
+ gbp = np.array (gbp)
211
+ stat [phi] = (np.average (gbp), np.std (gbp))
212
+ for k in stat:
213
+ print ("%3g: avg: %g std: %g cv: %g" % (k, stat [k][0],
214
+ stat [k][1], stat [k][1] / abs (stat [k][0])))
215
+
216
+ gp.plot ()
217
+ # end def main_csv_measurement_data
218
+
219
+ if __name__ == '__main__':
220
+ main_csv_measurement_data ()