Simple-Track 2.0.0__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.
@@ -0,0 +1,295 @@
1
+ import csv
2
+ import datetime
3
+ from ast import literal_eval
4
+ from pathlib import Path
5
+ from typing import Union
6
+
7
+ import numpy as np
8
+
9
+ from simpletrack.feature import Feature
10
+ from simpletrack.frame import Frame, Timeline
11
+ from simpletrack.utils import check_arrays
12
+
13
+
14
+ class FrameOutputManager:
15
+ """
16
+ Class for outputting contents of Frames to .txt, .csv and .field files.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ output_path: str = "./output",
22
+ expt_name: str = "default",
23
+ start_time: str = None,
24
+ config_path: str = None,
25
+ ):
26
+ output_path = Path(output_path)
27
+ output_path.mkdir(parents=True, exist_ok=True)
28
+ self.output_path = output_path
29
+ self.expt_name = expt_name
30
+ self.start_time = start_time
31
+ self.config_path = config_path
32
+ self.strftime = "%Y%m%d_%H%M"
33
+
34
+ def features_to_txt(self, frame: Frame) -> None:
35
+ """
36
+ Outputs contents of input Frame to a text file
37
+
38
+ Args:
39
+ frame (Frame): _description_
40
+ """
41
+ frame_time = frame.time
42
+ frame_time_str = frame_time.strftime(self.strftime)
43
+ output_fnm = f"{self.output_path}/frame_{frame_time_str}.txt"
44
+
45
+ with open(output_fnm, "w") as output_file:
46
+ output_file.write(self.expt_name + "\n")
47
+ if self.config_path is not None:
48
+ output_file.write(f"Config path: {self.config_path}\n")
49
+ if self.start_time is not None:
50
+ output_file.write(f"Start time: {self.start_time}\n")
51
+ output_file.write(f"Frame time: {frame_time_str}\n")
52
+ output_file.write(f"Total tracked features: {frame.max_id}\n")
53
+
54
+ frame_features_dict = frame.features
55
+
56
+ for feature_id in sorted(frame_features_dict):
57
+ feature = frame_features_dict[feature_id]
58
+ output_line = feature.summarise(output_type="str")
59
+ output_file.write(output_line + "\n")
60
+
61
+ def features_to_csv(self, frame: Frame) -> None:
62
+ frame_time = frame.time
63
+ frame_time_str = frame_time.strftime(self.strftime)
64
+ output_fnm = f"{self.output_path}/frame_{frame_time_str}.csv"
65
+
66
+ with open(output_fnm, "w") as output_file:
67
+ # Write headers
68
+ writer = csv.writer(output_file)
69
+ writer.writerow([self.expt_name])
70
+ if self.config_path is not None:
71
+ writer.writerow([f"Config path: {self.config_path}"])
72
+ if self.start_time is not None:
73
+ writer.writerow([f"Start time: {self.start_time}"])
74
+ writer.writerow([f"Frame time: {frame_time_str}"])
75
+ writer.writerow([f"Total tracked features: {frame.max_id}"])
76
+
77
+ # Write data
78
+ frame_features_dict = frame.features
79
+ # Get data headers by looking at any feature in the frame features dict
80
+ random_feature = frame_features_dict[list(frame_features_dict.keys())[0]]
81
+ data_headers = random_feature.summarise("dict").keys()
82
+ dict_writer = csv.DictWriter(output_file, fieldnames=data_headers)
83
+ dict_writer.writeheader()
84
+
85
+ for feature_id in sorted(frame_features_dict):
86
+ feature = frame_features_dict[feature_id]
87
+ dict_writer.writerow(feature.summarise("dict"))
88
+
89
+ def fields_to_npy(self, frame: Frame) -> None:
90
+ """
91
+ Output feature and lifetime fields to .npy files
92
+
93
+ Args:
94
+ frame (Frame): _description_
95
+ """
96
+ outputs = {
97
+ "features": [frame.feature_field, "%.6e"],
98
+ "lifetime": [frame.lifetime_field, "%.4e"],
99
+ "y-flow": [frame.get_flow()[0], "%.2e"],
100
+ "x-flow": [frame.get_flow()[1], "%.2e"],
101
+ }
102
+ frame_time = frame.time
103
+ frame_time_str = frame_time.strftime("%Y%m%d_%H%M")
104
+ for output_fnm, [output, output_fmt] in outputs.items():
105
+ if output is None:
106
+ continue
107
+ full_fnm = f"{self.output_path}/{output_fnm}_{frame_time_str}.field"
108
+ np.savetxt(full_fnm, output, fmt=output_fmt)
109
+
110
+ def output_density_field(
111
+ self, timeline: Timeline, field_type: str, centroid_only: bool = True
112
+ ):
113
+ """
114
+ Loops over all Frames in Timeline, makes density plot of areas
115
+ where new Features are being created
116
+
117
+ Args:
118
+ timeline (Timeline): _description_
119
+ """
120
+ valid_types = ["init", "dissipation"]
121
+ if field_type not in valid_types:
122
+ raise ValueError(
123
+ f"field_type ({field_type}) not in valid_types {valid_types}"
124
+ )
125
+
126
+ all_frames = list(timeline.get_timeline().values())
127
+ if not all(isinstance(frame, Frame) for frame in all_frames):
128
+ return TypeError(f"Expected all Frames, got {all_frames}")
129
+
130
+ check_arrays(
131
+ *[frame.feature_field for frame in all_frames],
132
+ ndim=2,
133
+ equal_shape=True,
134
+ non_negative=True,
135
+ )
136
+
137
+ # If above check passes, can make storage array from first frame field
138
+ field_shape = (len(all_frames), *all_frames[0].feature_field.shape)
139
+ field_density = np.zeros(field_shape)
140
+
141
+ for frame_idx, frame in enumerate(all_frames):
142
+ field = frame.get_field(field_type, centroid_only=centroid_only)
143
+ field_density[frame_idx, ...] = field
144
+
145
+ output_fnm = f"{self.output_path}/{field_type}_density.npy"
146
+ np.save(output_fnm, field_density)
147
+
148
+
149
+ class LoadOutput:
150
+ """
151
+ Contains functionality for reading previous outputs back into a Timeline object
152
+ (contanining Frames of field and Feature data) for further inspection and analysis.
153
+ """
154
+
155
+ def __init__(self, st_data_path: Union[str | Path]):
156
+ self.path = Path(st_data_path)
157
+ self.strftime = "%Y%m%d_%H%M"
158
+ # Links field type names in outputs to attribute names in Frame
159
+ self.field_attributes = {
160
+ "features": "feature_field",
161
+ "lifetime": "lifetime_field",
162
+ "x-flow": "x_flow",
163
+ "y-flow": "y_flow",
164
+ }
165
+
166
+ def load_to_timeline(self) -> Timeline:
167
+ timeline = Timeline()
168
+
169
+ # Get list of times from output fields.
170
+ frame_times = self.get_frame_times_from_field_filenames()
171
+
172
+ # Load blank Frames into Timeline
173
+ for frame_time in frame_times:
174
+ frame = Frame()
175
+ frame.set_time(frame_time)
176
+ timeline.add_to_timelime(frame)
177
+
178
+ # Load fields into blank Frames
179
+ self.load_frame_fields(timeline)
180
+ # Load raw data into frame
181
+ # TODO: will need config file for this to determine
182
+ # if a Loader has been used, and location of input data
183
+ # self.load_raw_fields(timeline)
184
+
185
+ # Populate features in each Frame
186
+ for frame in timeline.get_timeline().values():
187
+ frame.populate_features()
188
+
189
+ # Finally, fill these Features with data loaded from outputs
190
+ self.load_feature_data(timeline)
191
+ return timeline
192
+
193
+ def load_feature_data(self, timeline: Timeline) -> None:
194
+ # Get list of headers from blank Feature class
195
+ blank_feature = Feature(
196
+ id=1,
197
+ feature_coords=np.array(((0, 0), (0, 0))),
198
+ time=datetime.datetime.now(),
199
+ )
200
+ headers = blank_feature.summarise(headers_only=True)
201
+ # Set the number of headers to skip in each csv file
202
+ number_header_rows = 5
203
+
204
+ for frame_time, frame in timeline.get_timeline().items():
205
+ # Load all data for the current time
206
+ frame_time_str = frame_time.strftime(self.strftime)
207
+ frame_time_fnames = self.path.rglob(f"*{frame_time_str}.csv")
208
+
209
+ for fname in frame_time_fnames:
210
+ # Read data from output
211
+ with open(fname) as csv_file:
212
+ reader = csv.DictReader(csv_file, fieldnames=headers)
213
+ all_feature_data = [
214
+ row
215
+ for row_idx, row in enumerate(reader)
216
+ if row_idx > number_header_rows
217
+ ]
218
+
219
+ # Add data to feature object in Frame
220
+ for feature_data in all_feature_data:
221
+ id = int(feature_data["id"])
222
+ feature = frame.get_feature(id)
223
+
224
+ # Loop over all features, set attribute to value
225
+ for property, value in feature_data.items():
226
+ if property == "id" or property == "centroid":
227
+ continue
228
+
229
+ if len(value) == 0:
230
+ setattr(feature, property, None)
231
+ else:
232
+ # Literal eval converts str to inferred python type
233
+ setattr(feature, property, literal_eval(value))
234
+
235
+ def load_frame_fields(self, timeline: Timeline) -> None:
236
+ for frame_time, frame in timeline.get_timeline().items():
237
+ # Load all data for the current time
238
+ frame_time_str = frame_time.strftime(self.strftime)
239
+ frame_time_fnames = self.path.rglob(f"*{frame_time_str}.field")
240
+
241
+ for fname in frame_time_fnames:
242
+ ftype = fname.name.split("_")[0]
243
+ # Set the relevant attribute of frame, as mapped using
244
+ # self.field_attributes
245
+ setattr(frame, self.field_attributes[ftype], np.loadtxt(fname))
246
+
247
+ def get_frame_times_from_field_filenames(self) -> list:
248
+ """
249
+ Using a list of all field filenames from a given run of SimpleTrack,
250
+ determine the frame times. Checks whether data is present for all
251
+ field types
252
+
253
+ Returns:
254
+ list: list of all frame times as datetime.datetime object
255
+ """
256
+ # Containing dict for these times
257
+ times_from_each_field_type = {}
258
+
259
+ # Iterate over each field type to find time the data is available for
260
+ for ftype in self.field_attributes:
261
+ # Setup containing array for times
262
+ field_filenames = self.path.rglob(f"{ftype}*.field")
263
+ field_times = []
264
+
265
+ for field_fname in sorted(field_filenames):
266
+ fname_parts = str(field_fname.name).split("_")
267
+ yyyymmdd = fname_parts[1]
268
+ hhmm = fname_parts[2]
269
+ field_times.append(
270
+ datetime.datetime(
271
+ year=int(yyyymmdd[0:4]),
272
+ month=int(yyyymmdd[4:6]),
273
+ day=int(yyyymmdd[6:8]),
274
+ hour=int(hhmm[0:2]),
275
+ minute=int(hhmm[2:4]),
276
+ )
277
+ )
278
+ times_from_each_field_type[ftype] = field_times
279
+
280
+ # Check times from each field type to check consistency
281
+ # If all times are the same (have previously been sorted), should only
282
+ # be one set of unqiue dict values
283
+ times_list = times_from_each_field_type.values()
284
+ times_set = [set(tuple(times)) for times in times_list]
285
+ if len(times_set) == 1:
286
+ # All times are the same, can return any list
287
+ return times_from_each_field_type["features"]
288
+
289
+ else:
290
+ # Get the longest list to return
291
+ max_arr_key = max(
292
+ times_from_each_field_type,
293
+ key=lambda x: len(times_from_each_field_type[x]),
294
+ )
295
+ return times_from_each_field_type[max_arr_key]