Skip to content

Commit 9d4a97b

Browse files
committed
Merge branch 'mac_num_op_testing' into develop
potential fix for MacOS numerical discrepancy
2 parents 5724f31 + 7bc5f2e commit 9d4a97b

15 files changed

Lines changed: 261 additions & 25 deletions

File tree

.github/workflows/build-test-base.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ jobs:
77
test:
88
strategy:
99
matrix:
10-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
10+
# 3.13.5 specified due to Windows python linkage issue
11+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.5"]
1112
platform: [ubuntu-latest, macos-latest, windows-latest]
1213

1314
runs-on: ${{matrix.platform}}

.github/workflows/wheels.yml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,23 @@ on: [workflow_dispatch]
44

55
jobs:
66
build_wheels:
7-
name: Build wheels on ${{ matrix.os }}
8-
runs-on: ${{ matrix.os }}
7+
name: Build wheels for ${{ matrix.os }}
8+
runs-on: ${{ matrix.runs-on }}
99
strategy:
1010
matrix:
11-
os: [ubuntu-22.04, windows-2022, macos-12]
11+
include:
12+
- os: linux-intel
13+
runs-on: ubuntu-latest
14+
- os: linux-arm
15+
runs-on: ubuntu-24.04-arm
16+
- os: windows-intel
17+
runs-on: windows-latest
18+
- os: macos-intel
19+
# macos-13 was the last x86_64 runner
20+
runs-on: macos-13
21+
- os: macos-arm
22+
# macos-14+ (including latest) are ARM64 runners
23+
runs-on: macos-latest
1224

1325
steps:
1426
- name: Checkout
@@ -22,13 +34,13 @@ jobs:
2234
python-version: 3.11
2335

2436
- name: Install cibuildwheel
25-
run: python -m pip install cibuildwheel==2.15.0
37+
run: python -m pip install cibuildwheel==2.22.0
2638

2739
- name: Build wheels
2840
env:
2941
CIBW_SKIP: pp* *-musllinux_* *-manylinux_i686 *-win32 # skip PyPy, musllinux, 32-bit Linux & win32 builds
30-
CIBW_ARCHS_MACOS: x86_64 arm64
31-
CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-*
42+
CIBW_ARCHS_MACOS: auto
43+
CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-*
3244
run: python -m cibuildwheel --output-dir wheelhouse
3345

3446
- name: Store artifacts

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# User-specific stuff:
22
.idea
33

4+
# OS-specific files
5+
.DS_Store
6+
47
# Compiled Python & C extensions
58
*.pyc
69
*.so

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ transformation, and gating tasks commonly used within the flow community.
1717
FlowUtils is part of a suite of Python libraries for analyzing flow
1818
cytometry data. It was developed as an extension to the light-weight
1919
[FlowIO library](https://github.com/whitews/FlowIO). FlowIO reads and
20-
writes Flow Cytometry Standard (FCS) files, and has zero dependencies.
20+
writes Flow Cytometry Standard (FCS) files, and has minimal dependencies.
2121
For higher level interaction with flow cytometry data, including GatingML
2222
& FlowJo 10 support, see the related
2323
[FlowKit project](https://github.com/whitews/FlowKit).

setup.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,24 @@
1515
with open("README.md", "r") as fh:
1616
long_description = fh.read()
1717

18-
18+
# NOTE ON C EXTENSIONS
19+
# On MacOS, clang 14 and later introduced changes to
20+
# Fused Multiply-Add (FMA). Prior to version 14, clang
21+
# was less likely to use FMA. clang 14 began using FMA by
22+
# default, resulting in numerical differences in some
23+
# expressions. Specifically, the is_left() function in
24+
# gate_helpers.c was affected and resulted in failed tests.
25+
# To enforce consistency across platforms the compile
26+
# flag '-ffp-contract=off' was added to disable FMA.
27+
# This fixed the discrepancies on Mac OS.
1928
logicle_extension = Extension(
2029
'flowutils.logicle_c',
2130
sources=[
2231
'src/flowutils/logicle_c_ext/_logicle.c',
2332
'src/flowutils/logicle_c_ext/logicle.c'
2433
],
2534
include_dirs=[np.get_include(), 'src/flowutils/logicle_c_ext'],
26-
extra_compile_args=['-std=c99', '-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION']
35+
extra_compile_args=['-std=c99', '-ffp-contract=off']
2736
)
2837

2938
gating_extension = Extension(
@@ -33,7 +42,7 @@
3342
'src/flowutils/gating_c_ext/gate_helpers.c'
3443
],
3544
include_dirs=[np.get_include(), 'src/flowutils/gating_c_ext'],
36-
extra_compile_args=['-std=c99', '-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION']
45+
extra_compile_args=['-std=c99', '-ffp-contract=off']
3746
)
3847

3948
setup(

src/flowutils/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""
22
FlowUtils version
33
"""
4-
__version__ = "1.2.0b0"
4+
__version__ = "1.2.2b0"

src/flowutils/gating_c_ext/_gate_helpers.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#include <Python.h>
2-
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION // to avoid a warning
32
#include <numpy/arrayobject.h>
43
#include "gate_helpers.h"
54

src/flowutils/gating_c_ext/gate_helpers.c

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <stdlib.h>
2+
#include <stdio.h>
23
#include "gate_helpers.h"
34

45
double point_is_left(
@@ -9,22 +10,31 @@ double point_is_left(
910
double test_point_x,
1011
double test_point_y
1112
) {
12-
double is_left = (point_b_x - point_a_x) * (test_point_y - point_a_y) -
13-
(test_point_x - point_a_x) * (point_b_y - point_a_y);
13+
// double bx_ax_dist = point_b_x - point_a_x;
14+
// double tpy_ay_dist = test_point_y - point_a_y;
15+
// double tpx_ax_dist = test_point_x - point_a_x;
16+
// double by_ay_dist = point_b_y - point_a_y;
17+
// double mult1 = bx_ax_dist * tpy_ay_dist;
18+
// double mult2 = tpx_ax_dist * by_ay_dist;
19+
//
20+
// double is_left = mult1 - mult2;
21+
22+
double is_left = ((point_b_x - point_a_x) * (test_point_y - point_a_y)) -
23+
((test_point_x - point_a_x) * (point_b_y - point_a_y));
1424
return is_left;
1525
}
1626

1727
int calc_wind_count(double point_x, double point_y, int vert_count, double *poly_vertices) {
18-
int wind_count = 0;
19-
double vert_a_x;
20-
double vert_a_y;
21-
double vert_b_x;
22-
double vert_b_y;
23-
double is_left;
28+
int wind_count = 0;
29+
double vert_a_x;
30+
double vert_a_y;
31+
double vert_b_x;
32+
double vert_b_y;
33+
double is_left;
2434

2535
// loop through all edges of the polygon
2636
for (int i=0; i<vert_count; i++) {
27-
//edge from poly_vertices[i] to poly_vertices[i+1]
37+
// edge from poly_vertices[i] to poly_vertices[i+1]
2838
vert_a_x = poly_vertices[(i * 2) + 0];
2939
vert_a_y = poly_vertices[(i * 2) + 1];
3040

@@ -40,6 +50,8 @@ int calc_wind_count(double point_x, double point_y, int vert_count, double *poly
4050
if (point_y < vert_b_y) {
4151
// point crosses & edge travels upward
4252
is_left = point_is_left(vert_a_x, vert_a_y, vert_b_x, vert_b_y, point_x, point_y);
53+
// printf("if is_left: %.14f\n", is_left);
54+
4355
if (is_left > 0) {
4456
// point is left of edge
4557
wind_count += 1; // valid 'up' intersection
@@ -49,6 +61,21 @@ int calc_wind_count(double point_x, double point_y, int vert_count, double *poly
4961
if (vert_b_y <= point_y) {
5062
// point crosses & edge travels downward
5163
is_left = point_is_left(vert_a_x, vert_a_y, vert_b_x, vert_b_y, point_x, point_y);
64+
// printf("vert_a_x: %.15f\n", vert_a_x);
65+
// printf("vert_a_y: %.15f\n", vert_a_y);
66+
// printf("vert_b_x: %.15f\n", vert_b_x);
67+
// printf("vert_b_y: %.15f\n", vert_b_y);
68+
// printf("point_x: %.15f\n", point_x);
69+
// printf("point_y: %.15f\n", point_y);
70+
71+
// (point_b_x - point_a_x) * (test_point_y - point_a_y) -
72+
// (test_point_x - point_a_x) * (point_b_y - point_a_y)
73+
// printf("vert_b_x - vert_a_x: %.15f\n", vert_b_x - vert_a_x);
74+
// printf("point_y - vert_a_y: %.15f\n", point_y - vert_a_y);
75+
// printf("point_x - vert_a_x: %.15f\n", point_x - vert_a_x);
76+
// printf("vert_b_y - vert_a_y: %.15f\n", vert_b_y - vert_a_y);
77+
//
78+
// printf("else is_left: %.15f\n", is_left);
5279

5380
if (is_left < 0) {
5481
// point is right of edge

src/flowutils/gating_py.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
Temporary implementation of points_in_polygon()
3+
to troubleshoot discrepancies in latest Mac OS
4+
versions where some test events are incorrectly
5+
labelled. This will be removed after a solution
6+
is found but will remain in the repo in the
7+
'mac_num_op_testing' branch in case the issue
8+
occurs again in the future.
9+
"""
10+
11+
def point_is_left(
12+
point_a_x,
13+
point_a_y,
14+
point_b_x,
15+
point_b_y,
16+
test_point_x,
17+
test_point_y
18+
):
19+
is_left = (point_b_x - point_a_x) * (test_point_y - point_a_y) - \
20+
(test_point_x - point_a_x) * (point_b_y - point_a_y)
21+
return is_left
22+
23+
24+
def calc_wind_count(point_x, point_y, vert_count, poly_vertices):
25+
wind_count = 0
26+
27+
# loop through all edges of the polygon
28+
for i, (vert_a_x, vert_a_y) in enumerate(poly_vertices):
29+
if i >= vert_count - 1:
30+
vert_b_x = poly_vertices[0][0]
31+
vert_b_y = poly_vertices[0][1]
32+
else:
33+
vert_b_x = poly_vertices[i + 1][0]
34+
vert_b_y = poly_vertices[i + 1][1]
35+
36+
if vert_a_y <= point_y:
37+
if point_y < vert_b_y:
38+
# point crosses & edge travels upward
39+
is_left = point_is_left(vert_a_x, vert_a_y, vert_b_x, vert_b_y, point_x, point_y)
40+
# print("if is_left: %.14f" % is_left)
41+
42+
if is_left > 0:
43+
# point is left of edge
44+
wind_count += 1 # valid 'up' intersection
45+
else:
46+
if vert_b_y <= point_y:
47+
# point crosses & edge travels downward
48+
is_left = point_is_left(vert_a_x, vert_a_y, vert_b_x, vert_b_y, point_x, point_y)
49+
# print("vert_a_x: %.14f" % vert_a_x)
50+
# print("vert_a_y: %.14f" % vert_a_y)
51+
# print("vert_b_x: %.14f" % vert_b_x)
52+
# print("vert_b_y: %.14f" % vert_b_y)
53+
# print("point_x: %.14f" % point_x)
54+
# print("point_y: %.14f" % point_y)
55+
# print("else is_left: %.14f" % is_left)
56+
57+
if is_left < 0:
58+
# point is right of edge
59+
wind_count -= 1 # valid 'down' intersect
60+
61+
return wind_count
62+
63+
64+
def points_in_polygon(poly_vertices, points):
65+
"""
66+
Determines whether points in an array are inside a polygon. Points on the
67+
edge of the polygon are considered inclusive. This function uses the
68+
winding number method and is robust to complex polygons with crossing
69+
boundaries, including the presence of 'holes' created by boundary crosses.
70+
71+
This implementation is based on the C implementation by Dan Sunday.
72+
Original copyright notice:
73+
Copyright 2000 softSurfer, 2012 Dan Sunday
74+
75+
The website containing the above implementation is no longer available,
76+
but was archived by the Wayback Machine. The last archived version is
77+
available here:
78+
79+
https://web.archive.org/web/20210504233957/
80+
81+
:param poly_vertices: Polygon vertices (array of 2-D points)
82+
:param points: Points to test for polygon inclusion
83+
:return: Array of winding counts for each point. True is inside polygon.
84+
"""
85+
# First, find the polygon's bounding box & store the min/max values
86+
min_x = poly_vertices[0][0]
87+
max_x = poly_vertices[0][0]
88+
min_y = poly_vertices[0][1]
89+
max_y = poly_vertices[0][1]
90+
91+
for i, (vert_x, vert_y) in enumerate(poly_vertices):
92+
if vert_x < min_x:
93+
min_x = vert_x
94+
elif vert_x > max_x:
95+
max_x = vert_x
96+
97+
if vert_y < min_y:
98+
min_y = vert_y
99+
elif vert_y > max_y:
100+
max_y = vert_y
101+
102+
wind_counts = []
103+
104+
for i, (point_x, point_y) in enumerate(points):
105+
106+
if point_x < min_x or point_x > max_x or point_y < min_y or point_y > max_y:
107+
wind_count = 0
108+
else:
109+
wind_count = calc_wind_count(
110+
point_x,
111+
point_y,
112+
len(poly_vertices),
113+
poly_vertices
114+
)
115+
116+
wind_counts.append(wind_count)
117+
118+
return wind_counts

src/flowutils/logicle_c_ext/_logicle.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#include <Python.h>
2-
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION // to avoid a warning
32
#include <numpy/arrayobject.h>
43
#include "logicle.h"
54

0 commit comments

Comments
 (0)