water-column-sonar-annotation 26.1.8__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.
Files changed (27) hide show
  1. tests/__init__.py +0 -0
  2. tests/astronomical/__init__.py +0 -0
  3. tests/astronomical/test_astronomical_manager.py +148 -0
  4. tests/conftest.py +60 -0
  5. tests/cruise/__init__.py +0 -0
  6. tests/cruise/test_cruise_manager.py +80 -0
  7. tests/geospatial/__init__.py +0 -0
  8. tests/geospatial/test_geospatial_manager.py +86 -0
  9. tests/record/test_echoview_record_manager.py +160 -0
  10. water_column_sonar_annotation/__init__.py +5 -0
  11. water_column_sonar_annotation/astronomical/__init__.py +5 -0
  12. water_column_sonar_annotation/astronomical/astronomical_manager.py +82 -0
  13. water_column_sonar_annotation/cruise/__init__.py +5 -0
  14. water_column_sonar_annotation/cruise/cruise_manager.py +104 -0
  15. water_column_sonar_annotation/geospatial/__init__.py +5 -0
  16. water_column_sonar_annotation/geospatial/geospatial_manager.py +143 -0
  17. water_column_sonar_annotation/record/__init__.py +9 -0
  18. water_column_sonar_annotation/record/echoview_record_manager.py +426 -0
  19. water_column_sonar_annotation/record/graph_record_manager.py +82 -0
  20. water_column_sonar_annotation/record/parquet_record_manager.py +83 -0
  21. water_column_sonar_annotation/shape/__init__.py +5 -0
  22. water_column_sonar_annotation/shape/shape_manager.py +29 -0
  23. water_column_sonar_annotation-26.1.8.dist-info/METADATA +109 -0
  24. water_column_sonar_annotation-26.1.8.dist-info/RECORD +27 -0
  25. water_column_sonar_annotation-26.1.8.dist-info/WHEEL +5 -0
  26. water_column_sonar_annotation-26.1.8.dist-info/licenses/LICENSE +21 -0
  27. water_column_sonar_annotation-26.1.8.dist-info/top_level.txt +2 -0
tests/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,148 @@
1
+ import numpy as np
2
+
3
+ from water_column_sonar_annotation.astronomical import AstronomicalManager
4
+
5
+
6
+ #######################################################
7
+ def setup_module():
8
+ print("setup")
9
+
10
+
11
+ def teardown_module():
12
+ print("teardown")
13
+
14
+
15
+ def test_get_solar_azimuth():
16
+ astronomical_manager = AstronomicalManager()
17
+ # https://www.suncalc.org/#/39.9812,-105.2495,13/2026.01.26/11:52/1/3
18
+ azimuth_noon = astronomical_manager.get_solar_azimuth(
19
+ iso_time="2026-01-26T19:00:00Z", # noon
20
+ latitude=39.9674884, # Boulder
21
+ longitude=-105.2532602,
22
+ )
23
+ assert np.isclose(azimuth_noon, 31.38) # 15)
24
+
25
+ azimuth_sunset = astronomical_manager.get_solar_azimuth(
26
+ iso_time="2026-01-26T00:00:00Z", # sunset
27
+ latitude=39.9674884,
28
+ longitude=-105.2532602,
29
+ )
30
+ assert np.isclose(azimuth_sunset, 1.25) # 27)
31
+
32
+
33
+ def test_get_solar_azimuth_boulder_2pm():
34
+ # 2026-01-29 @2pm is UTC: "2026-01-29T21:02:23Z"
35
+ astronomical_manager = AstronomicalManager()
36
+ # https://www.suncalc.org/#/39.9812,-105.2495,13/2026.01.26/11:52/1/3
37
+ azimuth_noon = astronomical_manager.get_solar_azimuth(
38
+ iso_time="2026-01-29T21:02:23Z", # 2pm
39
+ latitude=39.9674884, # Boulder
40
+ longitude=-105.2532602,
41
+ )
42
+ assert np.isclose(azimuth_noon, 27.01)
43
+
44
+
45
+ ### PHASE OF DAY ###
46
+ # { 'dawn': 1, 'day': 2, 'dusk': 3, 'night': 4 }
47
+ def test_phase_of_day_at_noon():
48
+ astronomical_manager = AstronomicalManager()
49
+ phase = astronomical_manager.phase_of_day(
50
+ iso_time="2026-01-27T19:00:00Z", # noon
51
+ latitude=39.9674884, # Boulder
52
+ longitude=-105.2532602,
53
+ )
54
+ assert phase == 2
55
+
56
+
57
+ def test_phase_of_day_at_midnight():
58
+ astronomical_manager = AstronomicalManager()
59
+ phase = astronomical_manager.phase_of_day(
60
+ iso_time="2026-01-28T07:00:00Z",
61
+ latitude=39.9674884, # Boulder
62
+ longitude=-105.2532602,
63
+ )
64
+ assert phase == 4
65
+
66
+
67
+ def test_phase_of_day_before_sunset():
68
+ astronomical_manager = AstronomicalManager()
69
+ phase = astronomical_manager.phase_of_day(
70
+ # sunset is at 5:13pm on jan 27th, per https://psl.noaa.gov/boulder/boulder.sunset.html
71
+ iso_time="2026-01-28T00:09:00Z", # sunset @ 5:13pm, nautical sunset @6:16pm
72
+ latitude=39.9674884, # Boulder
73
+ longitude=-105.2532602,
74
+ )
75
+ assert phase == 2 # day
76
+
77
+
78
+ def test_phase_of_day_after_sunset():
79
+ astronomical_manager = AstronomicalManager()
80
+ phase = astronomical_manager.phase_of_day(
81
+ # sunset is at 5:13pm on jan 27th, per https://psl.noaa.gov/boulder/boulder.sunset.html
82
+ iso_time="2026-01-28T00:10:00Z", # sunset @ 5:13pm, nautical sunset @6:16pm
83
+ latitude=39.9674884, # Boulder
84
+ longitude=-105.2532602,
85
+ )
86
+ assert phase == 3 # dusk
87
+
88
+
89
+ def test_phase_of_day_before_nautical_sunset():
90
+ astronomical_manager = AstronomicalManager()
91
+ # an hour'ish later is nautical sunset
92
+ phase_before_nautical_sunset = astronomical_manager.phase_of_day(
93
+ iso_time="2026-01-28T01:15:00Z", # sunset @5:13pm, nautical sunset @6:16pm
94
+ latitude=39.9674884, # Boulder
95
+ longitude=-105.2532602,
96
+ )
97
+ assert phase_before_nautical_sunset == 3 # dusk
98
+
99
+
100
+ def test_phase_of_day_after_nautical_sunset():
101
+ astronomical_manager = AstronomicalManager()
102
+ phase_after_nautical_sunset = astronomical_manager.phase_of_day(
103
+ iso_time="2026-01-28T01:17:00Z", # sunset @5:13pm, nautical sunset @6:16pm
104
+ latitude=39.9674884, # Boulder
105
+ longitude=-105.2532602,
106
+ )
107
+ assert phase_after_nautical_sunset == 4 # night
108
+
109
+
110
+ def test_phase_of_day_before_sunrise():
111
+ astronomical_manager = AstronomicalManager()
112
+ phase_at_sunrise = astronomical_manager.phase_of_day(
113
+ iso_time="2026-01-27T14:13:00Z", # sunrise @7:13am, nautical sunrise @6:12am
114
+ latitude=39.9674884, # Boulder
115
+ longitude=-105.2532602,
116
+ )
117
+ assert phase_at_sunrise == 1 # dusk
118
+
119
+
120
+ def test_phase_of_day_after_sunrise():
121
+ astronomical_manager = AstronomicalManager()
122
+ phase_at_sunrise = astronomical_manager.phase_of_day(
123
+ iso_time="2026-01-27T14:20:00Z", # sunrise @7:13am, nautical sunrise @6:12am
124
+ latitude=39.9674884, # Boulder
125
+ longitude=-105.2532602,
126
+ )
127
+ assert phase_at_sunrise == 2 # day
128
+
129
+
130
+ def test_phase_of_day_before_nautical_sunrise():
131
+ astronomical_manager = AstronomicalManager()
132
+ # about an hour before is nautical sunrise
133
+ phase_before_nautical_sunrise = astronomical_manager.phase_of_day(
134
+ iso_time="2026-01-27T13:12:00Z", # sunrise @7:13am, nautical sunrise @6:12am
135
+ latitude=39.9674884, # Boulder
136
+ longitude=-105.2532602,
137
+ )
138
+ assert phase_before_nautical_sunrise == 4 # night
139
+
140
+
141
+ def test_phase_of_day_after_nautical_sunrise():
142
+ astronomical_manager = AstronomicalManager()
143
+ phase = astronomical_manager.phase_of_day(
144
+ iso_time="2026-01-27T14:13:00Z", # sunrise @7:13am, nautical sunrise @6:12am
145
+ latitude=39.9674884, # Boulder
146
+ longitude=-105.2532602,
147
+ )
148
+ assert phase == 1
tests/conftest.py ADDED
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+
3
+ import pooch
4
+ import pytest
5
+
6
+ HERE = Path(__file__).parent.absolute()
7
+ TEST_DATA_FOLDER = HERE / "test_resources"
8
+
9
+ HB1906_DATA = pooch.create(
10
+ path=pooch.os_cache("water-column-sonar-annotation"),
11
+ base_url="https://github.com/CI-CMG/water-column-sonar-annotation/releases/download/v26.1.0/",
12
+ retry_if_failed=1,
13
+ registry={
14
+ "HB201906_BOTTOMS.zip": "sha256:20609581493ea3326c1084b6868e02aafbb6c0eae871d946f30b8b5f0e7ba059",
15
+ "HB201906_EVR.zip": "sha256:ceed912a25301be8f1b8f91e134d0ca4cff717f52b6623a58677832fd60c2990",
16
+ #
17
+ # "ne_50m_coastline.shp": "sha256:797d675af9613f80b51ab6049fa32e589974d7a97c6497ca56772965f179ed26",
18
+ # "ne_50m_coastline.shx": "sha256:0ff1792f2d16b58246d074215edd9d12fa280880ecaad61a91b9382fee854065",
19
+ #
20
+ "ne_10m_coastline.shp": "sha256:459a4a97c09db19aadf5244026612de9d43748be27f83a360242b99f7fabb3c1",
21
+ "ne_10m_coastline.shx": "sha256:f873afee7f56779ce52253f740ec251c2f12244aea911dc40f0a85d75de8d5f2",
22
+ },
23
+ )
24
+
25
+
26
+ def fetch_raw_files():
27
+ HB1906_DATA.fetch(fname="HB201906_BOTTOMS.zip", progressbar=True)
28
+ HB1906_DATA.fetch(fname="HB201906_EVR.zip", progressbar=True)
29
+
30
+ # HB1906_DATA.fetch(fname="ne_50m_coastline.shp", progressbar=True)
31
+ # HB1906_DATA.fetch(fname="ne_50m_coastline.shx", progressbar=True)
32
+
33
+ HB1906_DATA.fetch(fname="ne_10m_coastline.shp", progressbar=True)
34
+ HB1906_DATA.fetch(fname="ne_10m_coastline.shx", progressbar=True)
35
+
36
+ file_name = HB1906_DATA.fetch(fname="HB201906_EVR.zip", progressbar=True)
37
+
38
+ """
39
+ water-column-sonar-annotation user$ ls /Users/user/Library/Caches/water-column-sonar-annotation
40
+ HB201906_BOTTOMS.zip HB201906_EVR.zip ne_10m_coastline.shp ne_10m_coastline.shx
41
+ """
42
+ return Path(file_name).parent
43
+
44
+
45
+ @pytest.fixture(scope="session")
46
+ def test_path():
47
+ return {
48
+ "DATA_TEST_PATH": fetch_raw_files(),
49
+ }
50
+
51
+
52
+ # """
53
+ # Folder locations in mac and windows:
54
+ #
55
+ # Windows
56
+ # C:\Users\<user>\AppData\Local\echopype\Cache\2024.12.23.10.10
57
+ #
58
+ # MacOS
59
+ # /Users//Library/Caches/echopype/2024.12.23.10.10
60
+ # """
File without changes
@@ -0,0 +1,80 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from water_column_sonar_annotation.cruise import CruiseManager
5
+
6
+
7
+ #######################################################
8
+ def setup_module():
9
+ print("setup")
10
+
11
+
12
+ def teardown_module():
13
+ print("teardown")
14
+
15
+
16
+ @pytest.fixture
17
+ def process_cruise_path(test_path):
18
+ return test_path["DATA_TEST_PATH"]
19
+
20
+
21
+ #######################################################
22
+ # def test_get_cruise_bottom_nan(process_cruise_path, tmp_path):
23
+ # cruise_manager = CruiseManager()
24
+ # cruise = cruise_manager.get_cruise()
25
+ # # count of non-nan values np.count_nonzero(~np.isnan(cruise.bottom.values)) / cruise.Sv.shape[1]
26
+ # assert len(cruise.Sv.shape) == 3
27
+
28
+
29
+ def test_get_cruise(process_cruise_path, tmp_path):
30
+ cruise_manager = CruiseManager()
31
+ cruise = cruise_manager.get_cruise()
32
+ assert len(cruise.Sv.shape) == 3
33
+
34
+
35
+ def test_get_coordinates():
36
+ """This only gets the depth over the interval, need to calculate the 'altitude'"""
37
+ cruise_manager = CruiseManager()
38
+ lat, lon = cruise_manager.get_coordinates(
39
+ start_time="2019-10-16T16:20:00",
40
+ end_time="2019-10-16T16:30:00",
41
+ )
42
+ assert np.isclose(lat, 41.48177)
43
+ assert np.isclose(lon, -68.50478)
44
+
45
+
46
+ def test_get_depth():
47
+ """This only gets the depth over the interval, need to calculate the 'altitude'"""
48
+ cruise_manager = CruiseManager()
49
+ depth_value = cruise_manager.get_depth(
50
+ start_time="2019-10-16T16:20:00",
51
+ end_time="2019-10-16T16:50:00",
52
+ )
53
+ assert np.isclose(depth_value, 96.36) # 96.356674
54
+
55
+
56
+ def test_get_altitude():
57
+ """This gets the distance from EVR to the bottom"""
58
+ cruise_manager = CruiseManager()
59
+ altitude_value = cruise_manager.get_altitude(
60
+ start_time="2019-10-16T16:20:00",
61
+ end_time="2019-10-16T16:50:00",
62
+ bbox_max=80.0,
63
+ )
64
+ # bottom is at 96.356674
65
+ # setting the bbox at 80.
66
+ assert np.isclose(altitude_value, 16.36) # 96.356674
67
+
68
+
69
+ def test_get_altitude_nan_bottom():
70
+ """This gets the distance from EVR to the bottom when there are nan values"""
71
+ cruise_manager = CruiseManager()
72
+ altitude_value = cruise_manager.get_altitude(
73
+ start_time="2019-09-26T10:02:39.03",
74
+ end_time="2019-09-26T10:02:40.00",
75
+ bbox_max=10.0,
76
+ )
77
+ assert np.isclose(altitude_value, 0.0)
78
+
79
+
80
+ # get_gps
File without changes
@@ -0,0 +1,86 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from water_column_sonar_annotation.geospatial import GeospatialManager
5
+
6
+
7
+ #######################################################
8
+ def setup_module():
9
+ print("setup")
10
+
11
+
12
+ def teardown_module():
13
+ print("teardown")
14
+
15
+
16
+ @pytest.fixture
17
+ def process_check_distance_from_coastline(test_path):
18
+ return test_path["DATA_TEST_PATH"]
19
+
20
+
21
+ #######################################################
22
+ def test_check_distance_from_coastline(process_check_distance_from_coastline, tmp_path):
23
+ geospatial_manager = GeospatialManager()
24
+ # Point in middle of atlantic https://wktmap.com/?ab28cbae
25
+ distance = geospatial_manager.check_distance_from_coastline(
26
+ latitude=51.508742,
27
+ longitude=-30.410156,
28
+ shapefile_path=process_check_distance_from_coastline,
29
+ )
30
+ # assert np.isclose(distance, 1_236_212.37356) # 1,200 km
31
+ assert np.isclose(distance, 1_233_910.720702243)
32
+
33
+
34
+ def test_check_distance_from_coastline_woods_hole(
35
+ process_check_distance_from_coastline, tmp_path
36
+ ):
37
+ geospatial_manager = GeospatialManager()
38
+ # Point in middle of woods hole vineyard sound: https://wktmap.com/?9b405aa9
39
+ distance = geospatial_manager.check_distance_from_coastline(
40
+ latitude=41.494692,
41
+ longitude=-70.647926,
42
+ shapefile_path=process_check_distance_from_coastline,
43
+ )
44
+ # The sound is 5 km across
45
+ # assert np.isclose(distance, 4_457.0347) # 4.5 km --> should be 2.5 km?
46
+ assert np.isclose(distance, 3_093) # 3_093.3015 km is close enough
47
+
48
+
49
+ def test_get_local_time():
50
+ geospatial_manager = GeospatialManager()
51
+ local_time = geospatial_manager.get_local_time(
52
+ iso_time="2026-01-26T20:35:00Z",
53
+ latitude=51.508742,
54
+ longitude=-30.410156,
55
+ )
56
+ assert local_time == "2026-01-26T18:35:00-02:00"
57
+
58
+
59
+ def test_get_local_hour_of_day():
60
+ geospatial_manager = GeospatialManager()
61
+ local_hour_of_day = geospatial_manager.get_local_hour_of_day(
62
+ iso_time="2026-01-26T20:35:00Z",
63
+ latitude=51.508742,
64
+ longitude=-30.410156,
65
+ )
66
+ assert local_hour_of_day == 18
67
+
68
+
69
+ def test_get_month_of_year_january():
70
+ geospatial_manager = GeospatialManager()
71
+ month_of_year = geospatial_manager.get_month_of_year(
72
+ iso_time="2026-01-26T20:35:00Z",
73
+ latitude=51.508742,
74
+ longitude=-30.410156,
75
+ )
76
+ assert month_of_year == 1
77
+
78
+
79
+ def test_get_month_of_year_july():
80
+ geospatial_manager = GeospatialManager()
81
+ month_of_year = geospatial_manager.get_month_of_year(
82
+ iso_time="2026-07-13T12:00:00Z",
83
+ latitude=51.508742,
84
+ longitude=-30.410156,
85
+ )
86
+ assert month_of_year == 7
@@ -0,0 +1,160 @@
1
+ import pytest
2
+
3
+ from water_column_sonar_annotation.record import EchoviewRecordManager
4
+
5
+
6
+ #######################################################
7
+ def setup_module():
8
+ print("setup")
9
+
10
+
11
+ def teardown_module():
12
+ print("teardown")
13
+
14
+
15
+ @pytest.fixture
16
+ def process_evr_file_path(test_path):
17
+ return test_path["DATA_TEST_PATH"]
18
+
19
+
20
+ #######################################################
21
+ # @pytest.mark.skip(reason="todo implement this")
22
+ # def test_process_evr_file(process_evr_file_path, tmp_path):
23
+ # echoview_record_manager = EchoviewRecordManager()
24
+ # echoview_record_manager.process_point_data()
25
+ # assert True
26
+
27
+ record_header = """EVRG 7 11.0.244.39215
28
+ 11"""
29
+
30
+ possible_herring_example = """13 4 7 0 3 -1 1 20190925 2247242130 24.1290795746 20190925 2247362460 35.1668500183
31
+ 0
32
+ 0
33
+ possible_herring
34
+ 20190925 2247242130 24.1290795746 20190925 2247242130 35.1668500183 20190925 2247362460 35.1668500183 20190925 2247362460 24.1290795746 1
35
+ 100"""
36
+
37
+ fish_school_example = """13 30 8 0 7 -1 1 20190925 1749451605 20.2800268439 20190925 1749501645 26.3028953087
38
+ 0
39
+ 10
40
+ School detected with:
41
+ Minimum data threshold: -66.00
42
+ Maximum data threshold: (none)
43
+ Distance mode: GPS distance
44
+ Minimum total school height (meters): 4.00
45
+ Minimum candidate length (meters): 1.00
46
+ Minimum candidate height (meters): 2.00
47
+ Maximum vertical linking distance (meters): 2.00
48
+ Maximum horizontal linking distance (meters): 20.00
49
+ Minimum total school length (meters): 4.00
50
+ fish_school
51
+ 20190925 1749451605 22.0286015595 20190925 1749451605 25.1371788317 20190925 1749461355 25.1371788317 20190925 1749461605 25.1420359837 20190925 1749461605 25.7200370702 20190925 1749471370 25.7200370702 20190925 1749471620 25.7248942222 20190925 1749471620 26.1086092292 20190925 1749481385 26.1086092292 20190925 1749481635 26.1134663812 20190925 1749481635 26.3028953087 20190925 1749491640 26.3028953087 20190925 1749491640 25.1371788317 20190925 1749501645 25.1371788317 20190925 1749501645 24.5543205932 20190925 1749491640 24.5543205932 20190925 1749491640 24.3600345136 20190925 1749501645 24.3600345136 20190925 1749501645 21.6400294005 20190925 1749491640 21.6400294005 20190925 1749491640 21.0571711620 20190925 1749481635 21.0571711620 20190925 1749481635 20.2800268439 20190925 1749471620 20.2800268439 20190925 1749471620 20.8580279305 20190925 1749471370 20.8628850825 20190925 1749461605 20.8628850825 20190925 1749461605 21.8294583280 20190925 1749461355 21.8343154800 20190925 1749451605 21.8343154800 1
52
+ Region 8"""
53
+
54
+ unclassified_regions_example = """13 12 1 0 2 -1 1 20190925 2053458953 9.2818 20190925 2054119318 11.5333
55
+ 0
56
+ 0
57
+ Unclassified regions
58
+ 20190925 2053458953 9.6034489515 20190925 2053521545 11.1197829964 20190925 2054046730 11.5333286451 20190925 2054064248 11.5333286451 20190925 2054079263 11.4414296120 20190925 2054116810 10.8440858974 20190925 2054119318 10.4764897652 20190925 2054116810 9.6953479845 20190925 2054111800 9.4196508854 20190925 2054091775 9.2818023359 20190925 2054076760 9.2818023359 20190925 2053483995 9.5574994350 0
59
+ 100"""
60
+
61
+ krill_schools_example = """13 23 49 0 7 -1 1 20191020 0536541560 13.0000000000 20191020 0536591655 17.0000000000
62
+ 0
63
+ 10
64
+ School detected with:
65
+ Minimum data threshold: -80.00
66
+ Maximum data threshold: (none)
67
+ Distance mode: GPS distance
68
+ Minimum total school height (meters): 4.00
69
+ Minimum candidate length (meters): 1.00
70
+ Minimum candidate height (meters): 2.00
71
+ Maximum vertical linking distance (meters): 2.00
72
+ Maximum horizontal linking distance (meters): 20.00
73
+ Minimum total school length (meters): 4.00
74
+ krill_schools
75
+ 20191020 0536551605 15.0250000000 20191020 0536551605 17.0000000000 20191020 0536561605 17.0000000000 20191020 0536561605 16.0000000000 20191020 0536571635 16.0000000000 20191020 0536571635 15.0000000000 20191020 0536581405 15.0000000000 20191020 0536581655 15.0250000000 20191020 0536581655 16.0000000000 20191020 0536591655 16.0000000000 20191020 0536591655 15.0000000000 20191020 0536581655 15.0000000000 20191020 0536581655 14.0000000000 20191020 0536571635 14.0000000000 20191020 0536571635 13.0000000000 20191020 0536561605 13.0000000000 20191020 0536561605 14.9750000000 20191020 0536561355 15.0000000000 20191020 0536551605 15.0000000000 20191020 0536551605 14.0000000000 20191020 0536541560 14.0000000000 20191020 0536541560 15.0000000000 20191020 0536551355 15.0000000000 1
76
+ Region 49"""
77
+
78
+ ah_school_example1 = """13 5 23 0 7 -1 1 20191106 1314583780 25.3008882713 20191106 1314593790 30.2941528987
79
+ 0
80
+ 10
81
+ School detected with:
82
+ Minimum data threshold: -66.00
83
+ Maximum data threshold: (none)
84
+ Distance mode: GPS distance
85
+ Minimum total school height (meters): 4.00
86
+ Minimum candidate length (meters): 1.00
87
+ Minimum candidate height (meters): 2.00
88
+ Maximum vertical linking distance (meters): 2.00
89
+ Maximum horizontal linking distance (meters): 20.00
90
+ Minimum total school length (meters): 4.00
91
+ AH_School
92
+ 20191106 1314583780 25.4929369108 20191106 1314583780 30.2941528987 20191106 1314593790 30.2941528987 20191106 1314593790 25.3008882713 20191106 1314583780 25.3008882713 1
93
+ Region 23"""
94
+
95
+
96
+ ah_school_example2 = """13 16 28 0 7 -1 1 20191106 1317305715 31.8305420148 20191106 1317335745 35.8635634446
97
+ 0
98
+ 10
99
+ School detected with:
100
+ Minimum data threshold: -66.00
101
+ Maximum data threshold: (none)
102
+ Distance mode: GPS distance
103
+ Minimum total school height (meters): 4.00
104
+ Minimum candidate length (meters): 1.00
105
+ Minimum candidate height (meters): 2.00
106
+ Maximum vertical linking distance (meters): 2.00
107
+ Maximum horizontal linking distance (meters): 20.00
108
+ Minimum total school length (meters): 4.00
109
+ AH_School
110
+ 20191106 1317315725 34.3319755445 20191106 1317315725 35.8635634446 20191106 1317325735 35.8635634446 20191106 1317325735 35.4794661656 20191106 1317335745 35.4794661656 20191106 1317335745 34.5192229680 20191106 1317325735 34.5192229680 20191106 1317325735 34.3271743285 20191106 1317335745 34.3271743285 20191106 1317335745 31.8305420148 20191106 1317315725 31.8305420148 20191106 1317315725 34.1303244730 20191106 1317315475 34.1351256890 20191106 1317305715 34.1351256890 20191106 1317305715 34.3271743285 20191106 1317315475 34.3271743285 1
111
+ Region 28"""
112
+
113
+ atlantic_herring_example = """13 70 2 0 2 -1 1 20191014 0011274130 26.8060139347 20191014 0017599670 76.3659699303
114
+ 0
115
+ 0
116
+ atlantic_herring
117
+ 20191014 0011274130 32.7282728273 20191014 0011339205 48.0014668133 20191014 0011474370 54.2354235424 20191014 0011499420 54.2354235424 20191014 0011544520 54.2354235424 20191014 0012260060 57.6640997433 20191014 0012310115 57.6640997433 20191014 0012350165 57.6640997433 20191014 0012390210 57.6640997433 20191014 0013370930 70.7554088742 20191014 0013420980 71.6905023836 20191014 0013506080 71.6905023836 20191014 0013541115 71.3788045471 20191014 0013586170 71.3788045471 20191014 0014011185 70.7554088742 20191014 0015067420 75.7425742574 20191014 0015107470 76.0542720939 20191014 0015187560 76.3659699303 20191014 0015282670 76.3659699303 20191014 0015307700 76.0542720939 20191014 0015337730 75.7425742574 20191014 0016278420 75.1191785845 20191014 0016383575 75.1191785845 20191014 0016473695 75.1191785845 20191014 0016528780 74.4957829116 20191014 0016568850 73.2489915658 20191014 0017129040 68.2618261826 20191014 0017154065 66.7033370004 20191014 0017184115 64.8331499817 20191014 0017199150 62.9629629630 20191014 0017339330 56.4173083975 20191014 0017384400 55.7939127246 20191014 0017439460 55.1705170517 20191014 0017479510 53.6120278695 20191014 0017599670 49.5599559956 20191014 0017599670 48.0014668133 20191014 0017584660 44.8844884488 20191014 0017559630 44.5727906124 20191014 0017309300 44.5727906124 20191014 0017284270 44.2610927759 20191014 0017259240 44.2610927759 20191014 0017214180 43.3259992666 20191014 0017179110 43.0143014301 20191014 0016323505 43.0143014301 20191014 0016288450 43.0143014301 20191014 0016223340 42.0792079208 20191014 0016188310 42.0792079208 20191014 0016143230 41.1441144114 20191014 0015467870 39.2739273927 20191014 0015402790 38.0271360469 20191014 0015327710 36.7803447011 20191014 0014351845 34.9101576824 20191014 0014306720 34.5984598460 20191014 0014261660 33.9750641731 20191014 0014221575 33.3516685002 20191014 0014191530 33.3516685002 20191014 0014161465 33.3516685002 20191014 0014116410 33.3516685002 20191014 0013150670 30.2346901357 20191014 0013060570 28.6762009534 20191014 0013020530 27.4294096076 20191014 0012555450 26.8060139347 20191014 0012485320 26.8060139347 20191014 0012455280 26.8060139347 20191014 0012134870 29.6112944628 20191014 0012074750 29.9229922992 20191014 0011534490 31.4814814815 20191014 0011429315 32.4165749908 20191014 0011339205 32.7282728273 20191014 0011314180 32.7282728273 1
118
+ 100"""
119
+
120
+
121
+ def test_process_evr_record_possible_herring():
122
+ echoview_record_manager = EchoviewRecordManager()
123
+ echoview_record_manager.process_evr_record(
124
+ evr_record=possible_herring_example, filename="test.evr"
125
+ )
126
+
127
+
128
+ def test_process_evr_record_fish_school():
129
+ echoview_record_manager = EchoviewRecordManager()
130
+ echoview_record_manager.process_evr_record(
131
+ evr_record=fish_school_example, filename="test.evr"
132
+ )
133
+
134
+
135
+ def test_process_evr_record_unclassified_regions():
136
+ echoview_record_manager = EchoviewRecordManager()
137
+ echoview_record_manager.process_evr_record(
138
+ evr_record=unclassified_regions_example, filename="test.evr"
139
+ )
140
+
141
+
142
+ def test_process_evr_record_krill_schools():
143
+ echoview_record_manager = EchoviewRecordManager()
144
+ echoview_record_manager.process_evr_record(
145
+ evr_record=krill_schools_example, filename="test.evr"
146
+ )
147
+
148
+
149
+ def test_process_evr_record_ah_school_1():
150
+ echoview_record_manager = EchoviewRecordManager()
151
+ echoview_record_manager.process_evr_record(
152
+ evr_record=ah_school_example1, filename="test.evr"
153
+ )
154
+
155
+
156
+ def test_process_evr_record_ah_school_2():
157
+ echoview_record_manager = EchoviewRecordManager()
158
+ echoview_record_manager.process_evr_record(
159
+ evr_record=ah_school_example2, filename="test.evr"
160
+ )
@@ -0,0 +1,5 @@
1
+ from __future__ import absolute_import
2
+
3
+ from . import astronomical, geospatial, record, shape
4
+
5
+ __all__ = ["astronomical", "record", "geospatial", "shape"]
@@ -0,0 +1,5 @@
1
+ from .astronomical_manager import AstronomicalManager
2
+
3
+ __all__ = [
4
+ AstronomicalManager,
5
+ ]
@@ -0,0 +1,82 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import pvlib
4
+
5
+ from water_column_sonar_annotation.geospatial import GeospatialManager
6
+
7
+
8
+ class AstronomicalManager:
9
+ #######################################################
10
+ def __init__(
11
+ self,
12
+ ):
13
+ self.DECIMAL_PRECISION = 6
14
+ # https://github.com/CI-CMG/water-column-sonar-annotation/issues/6
15
+ self.SUNRISE_DEGREES = 0.0
16
+ self.CIVIL_TWILIGHT_DEGREES = -6.0
17
+ self.NAUTICAL_TWILIGHT_DEGREES = -12.0 # Requested metric to calculate
18
+ self.ASTRONOMICAL_TWILIGHT_DEGREES = -18.0
19
+
20
+ @staticmethod
21
+ def get_solar_azimuth(
22
+ iso_time: str = "2026-01-26T00:06:00Z",
23
+ latitude: float = 39.9674884, # boulder gps coordinates
24
+ longitude: float = -105.2532602,
25
+ ) -> float:
26
+ """
27
+ Good reference for calculating: https://www.suncalc.org/#/39.9812,-105.2495,13/2026.01.26/11:52/1/3
28
+ utc time now: '2026-01-25T18:42:00Z' # 11:43am
29
+ 7:14 am↑ (sunrise)
30
+ (Timestamp('2026-01-25 18:42:00+0000', tz='UTC'), Timestamp('2026-01-25 14:15:07.145030400+0000', tz='UTC'))
31
+ 5:13 pm↑ (sunset)
32
+ (Timestamp('2026-01-25 18:42:00+0000', tz='UTC'), Timestamp('2026-01-26 00:10:51.244243200+0000', tz='UTC'))
33
+ solar altitude should be: 31.26°, azimuth should be: 174.01°
34
+ """
35
+ solar_position = pvlib.solarposition.get_solarposition(
36
+ time=pd.DatetimeIndex([iso_time]),
37
+ latitude=latitude,
38
+ longitude=longitude,
39
+ )
40
+ # 'elevation' is analogous to 'altitude' in suncalc
41
+ elevation = solar_position.elevation.iloc[0]
42
+ ### The altitude aka elevation is the angle between horizon and the center of the sun including refraction ###
43
+ return np.round(elevation, 2)
44
+
45
+ def phase_of_day(
46
+ self,
47
+ iso_time: str,
48
+ latitude: float,
49
+ longitude: float,
50
+ ) -> int:
51
+ """
52
+ Returns whether the time/gps references a Nautical Daylight time
53
+ Going to need to verify the az is correctly computed
54
+ { 'night': 4, 'dawn': 1, 'day': 2, 'dusk': 3 }
55
+ """
56
+ # categories = {"night": 4, "dawn": 1, "day": 2, "dusk": 3}
57
+ solar_azimuth = self.get_solar_azimuth(iso_time, latitude, longitude)
58
+ geospatial_manager = GeospatialManager()
59
+ local_hour = geospatial_manager.get_local_hour_of_day(
60
+ iso_time=iso_time,
61
+ latitude=latitude,
62
+ longitude=longitude,
63
+ )
64
+ if solar_azimuth < self.NAUTICAL_TWILIGHT_DEGREES:
65
+ return 4 # night
66
+ if solar_azimuth >= 0.0:
67
+ return 2 # day
68
+ if local_hour < 12:
69
+ return 1 # dawn
70
+ return 3 # dusk
71
+
72
+ # def get_moon_phase(self):
73
+ # # TODO: add method for getting the moon phase
74
+ # pass
75
+
76
+ # TODO: calculate moonrise and moonset
77
+
78
+
79
+ # if __name__ == "__main__":
80
+ # astronomical_manager = AstronomicalManager()
81
+ # azimuth = astronomical_manager.get_solar_azimuth()
82
+ # print(azimuth)
@@ -0,0 +1,5 @@
1
+ from .cruise_manager import CruiseManager
2
+
3
+ __all__ = [
4
+ CruiseManager,
5
+ ]