Skip to content

Commit c643946

Browse files
committed
added performance plotting
1 parent 492d34a commit c643946

File tree

2 files changed

+363
-1
lines changed

2 files changed

+363
-1
lines changed

doc/articles/first_true_2d.py

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
2+
3+
4+
import os
5+
import sys
6+
import timeit
7+
import typing as tp
8+
from itertools import repeat
9+
10+
from arraykit import first_true_2d as ak_first_true_2d
11+
from arrayredox import first_true_2d as ar_first_true_2d
12+
import arraykit as ak
13+
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
import pandas as pd
17+
18+
sys.path.append(os.getcwd())
19+
20+
21+
22+
class ArrayProcessor:
23+
NAME = ''
24+
SORT = -1
25+
26+
def __init__(self, array: np.ndarray):
27+
self.array = array
28+
29+
#-------------------------------------------------------------------------------
30+
class AKFirstTrueAxis0Forward(ArrayProcessor):
31+
NAME = 'ak.first_true_2d(forward=True, axis=0)'
32+
SORT = 0
33+
34+
def __call__(self):
35+
_ = ak_first_true_2d(self.array, forward=True, axis=0)
36+
37+
class AKFirstTrueAxis1Forward(ArrayProcessor):
38+
NAME = 'ak.first_true_2d(forward=True, axis=1)'
39+
SORT = 1
40+
41+
def __call__(self):
42+
_ = ak_first_true_2d(self.array, forward=True, axis=1)
43+
44+
class AKFirstTrueAxis0Reverse(ArrayProcessor):
45+
NAME = 'ak.first_true_2d(forward=False, axis=0)'
46+
SORT = 2
47+
48+
def __call__(self):
49+
_ = ak_first_true_2d(self.array, forward=False, axis=0)
50+
51+
class AKFirstTrueAxis1Reverse(ArrayProcessor):
52+
NAME = 'ak.first_true_2d(forward=False, axis=1)'
53+
SORT = 3
54+
55+
def __call__(self):
56+
_ = ak_first_true_2d(self.array, forward=False, axis=1)
57+
58+
59+
60+
#-------------------------------------------------------------------------------
61+
class ARFirstTrueAxis0Forward(ArrayProcessor):
62+
NAME = 'ar.first_true_2d(forward=True, axis=0)'
63+
SORT = 10
64+
65+
def __call__(self):
66+
_ = ar_first_true_2d(self.array, forward=True, axis=0)
67+
68+
class ARFirstTrueAxis1Forward(ArrayProcessor):
69+
NAME = 'ar.first_true_2d(forward=True, axis=1)'
70+
SORT = 11
71+
72+
def __call__(self):
73+
_ = ar_first_true_2d(self.array, forward=True, axis=1)
74+
75+
class ARFirstTrueAxis0Reverse(ArrayProcessor):
76+
NAME = 'ar.first_true_2d(forward=False, axis=0)'
77+
SORT = 12
78+
79+
def __call__(self):
80+
_ = ar_first_true_2d(self.array, forward=False, axis=0)
81+
82+
class ARFirstTrueAxis1Reverse(ArrayProcessor):
83+
NAME = 'ar.first_true_2d(forward=False, axis=1)'
84+
SORT = 13
85+
86+
def __call__(self):
87+
_ = ar_first_true_2d(self.array, forward=False, axis=1)
88+
89+
90+
#-------------------------------------------------------------------------------
91+
92+
93+
class NPNonZero(ArrayProcessor):
94+
NAME = 'np.nonzero()'
95+
SORT = 3
96+
97+
def __call__(self):
98+
x, y = np.nonzero(self.array)
99+
# list(zip(x, y)) # simulate iteration
100+
101+
102+
class NPArgMaxAxis0(ArrayProcessor):
103+
NAME = 'np.any(axis=0), np.argmax(axis=0)'
104+
SORT = 4
105+
106+
def __call__(self):
107+
_ = ~np.any(self.array, axis=0)
108+
_ = np.argmax(self.array, axis=0)
109+
110+
class NPArgMaxAxis1(ArrayProcessor):
111+
NAME = 'np.any(axis=1), np.argmax(axis=1)'
112+
SORT = 4
113+
114+
def __call__(self):
115+
_ = ~np.any(self.array, axis=1)
116+
_ = np.argmax(self.array, axis=1)
117+
118+
119+
120+
#-------------------------------------------------------------------------------
121+
NUMBER = 100
122+
123+
def seconds_to_display(seconds: float) -> str:
124+
seconds /= NUMBER
125+
if seconds < 1e-4:
126+
return f'{seconds * 1e6: .1f} (µs)'
127+
if seconds < 1e-1:
128+
return f'{seconds * 1e3: .1f} (ms)'
129+
return f'{seconds: .1f} (s)'
130+
131+
132+
def plot_performance(frame):
133+
fixture_total = len(frame['fixture'].unique())
134+
cat_total = len(frame['size'].unique())
135+
processor_total = len(frame['cls_processor'].unique())
136+
fig, axes = plt.subplots(cat_total, fixture_total)
137+
138+
# cmap = plt.get_cmap('terrain')
139+
cmap = plt.get_cmap('plasma')
140+
141+
color = cmap(np.arange(processor_total) / processor_total)
142+
143+
# category is the size of the array
144+
for cat_count, (cat_label, cat) in enumerate(frame.groupby('size')):
145+
for fixture_count, (fixture_label, fixture) in enumerate(
146+
cat.groupby('fixture')):
147+
ax = axes[cat_count][fixture_count]
148+
149+
# set order
150+
fixture['sort'] = [f.SORT for f in fixture['cls_processor']]
151+
fixture = fixture.sort_values('sort')
152+
153+
results = fixture['time'].values.tolist()
154+
names = [cls.NAME for cls in fixture['cls_processor']]
155+
# x = np.arange(len(results))
156+
names_display = names
157+
post = ax.bar(names_display, results, color=color)
158+
159+
density, position = fixture_label.split('-')
160+
# cat_label is the size of the array
161+
title = f'{cat_label:.0e}\n{FixtureFactory.DENSITY_TO_DISPLAY[density]}\n{FixtureFactory.POSITION_TO_DISPLAY[position]}'
162+
163+
ax.set_title(title, fontsize=6)
164+
ax.set_box_aspect(0.75) # makes taller tan wide
165+
time_max = fixture['time'].max()
166+
ax.set_yticks([0, time_max * 0.5, time_max])
167+
ax.set_yticklabels(['',
168+
seconds_to_display(time_max * .5),
169+
seconds_to_display(time_max),
170+
], fontsize=6)
171+
# ax.set_xticks(x, names_display, rotation='vertical')
172+
ax.tick_params(
173+
axis='x',
174+
which='both',
175+
bottom=False,
176+
top=False,
177+
labelbottom=False,
178+
)
179+
180+
fig.set_size_inches(9, 3.5) # width, height
181+
fig.legend(post, names_display, loc='center right', fontsize=6)
182+
# horizontal, vertical
183+
fig.text(.05, .96, f'ak_first_true_2d() Performance: {NUMBER} Iterations', fontsize=10)
184+
fig.text(.05, .90, get_versions(), fontsize=6)
185+
186+
fp = '/tmp/first_true.png'
187+
plt.subplots_adjust(
188+
left=0.075,
189+
bottom=0.05,
190+
right=0.75,
191+
top=0.85,
192+
wspace=1, # width
193+
hspace=0.1,
194+
)
195+
# plt.rcParams.update({'font.size': 22})
196+
plt.savefig(fp, dpi=300)
197+
198+
if sys.platform.startswith('linux'):
199+
os.system(f'eog {fp}&')
200+
else:
201+
os.system(f'open {fp}')
202+
203+
204+
#-------------------------------------------------------------------------------
205+
206+
class FixtureFactory:
207+
NAME = ''
208+
209+
@staticmethod
210+
def get_array(size: int) -> np.ndarray:
211+
return np.full(size, False, dtype=bool)
212+
213+
def _get_array_filled(
214+
size: int,
215+
start_third: int, # 1 or 2
216+
density: float, # less than 1
217+
) -> np.ndarray:
218+
a = FixtureFactory.get_array(size)
219+
count = size * density
220+
start = int(len(a) * (start_third/3))
221+
length = len(a) - start
222+
step = int(length / count)
223+
fill = np.arange(start, len(a), step)
224+
a[fill] = True
225+
return a
226+
227+
@classmethod
228+
def get_label_array(cls, size: int) -> tp.Tuple[str, np.ndarray]:
229+
array = cls.get_array(size)
230+
return cls.NAME, array
231+
232+
DENSITY_TO_DISPLAY = {
233+
'single': '1 True',
234+
'tenth': '10% True',
235+
'third': '33% True',
236+
}
237+
238+
POSITION_TO_DISPLAY = {
239+
'first_third': 'Fill 1/3 to End',
240+
'second_third': 'Fill 2/3 to End',
241+
}
242+
243+
244+
class FFSingleFirstThird(FixtureFactory):
245+
NAME = 'single-first_third'
246+
247+
@staticmethod
248+
def get_array(size: int) -> np.ndarray:
249+
a = FixtureFactory.get_array(size)
250+
a[int(len(a) * (1/3))] = True
251+
return a
252+
253+
class FFSingleSecondThird(FixtureFactory):
254+
NAME = 'single-second_third'
255+
256+
@staticmethod
257+
def get_array(size: int) -> np.ndarray:
258+
a = FixtureFactory.get_array(size)
259+
a[int(len(a) * (2/3))] = True
260+
return a
261+
262+
263+
class FFTenthPostFirstThird(FixtureFactory):
264+
NAME = 'tenth-first_third'
265+
266+
@classmethod
267+
def get_array(cls, size: int) -> np.ndarray:
268+
return cls._get_array_filled(size, start_third=1, density=.1)
269+
270+
271+
class FFTenthPostSecondThird(FixtureFactory):
272+
NAME = 'tenth-second_third'
273+
274+
@classmethod
275+
def get_array(cls, size: int) -> np.ndarray:
276+
return cls._get_array_filled(size, start_third=2, density=.1)
277+
278+
279+
class FFThirdPostFirstThird(FixtureFactory):
280+
NAME = 'third-first_third'
281+
282+
@classmethod
283+
def get_array(cls, size: int) -> np.ndarray:
284+
return cls._get_array_filled(size, start_third=1, density=1/3)
285+
286+
287+
class FFThirdPostSecondThird(FixtureFactory):
288+
NAME = 'third-second_third'
289+
290+
@classmethod
291+
def get_array(cls, size: int) -> np.ndarray:
292+
return cls._get_array_filled(size, start_third=2, density=1/3)
293+
294+
295+
def get_versions() -> str:
296+
import platform
297+
return f'OS: {platform.system()} / ArrayKit: {ak.__version__} / NumPy: {np.__version__}\n'
298+
299+
300+
CLS_PROCESSOR = (
301+
AKFirstTrueAxis0Forward,
302+
AKFirstTrueAxis1Forward,
303+
AKFirstTrueAxis0Reverse,
304+
AKFirstTrueAxis1Reverse,
305+
306+
ARFirstTrueAxis0Forward,
307+
ARFirstTrueAxis1Forward,
308+
ARFirstTrueAxis0Reverse,
309+
ARFirstTrueAxis1Reverse,
310+
311+
# NPNonZero,
312+
# NPArgMaxAxis0,
313+
# NPArgMaxAxis1
314+
)
315+
316+
CLS_FF = (
317+
FFSingleFirstThird,
318+
FFSingleSecondThird,
319+
FFTenthPostFirstThird,
320+
FFTenthPostSecondThird,
321+
FFThirdPostFirstThird,
322+
FFThirdPostSecondThird,
323+
)
324+
325+
326+
def run_test():
327+
records = []
328+
for size in (100_000, 1_000_000, 10_000_000):
329+
for ff in CLS_FF:
330+
fixture_label, fixture = ff.get_label_array(size)
331+
# TEMP
332+
fixture = fixture.reshape(size // 10, 10)
333+
334+
for cls in CLS_PROCESSOR:
335+
runner = cls(fixture)
336+
337+
record = [cls, NUMBER, fixture_label, size]
338+
print(record)
339+
try:
340+
result = timeit.timeit(
341+
f'runner()',
342+
globals=locals(),
343+
number=NUMBER)
344+
except OSError:
345+
result = np.nan
346+
finally:
347+
pass
348+
record.append(result)
349+
records.append(record)
350+
351+
f = pd.DataFrame.from_records(records,
352+
columns=('cls_processor', 'number', 'fixture', 'size', 'time')
353+
)
354+
print(f)
355+
plot_performance(f)
356+
357+
if __name__ == '__main__':
358+
359+
run_test()
360+
361+
362+

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ pub fn first_true_2d<'py>(
400400
let prepped = prepare_array_for_axis(py, array, axis)?;
401401
let view = unsafe { prepped.as_array() };
402402

403+
// let view = array.as_array();
403404
// NOTE: these are rows in the view, not always the same as rows
404405
let rows = view.nrows();
405406
let mut result = Vec::with_capacity(rows);
@@ -408,7 +409,6 @@ pub fn first_true_2d<'py>(
408409
const LANES: usize = 32;
409410
let ones = u8x32::splat(1);
410411

411-
412412
for row in 0..rows {
413413
let mut found = -1;
414414
let row_slice = &view.row(row);

0 commit comments

Comments
 (0)