Finding The Air Cannon

tldr; An agricultural air cannon began firing every two minutes all night long disrupting the sleep of many in my community. I used Google Maps, three listening posts, and a programmed simulation to pinpoint the field hosting the air cannon. The county tax assessor office identified the land owner. I had a phone conversation with the farm manager. That conversation was coincident with the cessation of the nighttime air cannon. I can sleep again.

120dB;
serious hearing damage;
5 gallons of propane gives
18000 blasts
The use of agricultural air cannons south of Corvallis has been extreme this month. Farmers with field crops are often beset with Canadian Geese overwintering in the Willamette Valley. To scare the geese away, they frequently use propane air cannons on timers. Starting on January 5th, an air cannon began firing every two minutes all day and throughout the night. My sleep and that of many neighbors was disrupted for nearly a month.

Trying to figure out where the cannon shots originated, I drove out and listened from several locations. It was loud on Airport Road, Bellfountain Rd, 53rd St, Plymouth Dr, Nash Ave, Brooklane Dr, Wake Robin Road, Rivergreen Ave and as far north as the Benton County Fair Grounds. It was silent anywhere south of Llewellyn Rd. Trying to get a heading for the sound with just my ears and a compass was way more difficult than I expected.

The Corvallis Airport
looking South

All indications were that the sound was coming from the area of the airport, but I could not pin it down any more accurately. The airport staff denied using an air cannon. However, they indicated that they could hear one to the south.

The most easily identifiable farm south of the airport was Venell Farms. They fervently denied they were using an air cannon.

That left a big chunk of land south and west of the airport that was not crossed by public roads. Not willing to trespass to find the source, I needed a new strategy.

You can support my
artwork and writing
on Patreon or Ko-fi
Retirement is tough.
Even small
contributions help.

After some deep thought, I theorized I could pinpoint the location of the air cannon by logging the arrival times of the sound from three locations. It took three people stationed at remote locations miles apart using a synchronized clock on our cell phones. We each waited over the same ten minute period, noting the exact time for each of the five cannon shots that we heard.

The sound arrived at our Bellfountain Rd location first. About four seconds later, the sound arrived at our Brooklane Dr. lookout. Two seconds later, it arrived at our Rivergreen Ave listening post.

I took a screen capture of a Google Map of our area with a measurement line on the map for scale. I plotted our three listening locations. The scale allowed us to figure out how far apart our test locations were and facilitated a method for translating back and forth between the image coordinate system and the real world coordinate system.

6.3km Δ~6S 3.8km Δ~4S 3.0km Δ~2S A B C Sound Source Search Area Brooklane Dr Rivergreen Ave Bellfountain Rd

I wrote a program in Python (see source code below) that could iterate all the points in the image in the search area where we suspected the air cannon sat. Translating those image points to real world coordinates, I used the speed of sound at 35℉ to calculate the travel time to each of our three listening posts. Comparing the arrival times of the simulated sounds, I could match them with the deltas from the real observations. Once my program scanned and tested every point on the south half of the map image, it identified a point that exactly replicated the deltas. I knew I found the only possible origin of the sound and therefore the location of the air cannon. I translated my grid system back to real world coordinates and plotted it on my map. That identified the offending farm field.

Image by claralieu, licensed under CC BY 2.0
Angry goose does not like
you or air cannons

I went to the County Tax Assessor Office to find the owner of the property. With a company name, I found a Web site and a phone number.

I called the owner of the farm (headquartered in Monmouth) and asked if they used an air cannon on their property near the Corvallis airport. They confirmed that they do. I asked if they run it at night, they said they do not. However, the manager said that he'd contact the people working onsite. Twenty minutes later, he called back reaffirming they are not running their cannons at night. He also assured me that their air cannons could not be heard more than a mile away. The air cannon that we tracked to his farm was clearly audible 6 miles away from the Benton County Fairgrounds. I did not trust his knowledge of his own equipment nor how it was used by the onsite staff.

However, in an amazing coincidence, the air cannons stopped that very evening of our phone conversation (Thursday, January 25). The air cannons have fallen silent at night since that day. I suspect that the cessation was not a random correlation. I surmise that they actually were using the cannon at night and the manager stopped the practice.

It was a miserable twenty days of ruined sleep. I found threads about the air cannon on Facebook, Reddit, Twitter/X and Nextdoor. How many people in Corvallis and Philomath were disrupted?

I’m refraining from revealing the name of the farm. I don’t want folk harassing them. I will say, though, it wasn’t Venell Farms as so many expected.

Should the nighttime air cannon start up again, I have an easy tool to figure out its exact location from miles away. I will exercise this newly found power whenever I need it.

Image by audreyjm529, licensed under CC BY 2.0

Please note: this code is shared to show the algorithm, not as a drop-in application that just runs. It depends on two private modules configmanners and points. Both of these are on github, but to upload them to pip would be to commit to supporting them in perpetuity. I'm not willing to do that.

There is no warranty, no guarantee, and the author is absentee.

The Source Code to my Air Cannon location search program.
#!/usr/bin/env python3

from math import sqrt, isclose
from functools import partial

from points import Point, Block
from configmanners import Namespace, configuration
from configmanners.converters import str_to_list

terrible_number = 100000

def distance(p1, p2):
    return sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)


str_to_Point = partial(
    str_to_list, item_converter=float, list_to_collection_converter=Point
)

str_to_Block = partial(
    str_to_list, item_converter=int, list_to_collection_converter=Block
)


def all_points_within_block_iter(a_block):
    for x in range(a_block.left, a_block.right):
        for y in range(a_block.upper, a_block.lower):
            yield Point(x, y)


required_config = Namespace()
required_config.add_option(
    "A_in_px",
    default=Point(58.34, 844.29),
    doc="point A in pixel coordinates",
    from_string_converter=str_to_Point,
)

required_config.add_option(
    "B_in_px",
    default=Point(846.05, 290.27),
    doc="point B in pixel coordinates",
    from_string_converter=str_to_Point,
)
required_config.add_option(
    "C_in_px",
    default=Point(1574.73, 453.96),
    doc="point C in pixel coordinates",
    from_string_converter=str_to_Point,
)
required_config.add_option(
    "AB_time_delta",
    default=4.0,
    doc="time delta between listening posts A and B",
    from_string_converter=str_to_Point,
)
required_config.add_option(
    "AC_time_delta",
    default=6.0,
    doc="time delta between listening posts A and C",
    from_string_converter=str_to_Point,
)
required_config.add_option(
    "BC_time_delta",
    default=2.0,
    doc="time delta between listening posts B and C",
    from_string_converter=str_to_Point,
)
required_config.add_option(
    "AB_in_m",
    default=3800.0,
    doc="distance in meters between points A and B",
)
required_config.add_option(
    "speed_of_sound_in_mps",
    default=332.24,
    doc="the speed of sound in meters per second",
)
required_config.add_option(
    "search_box_in_px",
    default="400, 1000, 1200, 2000",
    doc="coodinate extents in pixels of the search area",
    from_string_converter=str_to_Block,
)


config = configuration(required_config)

scale = config.AB_in_m / distance(config.A_in_px, config.B_in_px)


def test_fitness(test_point_in_px):
    SA_in_m = distance(config.A_in_px, test_point_in_px) * scale
    SB_in_m = distance(config.B_in_px, test_point_in_px) * scale
    SC_in_m = distance(config.C_in_px, test_point_in_px) * scale

    TA_in_s = SA_in_m / config.speed_of_sound_in_mps
    TB_in_s = SB_in_m / config.speed_of_sound_in_mps
    TC_in_s = SC_in_m / config.speed_of_sound_in_mps

    test_AB_delta = abs(TB_in_s - TA_in_s)
    if not isclose(test_AB_delta, config.AB_time_delta, abs_tol=0.1):
        return terrible_number

    test_AC_delta = abs(TA_in_s - TC_in_s)
    if not isclose(test_AC_delta, config.AC_time_delta, abs_tol=0.1):
        return terrible_number

    test_BC_delta = abs(TB_in_s - TC_in_s)
    if not isclose(test_BC_delta, config.BC_time_delta, abs_tol=0.1):
        return terrible_number

    # return the deviation from the sum of the deltas as a fittness value
    return abs(
        test_AB_delta
        + test_AC_delta
        + test_BC_delta
        - config.AB_time_delta
        - config.AC_time_delta
        - config.BC_time_delta
    )


best_fitness = terrible_number
best_point = None
for test_point_in_px in all_points_within_block_iter(config.search_box_in_px):
    fitness = test_fitness(test_point_in_px)
    if fitness < best_fitness:
        best_fitness = fitness
        best_point = test_point_in_px


if best_point is not None:
    print(f'{best_point=} at {best_fitness=}')
else:
    print('the air cannon is not in the search area')
(this code is not currently hosted in a public repository)

Please note: this code is shared to show the algorithm, not as a drop-in application that just runs. It depends on two private modules configmanners and points. Both of these are on github, but to upload them to pip would be to commit to supporting them in perpetuity. I'm not willing to do that.

There is no warranty, no guarantee, and the author is absentee.