Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions data/templates/bat_phase_analysis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from batphase import detect_feeding_buzz_phases
from batphase import detect_feeding_buzz_phases, classify
from tabulate import build_table, print_table

#: Pulse widths in ms
Expand All @@ -11,7 +11,10 @@
DPRI = $DPRI

#: Detect the feeding buzz phases
_, regions, classification = detect_feeding_buzz_phases(WIDTHS, PRI, DPRI)
_, regions = detect_feeding_buzz_phases(WIDTHS, PRI, DPRI)

#: Classify the sequence
classification = classify(regions)

#: Build the phase table
table = build_table(regions, ("Phase", "Start", "End", "Length"))
Expand Down
57 changes: 0 additions & 57 deletions src/common/storage.py

This file was deleted.

50 changes: 45 additions & 5 deletions src/ecology/batphase.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,52 @@ def detect_feeding_buzz_phases(widths, pri, dpri):
for i in range(buzz_end + 1, n):
phases[i] = EXIT

return phases, build_regions(phases)


def classify(regions):
"""
Classify a sequence based on its regions and their length

:param regions: List of tuples of regions
:return: Classification
"""
total = 0
lengths = {}

# Collate the number of pulses by phase assignment
for phase, _, _, length in regions:
total += length
lengths[phase] = lengths.get(phase, 0) + length

# Extract the lengths of each phase assignment
search_pulses = lengths.get("SEARCH", 0)
approach_pulses = lengths.get("APPROACH", 0)
buzz_pulses = lengths.get("BUZZ", 0)
exit_pulses = lengths.get("EXIT", 0)

# Calculate the fraction of the total that are search pulses
search_frac = search_pulses / total if total else 0

# Determine which regions are present
has_buzz = buzz_pulses > 0
has_approach = approach_pulses > 0
has_exit = exit_pulses > 0

# Classify the sequence
if BUZZ in phases:
if has_buzz and has_exit and buzz_pulses >= 6 and exit_pulses >= 5:
classification = "FEEDING PASS WITH RECOVERY EXIT"
elif has_buzz and buzz_pulses >= 6:
classification = "FEEDING BUZZ"
elif APPROACH in phases:
classification = "APPROACH ONLY"
else:
elif has_buzz:
classification = "SHORT OR WEAK BUZZ"
elif has_approach and approach_pulses >= 5:
classification = "APPROACH WITHOUT BUZZ"
elif has_approach:
classification = "BRIEF APPROACH ONLY"
elif search_frac >= 0.85:
classification = "SEARCH ONLY"
else:
classification = "MIXED OR UNCERTAIN PASS"

return phases, build_regions(phases), classification
return classification
13 changes: 8 additions & 5 deletions src/examples/batbuzz.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
from batphase import detect_feeding_buzz_phases
from batphase import detect_feeding_buzz_phases, classify
from tabulate import build_table, print_table

#: Pulse widths in ms
WIDTHS = (0.05111111111111111, 0.04693877551020406, 0.046485260770975034, 0.03356009070294785, 0.04643990929705216, 0.040362811791383235, 0.049024943310657654, 0.04435374149659865, 0.04825396825396833, 0.0386848072562358, 0.0338321995464852, 0.04022675736961445, 0.03573696145124727, 0.039682539682539764, 0.04240362811791387, 0.040181405895691746, 0.03959183673469391, 0.04367346938775518, 0.04349206349206347, 0.045941043083900235, 0.04589569160997731, 0.0401360544217686, 0.036190476190476106, 0.03396825396825398, 0.029251700680272164, 0.02285714285714291, 0.02408163265306107, 0.03224489795918384, 0.03396825396825376, 0.03519274376417236, 0.034739229024943086, 0.03795918367346918, 0.03428571428571425, 0.03609977324263047, 0.03256235827664433, 0.04548752834467118, 0.04081632653061229, 0.04099773242630356, 0.04004535147392252, 0.04040816326530594, 0.028888888888888964, 0.0394557823129249, 0.026848072562358105, 0.03859410430838972, 0.0439002267573696, 0.044625850340136, 0.04757369614512452, 0.04349206349206369, 0.04258503401360514, 0.03727891156462615)
WIDTHS = (0.04240362811791387, 0.040181405895691746, 0.03959183673469391, 0.04367346938775518, 0.04349206349206347, 0.045941043083900235, 0.04589569160997731, 0.0401360544217686, 0.036190476190476106, 0.03396825396825398, 0.029251700680272164)

#: Pulse Repetition Interval, PRI
PRI = (0.08707482993197277, 0.09188208616780047, 0.0875283446712018, 0.08712018140589572, 0.09052154195011336, 0.09011337868480729, 0.09560090702947843, 0.07941043083900223, 0.08739229024943318, 0.09002267573696143, 0.08444444444444443, 0.081859410430839, 0.08802721088435383, 0.08263038548752832, 0.0840362811791382, 0.06390022675736962, 0.08589569160997734, 0.0734240362811791, 0.051337868480725746, 0.05764172335600892, 0.04335600907029491, 0.045578231292517035, 0.028843537414965814, 0.043809523809523965, 0.03428571428571425, 0.027936507936507926, 0.37918367346938786, 0.07800453514739214, 0.08585034013605464, 0.0790022675736961, 0.08331065759637157, 0.08195011337868507, 0.07818594104308385, 0.07183673469387752, 0.08258503401360562, 0.08145124716553287, 0.08485260770975023, 0.09065759637188231, 0.11800453514739218, 0.09024943310657596, 0.09283446712018151, 0.08947845804988663, 0.08916099773242614, 0.08580498866213171, 0.08825396825396803, 0.08916099773242658, 0.08829931972789096, 0.09814058956916138, 0.09247165532879809, None)
PRI = (0.0840362811791382, 0.06390022675736962, 0.08589569160997734, 0.0734240362811791, 0.051337868480725746, 0.05764172335600892, 0.04335600907029491, 0.045578231292517035, 0.028843537414965814, 0.043809523809523965, None)

#: Delta-PRI, DPRI
DPRI = (None, 0.0048072562358277005, -0.004353741496598673, -0.00040816326530607183, 0.0034013605442176353, -0.00040816326530607183, 0.005487528344671144, -0.0161904761904762, 0.007981859410430947, 0.002630385487528253, -0.005578231292517, -0.002585034013605436, 0.006167800453514838, -0.005396825396825511, 0.0014058956916098708, -0.02013605442176858, 0.021995464852607727, -0.012471655328798237, -0.02208616780045336, 0.006303854875283177, -0.014285714285714013, 0.0022222222222221255, -0.01673469387755122, 0.01496598639455815, -0.009523809523809712, -0.006349206349206327, 0.35124716553287993, -0.3011791383219957, 0.007845804988662497, -0.0068480725623585315, 0.004308390022675468, -0.001360544217686499, -0.0037641723356012236, -0.006349206349206327, 0.010748299319728094, -0.0011337868480727487, 0.003401360544217358, 0.005804988662132082, 0.027346938775509866, -0.027755102040816215, 0.002585034013605547, -0.003356009070294874, -0.00031746031746049397, -0.00335600907029443, 0.00244897959183632, 0.0009070294784585542, -0.0008616780045356265, 0.009841269841270428, -0.005668934240363299, None)
DPRI = (None, -0.02013605442176858, 0.021995464852607727, -0.012471655328798237, -0.02208616780045336, 0.006303854875283177, -0.014285714285714013, 0.0022222222222221255, -0.01673469387755122, 0.01496598639455815, None)

#: Detect the feeding buzz phases
_, regions, classification = detect_feeding_buzz_phases(WIDTHS, PRI, DPRI)
_, regions = detect_feeding_buzz_phases(WIDTHS, PRI, DPRI)

#: Classify the sequence
classification = classify(regions)

#: Build the phase table
table = build_table(regions, ("Phase", "Start", "End", "Length"))
Expand Down
43 changes: 40 additions & 3 deletions src/support/extract_pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,37 +52,70 @@
import os
import argparse
from pathlib import Path
from datetime import datetime

DEFAULT_TEMPLATE = Path(__file__).parent.parent.parent / "data" / "templates" / "bat_pulse_chart.py"


def load_pulse_json(input_file_path: str | Path) -> dict:
def print_message(message: str) -> None:
"""
Show a timestamped message

:param message: Message text
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{timestamp} : {message}")


def load_pulse_json(
input_file_path: str | Path,
start_index: int = 1,
end_index: int = None
) -> dict:
"""
Load the consensus parameter set from the seasonal modelling consensus JSON
file for the species

:param input_file_path: JSON file path
:return: Tuple of the species namd and the filtered list of parameters
"""
print_message(f"Loading {input_file_path}")
with open(input_file_path, "r", encoding="utf-8") as f:
data = json.load(f)

# Extract the original input file and extract it's name as the source
source_file = data.get("input", data.get("input_file", None))
source = Path(source_file).name
print_message(f"Source file name {source}")

# Extract the analysis mode
mode = data["analysis_mode"]
print_message(f"Detected mode {mode}")

# Extract only the required range of pulses
pulses = data["pulses"]
max_pulse_index = len(pulses)
print_message(f"Detected {max_pulse_index} pulses")

if start_index is None or start_index <= 0:
start_index = 1

if end_index is None or end_index > max_pulse_index:
end_index = max_pulse_index

pulses = pulses[start_index - 1:end_index]
print_message(f"{len(pulses)} pulses remain after extracting the slice from {start_index} to {end_index}")

# Extract the pulses. Use the "real" start time, end time and peak timing fiels.
# For heterodyne recordings, these will be the same as the "non-real" versions
# but for time expansion the "non-real" versions reflect the expanded timings
timings = []
for pulse in data["pulses"]:
for pulse in pulses:
timings.append(pulse["real_start_time_s"])
timings.append(pulse["real_end_time_s"])
timings.append(pulse["real_peak_time_s"])

print_message(f"Extracted {len(timings) // 3} sets of timing values")
return source, mode, tuple(timings)


Expand All @@ -93,6 +126,7 @@ def read_template(template_file_path: str | Path) -> str:
:param template_file_path: Path to the template
:return: Contents of the template
"""
print_message(f"Loading template {template_file_path}")
with open(template_file_path, "r", encoding="utf-8") as f:
return f.read()

Expand All @@ -116,6 +150,7 @@ def write_analysis_script(output_path: str | Path, script: str):
:param output_path: Path to the folder where the script is to be written
:param script: File contents
"""
print_message(f"Writing launcher {output_path}")
with open(output_path, "w", encoding="utf-8") as f:
return f.write(script)

Expand All @@ -128,11 +163,13 @@ def main():
parser.add_argument("-i", "--input", required=True, help="Path to the input JSON file")
parser.add_argument("-t", "--template", default=DEFAULT_TEMPLATE,
help="Template used to build the bat call analysis script")
parser.add_argument("-si", "--start-index", type=int, default=1, help="Index of first pulse to include")
parser.add_argument("-ei", "--end-index", type=int, default=None, help="Index of last pulse to include")
parser.add_argument("-o", "--output", required=True, help="Path to the output script")
args = parser.parse_args()

# Load the data and extract the pulse information into a tuple
_, _, pulses = load_pulse_json(args.input)
_, _, pulses = load_pulse_json(args.input, args.start_index, args.end_index)

# Load the template and generate the script content
template = read_template(args.template)
Expand Down
7 changes: 6 additions & 1 deletion src/support/extract_timings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@

scripts/extract-timings.sh -i data/spectrogram/BD-A-99-001-analysis.json -o src/examples/batbuzz.py

If only part of the sequence is to be included, the --start-index and --end-index arguments can
be used to specify the 1-based indices for the first and last pulse to include

The resulting script can then be minified and transferred to the calculator along with
the bat phase analysis library module and then run to generate the pulse characteristics.

Expand Down Expand Up @@ -114,11 +117,13 @@ def main():
parser.add_argument("-i", "--input", required=True, help="Path to the input JSON file")
parser.add_argument("-t", "--template", default=DEFAULT_TEMPLATE,
help="Template used to build the bat phase analysis script")
parser.add_argument("-si", "--start-index", type=int, default=1, help="Index of first pulse to include")
parser.add_argument("-ei", "--end-index", type=int, default=None, help="Index of last pulse to include")
parser.add_argument("-o", "--output", required=True, help="Path to the output script")
args = parser.parse_args()

# Load the pulse data from the JSON file and generate the pulse timing information
_, _, pulses = load_pulse_json(args.input)
_, _, pulses = load_pulse_json(args.input, args.start_index, args.end_index)
widths, pri, _, dpri = analyse_pulse_timings(pulses)

# Load the template and generate the script content
Expand Down
2 changes: 1 addition & 1 deletion src/support/minimiser.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"batphase.py": {
"aggressive": true,
"preserve": [
"classify_sequence",
"classify",
"detect_feeding_buzz_phases"
]
},
Expand Down
Loading