vectorose 0.2.1__py3-none-any.whl

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.
vectorose/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """VectoRose: A new tool for visualising and analysing directional data."""
2
+
3
+ from vectorose import polar_data
4
+ from vectorose import plotting
5
+ from vectorose import io
6
+ from vectorose import triangle_sphere
7
+ from vectorose import tregenza_sphere
8
+ from vectorose import util
9
+ from vectorose import stats
10
+
11
+ from vectorose.sphere_base import SphereBase
12
+
13
+ from importlib_metadata import version
14
+
15
+ __version__ = version("vectorose")
vectorose/io.py ADDED
@@ -0,0 +1,400 @@
1
+ # Copyright (c) 2023-, Benjamin Rudski, Joseph Deering
2
+ #
3
+ # This code is licensed under the MIT License. See the `LICENSE` file for
4
+ # more details about copying.
5
+
6
+ """
7
+ Functions for import and export.
8
+
9
+ This module provides the ability to load vector fields from file and save
10
+ vector fields and vector rose histogram data.
11
+ """
12
+
13
+ import enum
14
+ import os
15
+ from typing import Any, Dict, Optional, Sequence, Type, Union
16
+
17
+ import matplotlib.animation
18
+ import numpy as np
19
+ import pandas as pd
20
+
21
+ DEFAULT_LOCATION_COLUMNS = (0, 1, 2)
22
+ """Default column numbers for the location coordinates in the order
23
+ ``(x, y, z)``."""
24
+
25
+ DEFAULT_COMPONENT_COLUMNS = (-3, -2, -1)
26
+ """Default column numbers for the vector components in the order
27
+ ``(vx, vy, vz)``."""
28
+
29
+
30
+ class VectorFileType(enum.Enum):
31
+ """File types for numeric data.
32
+
33
+ Numeric data may be imported and exported in a number of different
34
+ formats. This enumerated type allows the user to specify which file
35
+ type they would like to use to load or store numeric data, such as
36
+ vector lists and binning arrays. The associated strings for each member
37
+ are the file extension **without a dot**.
38
+
39
+ Members
40
+ -------
41
+ CSV
42
+ Comma-separated value file, in which the columns will be separated
43
+ by a tab "\\t". File extension: ``*.csv``.
44
+ NPY
45
+ NumPy array, which can easily be loaded into NumPy. File extension:
46
+ ``*.npy``.
47
+ EXCEL
48
+ Microsoft Excel spreadsheet (compatible with Excel 2007 or later).
49
+ File extension: ``*.xlsx``.
50
+
51
+ Warnings
52
+ --------
53
+ When constructing a filename using the members of this type, a dot
54
+ ``(.)`` must be added.
55
+ """
56
+
57
+ CSV = "csv"
58
+ NPY = "npy"
59
+ EXCEL = "xlsx"
60
+
61
+
62
+ class ImageFileType(enum.Enum):
63
+ """Image File Types.
64
+
65
+ File types for images. These include both raster formats (``*.png`` and
66
+ ``*.tiff``) and vector formats (``*.svg`` and ``*.pdf``). The members
67
+ of this enumerated type have as value the string extensions for the
68
+ respective file types **without** the dot.
69
+
70
+ Members
71
+ -------
72
+ PNG
73
+ Portable Network Graphics (png) image (raster).
74
+ TIFF
75
+ Tagged Image File Format (tiff) image (raster).
76
+ SVG
77
+ Scalable Vector Graphic (svg) image (vector).
78
+ PDF
79
+ Portable Document Format (pdf) file (vector).
80
+
81
+ Warnings
82
+ --------
83
+ When constructing a filename using the members of this type, a dot
84
+ ``(.)`` must be added.
85
+ """
86
+
87
+ PNG = "png"
88
+ TIFF = "tiff"
89
+ SVG = "svg"
90
+ PDF = "pdf"
91
+
92
+
93
+ class VideoFileType(enum.Enum):
94
+ """Video File Types.
95
+
96
+ File types for videos and animations. The raw values for the members of
97
+ this enumerated type correspond to the respective file extensions
98
+ **without** a period.
99
+
100
+ Members
101
+ -------
102
+ MP4
103
+ Moving Picture Experts Group (MPEG) 4 video format.
104
+ GIF
105
+ Graphics Interchange Format animated image (regardless of whether
106
+ you pronounce if G-IF or J-IF).
107
+
108
+
109
+ Warnings
110
+ --------
111
+ When constructing a filename using the members of this type, a dot
112
+ ``(.)`` must be added.
113
+ """
114
+
115
+ MP4 = "mp4"
116
+ GIF = "gif"
117
+
118
+
119
+ def __infer_filetype_from_filename(
120
+ filename: str, file_type_enum: Type[enum.Enum]
121
+ ) -> Optional[enum.Enum]:
122
+ """Infer a file type from a filename.
123
+
124
+ This function tries to infer a file type, of the provided enumerated
125
+ type ``file_type_enum`` from a provided filename by checking the
126
+ extension. If no valid extension is found, ``None`` is returned.
127
+ Otherwise, the determined file type is returned.
128
+
129
+ Parameters
130
+ ----------
131
+ filename
132
+ String containing the filename.
133
+ file_type_enum
134
+ Enumerated type representing the desired file type. This enumerated
135
+ type should have string values representing various file
136
+ extensions. These values **should not** contain a dot.
137
+
138
+ Returns
139
+ -------
140
+ file_type_enum or None:
141
+ Member of ``file_type_enum`` if a valid file type is found.
142
+ Otherwise, ``None``.
143
+
144
+ See Also
145
+ --------
146
+ ImageFileType: Sample enumerated types to pass in for image files.
147
+ VectorFileType: Sample enumerated types for vector data files.
148
+ """
149
+
150
+ # Separate out the file extension
151
+ basename, extension = os.path.splitext(filename)
152
+
153
+ # Remove the dot from the extension.
154
+ cleaned_extension = extension.lstrip(".")
155
+
156
+ try:
157
+ # Try to get the file type based on the extension.
158
+ file_type = file_type_enum(cleaned_extension)
159
+ except ValueError:
160
+ # Otherwise, no filetype found.
161
+ file_type = None
162
+
163
+ return file_type
164
+
165
+
166
+ def __infer_vector_filetype_from_filename(
167
+ filename: str,
168
+ ) -> Optional[VectorFileType]:
169
+ """Infer a vector field file type from a filename.
170
+
171
+ This function tries to infer a :class:`VectorFileType` from a provided
172
+ filename by checking the extension. If no valid extension is found,
173
+ :class:`None` is returned. Otherwise, the determined vector type is
174
+ returned.
175
+
176
+ Parameters
177
+ ----------
178
+ filename
179
+ String containing the filename.
180
+
181
+ Returns
182
+ -------
183
+ VectorFileType or None:
184
+ Vector file type corresponding to the filename if a valid filetype
185
+ is found. Otherwise, :class:`None`.
186
+ """
187
+
188
+ vector_file_type = __infer_filetype_from_filename(
189
+ filename=filename, file_type_enum=VectorFileType
190
+ )
191
+
192
+ return vector_file_type
193
+
194
+
195
+ def __infer_video_filetype_from_filename(
196
+ filename: str,
197
+ ) -> Optional[VideoFileType]:
198
+ """Infer a video file type from a filename.
199
+
200
+ This function tries to infer a :class:`VideoFileType` from a provided
201
+ filename by checking the extension. If no valid extension is found,
202
+ :class:`None` is returned. Otherwise, the determined vector type is
203
+ returned.
204
+
205
+ Parameters
206
+ ----------
207
+ filename
208
+ String containing the filename.
209
+
210
+ Returns
211
+ -------
212
+ VideoFileType or None:
213
+ Video file type corresponding to the filename if a valid filetype
214
+ is found. Otherwise, :class:`None`.
215
+ """
216
+
217
+ video_file_type = __infer_filetype_from_filename(
218
+ filename=filename, file_type_enum=VideoFileType
219
+ )
220
+
221
+ return video_file_type
222
+
223
+
224
+ def import_vector_field(
225
+ filepath: str,
226
+ default_file_type: VectorFileType = VectorFileType.NPY,
227
+ contains_headers: bool = False,
228
+ sheet: Optional[Union[str, int]] = None,
229
+ location_columns: Optional[Sequence[int]] = DEFAULT_LOCATION_COLUMNS,
230
+ component_columns: Sequence[int] = DEFAULT_COMPONENT_COLUMNS,
231
+ component_axis: int = -1,
232
+ separator: str = "\t",
233
+ ) -> Optional[np.ndarray]:
234
+ """Import a vector field.
235
+
236
+ Load a vector field from a file into a NumPy array. For available
237
+ file formats, see :class:`VectorFileType`. The file type is inferred
238
+ from the filename. If it cannot be inferred, the ``default_file_type``
239
+ is tried. If the vector field is not valid, then :class:`None` is
240
+ returned.
241
+
242
+ Parameters
243
+ ----------
244
+ filepath
245
+ File path to the vector field file.
246
+ default_file_type
247
+ File type to attempt if the type cannot be inferred from the
248
+ filename.
249
+ contains_headers
250
+ Indicate whether the file contains headers. This option is only
251
+ considered if the vectors are in a CSV or Excel file.
252
+ sheet
253
+ Name or index of the sheet to consider if the vectors are in an
254
+ Excel file.
255
+ location_columns
256
+ Column indices for the vector *spatial coordinates* in the order
257
+ ``(x, y, z)``. If this is set to :class:`None`, the vectors are
258
+ assumed to be located at the origin. By default, the first three
259
+ columns are assumed to refer to ``(x, y, z)``, respectively.
260
+ component_columns
261
+ Column indices referring to the vector *components* in the order
262
+ ``(vx, vy, vz)``. By default, the last three columns
263
+ ``(-3, -2, -1)`` are assumed to be the ``(vx, vy, vz)``.
264
+ component_axis
265
+ Axis along which the components are defined, in the case of a NumPy
266
+ array which has more than 2 dimensions.
267
+ separator
268
+ Column separator to use if the vector field is a CSV file.
269
+
270
+ Returns
271
+ -------
272
+ numpy.ndarray or None
273
+ NumPy array containing the vectors. The array has shape
274
+ ``(n, 3)`` or ``(n, 6)``, depending on whether the locations
275
+ are included. The columns correspond to ``(x,y,z)`` coordinates
276
+ of the location (if available), followed by ``(vx, vy, vz)``
277
+ components. If the filetype cannot be properly inferred,
278
+ a value of ``None`` is returned instead.
279
+
280
+ Raises
281
+ ------
282
+ OSError
283
+ The requested file does not exist.
284
+ ValueError
285
+ The requested file cannot be parsed.
286
+
287
+ Notes
288
+ -----
289
+ If the vector field file passed contains an array with a dimension > 2,
290
+ then the components are assumed to be along the axis passed in the
291
+ argument `component_axis` and the array will be flattened to 2D.
292
+ """
293
+
294
+ # First, infer the file type from the filename
295
+ filetype = __infer_vector_filetype_from_filename(filepath)
296
+
297
+ # If inference fails, try the default file type.
298
+ if filetype is None:
299
+ filetype = default_file_type
300
+
301
+ if filetype is VectorFileType.NPY:
302
+ vector_field: np.ndarray = np.load(filepath)
303
+
304
+ # Check the dimension of the vector field array
305
+ if vector_field.ndim > 2:
306
+ # Get the vector dimension
307
+ d = vector_field.shape[component_axis]
308
+
309
+ # Flatten the array
310
+ vector_field = np.moveaxis(vector_field, component_axis, -1).reshape(
311
+ -1, d
312
+ )
313
+
314
+ # Remove any rows containing NaN
315
+ vector_field = vector_field[~np.any(np.isnan(vector_field), axis=1)]
316
+
317
+ # Use Pandas in the other cases
318
+ else:
319
+ header_row: Optional[int] = 0 if contains_headers else None
320
+ # Reading function depends on whether CSV or Excel
321
+ if filetype is VectorFileType.CSV:
322
+ vector_field_dataframe = pd.read_csv(
323
+ filepath, header=header_row, sep=separator
324
+ )
325
+ elif filetype is VectorFileType.EXCEL:
326
+ vector_field_dataframe = pd.read_excel(
327
+ filepath, sheet_name=sheet, header=header_row
328
+ )
329
+ else:
330
+ return None
331
+
332
+ # Remove NaN values
333
+ vector_field = vector_field_dataframe.dropna().to_numpy()
334
+
335
+ n, d = vector_field.shape
336
+
337
+ # Now, for the column parsing
338
+ if location_columns is None or d < 6:
339
+ # No location, only consider the components
340
+ clean_vector_field = vector_field[:, component_columns]
341
+ else:
342
+ # Consider both the location and the components.
343
+ column_indices = list(location_columns) + list(component_columns)
344
+
345
+ # Squeeze is necessary to not break type safety.
346
+ clean_vector_field = vector_field[:, column_indices]
347
+
348
+ # Convert the vector field to have high-precision floating point type
349
+ clean_vector_field = clean_vector_field.astype(float)
350
+
351
+ return clean_vector_field
352
+
353
+
354
+ def export_mpl_animation(
355
+ animation: matplotlib.animation.Animation,
356
+ filename: str,
357
+ file_type: VideoFileType = VideoFileType.MP4,
358
+ dpi: Optional[int] = 150,
359
+ fps: Optional[int] = None,
360
+ **export_kwargs: Dict[str, Any],
361
+ ):
362
+ """Export a Matplotlib animation.
363
+
364
+ Export a provided Matplotlib animation as a video.
365
+
366
+ Parameters
367
+ ----------
368
+ animation
369
+ The :class:`matplotlib.animation.Animation` animation to export.
370
+ filename
371
+ The destination for the video export. This filename will be used to
372
+ infer the export file type.
373
+ file_type
374
+ The default filetype to consider if unable to resolve the file type
375
+ from the file name.
376
+ dpi
377
+ Resolution of the exported video in dots-per-inch (DPI).
378
+ fps
379
+ Desired frame rate in the exported video.
380
+ export_kwargs
381
+ Additional keyword arguments
382
+ to :meth:`matplotlib.animation.Animation.save`.
383
+
384
+ See Also
385
+ --------
386
+ matplotlib.animation.Animation.save:
387
+ The function called to perform the actual export.
388
+
389
+ """
390
+
391
+ # Infer the file type from the filename
392
+ inferred_type = __infer_video_filetype_from_filename(filename)
393
+
394
+ if inferred_type is None:
395
+ filename = f"{filename}.{file_type.value}"
396
+
397
+ if fps == 0:
398
+ fps = None
399
+
400
+ animation.save(filename=filename, fps=fps, dpi=dpi, **export_kwargs)