Skip to main content

Aquaculture — salmon farming

The problem. In a sea-pen salmon farm, the margin between a healthy harvest and a catastrophe is thin and fast. Dissolved oxygen can fall through a critical level on a warm, still afternoon and suffocate an entire pen within hours. Sea lice spread between pens and trip regulatory limits. And feed — the single largest cost — is wasted the moment fish stop eating, fouling the water as it sinks. Farmers need to see a pen turning dangerous in time to act: aerate before an oxygen crash, treat before lice escalate, and stop feeding the moment appetite drops.

What we solve here is preventing mass mortality and protecting feed margin on a farm where conditions change by the minute.

Set up demo data

New workspace? Run this once (Python) to create the pen's oxygen/temperature series, create the subscription before we listen, and feed in oxygen that falls below the 6 mg/L danger line — so the loop raises a low_oxygen alarm. Safe to re-run.

import datahub_sdk, numpy as np, pandas as pd

client = datahub_sdk.DataHubClient.from_env()

for s, u in [("pen_h_07_dissolved_oxygen_mg_l", "mg_l"), ("pen_h_07_water_temp_c", "deg_c")]:
client.timeseries.create([datahub_sdk.TimeSeries(external_id=s, name=s, unit=u, value_type="float")])
client.subscriptions.create([datahub_sdk.Subscription(
external_id="farm_hardanger_pens", name="Hardanger pens",
timeseries=["pen_h_07_dissolved_oxygen_mg_l"])])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=120, freq="1min")
client.timeseries.insert_from_lists(timestamps=idx, values=8 - np.linspace(0, 3, 120),
ts="pen_h_07_dissolved_oxygen_mg_l")

1. Watch pen conditions live — and aerate before a crash

Dissolved oxygen and temperature per pen stream into a subscription. The instant oxygen dips toward the danger line, raise a low_oxygen alarm so aerators start while there's still time. See Consume live data.

import ai.intellistream.datahub.sdk.subscriptions.SubscriptionMessage;

try (var stream = client.subscriptions().listen(List.of("farm_hardanger_pens"))
.stream((SubscriptionMessage msg) -> { // auto-acks after each message
if (oxygenBelow(msg.payload(), 6.0)) { // mg/L — danger approaching
EventModel alarm = new EventModel();
alarm.setExternalId("low_oxygen_h07_" + System.currentTimeMillis());
alarm.setType("low_oxygen");
alarm.setStatus("critical");
alarm.setMetadata(Map.of("pen", "pen_h_07", "do_mg_l", "5.6"));
alarm.setEventTime(ZonedDateTime.now());
client.events().create(List.of(alarm)); // triggers aeration
}
})) {
awaitShutdown(); // your app lifecycle; closing the stream ends delivery
}

2. Protect feed margin

Feed dispensed and biomass are series per pen. Roll them up to a feed-conversion ratio (feed in vs growth) so a pen quietly going off its feed shows up before the cost does. See Query & aggregate.

var feed = Timeseries.of("pen_h_07_feed_kg").name("Pen H-07 feed");
feed.setUnit("kg");
var biomass = Timeseries.of("pen_h_07_biomass_kg").name("Pen H-07 biomass");
biomass.setUnit("kg");
client.timeseries().create(feed, biomass);

client.timeseries().ingest(Map.of(
"pen_h_07_feed_kg", List.of(Datapoint.of(Instant.now(), 420.0))));

3. Keep the compliance record

Sea-lice counts and any mortality are lice_count and mortality events tied to the pen — the auditable record the regulator and the vet both need.

See the result

The falling oxygen trips the loop before a crash:

low_oxygen_h07_… → critical (dissolved oxygen fell below 6 mg/L)

See also