Nutrition 8 min read

Eat more fish, but not that fish

Omega-3 pushes you toward fish, mercury pushes back. How gostrong's solver balances both, plus protein quality, so you never do the arithmetic.

Ask a training-literate friend whether to eat more fish and you get a confident “yes, omega-3”. Ask whether to avoid certain fish and you get a confident “yes, mercury”. Both are right, and they pull in opposite directions. Doing that bookkeeping by hand, across seven days of meals that also have to hit your protein and calorie targets, is not something anyone should do.

This post explains the science we encode, and how a constraint solver turns it into a meal plan without you touching a calculator.

Four constraints on one food group

Fish is the one food group gostrong constrains from four directions at once, because the evidence points in four directions: eat enough of it, get the omega-3 that is the reason to eat it, do not concentrate the mercury, and vary the species. None of the four subsumes another.

ConstraintValueDirection
Weekly EPA+DHA1750 mgfloor
Weekly methylmercury0.7 µg per kg of bodyweightceiling
Per-species fish400 g per weekceiling
White fish125 g per weekfloor

Weekly numbers are pro-rated for partial windows. Single-day regenerations drop all four rows: an adequate intake is a weekly average, and forcing one day to carry the week’s fish would be a different rule, not a stricter one.

The reason to eat fish: an EPA+DHA floor

The floor is 250 mg/day of combined EPA and DHA, times seven, which gives 1750 mg per week. It comes from the EFSA Adequate Intake for adults (cardiovascular endpoint). The American Heart Association advice of about two oily-fish servings per week is concordant with it at the lower bound.

Each fish in the catalog carries its EPA+DHA content per 100 g. The solver sums grams times that density over every meal in the window and requires the total to reach the weekly target.

Why a dedicated floor, when the plan already has a weekly fish category target? Because that category floor (at least 300 g per week) is satisfied by any fish at all. Without the omega-3 floor, the optimizer had no reason to prefer the oily species that actually carry the omega-3.

The cost of eating fish: a personal mercury budget

The US EPA reference dose for methylmercury is 0.1 µg per kg of bodyweight per day, which is 0.7 µg/kg/week. The reference dose carries roughly a 10x uncertainty factor below the benchmark dose, so staying under it leaves a wide margin.

The budget is personal: 0.7 multiplied by your current bodyweight in kg. A lighter person gets a smaller budget, because the dose is defined per kilogram. In code, it is as plain as it sounds:

const MercuryRfDUgPerKgPerWeek = 0.7

func MercuryWeeklyBudgetUg(bodyWeightKg float64) float64 {
    return MercuryRfDUgPerKgPerWeek * bodyWeightKg
}

Each fish has a mean mercury concentration in µg/g (from the FDA monitoring database). Since ppm is micrograms per gram, weekly exposure is simply the sum of grams times ppm. No unit juggling. We chose the EPA figure over the looser EFSA tolerable weekly intake (1.3 µg/kg/week) because it matches the FDA/EPA per-serving advice that the species means come from.

Here is how a few species look, as the catalog defines them:

SpeciesBlue/white classMean mercury (µg/g)
Sardineoily (blue)0.013
Salmonoily (blue)0.022
European sea basswhite~0.10
Codwhite0.111
Canned light tunaoily (blue)0.126
Bluefin tunaoily (blue)~0.69
Swordfishlarge predator0.995

The spread is the point: swordfish carries roughly 75 times the mercury per gram of sardine. That is why the mercury column matters when choosing which fish to eat, and why large predators (bluefin, swordfish, shark) are additionally flagged as manual-only in the catalog, following Spanish AESAN advice for adults.

Variety: per-species caps and the blue/white axis

The mercury budget alone has a loophole. The optimizer could concentrate the whole week in whichever species has the best protein per unit of mercury, which maximizes exposure to that one species’ average. And that average is a monitoring mean, not a property of the particular tin you bought. So every fish sub-category gets a weekly cap of 400 g. Composed recipes count too: their fish grams are attributed to the species they contain.

Then there is the blue/white axis. Spanish guidance (AESAN, 2019) recommends 3 to 4 servings per week, varying between white and oily fish. We classify each fish sub-category as oily, white, shellfish or none, using muscle-fat content as the basis: oily above 5 %, white below 2 %. Sea bream and sea bass are technically in between (2.5 to 6 %), but we place them on the white side because their EPA+DHA density is far below the oily cluster, and EPA+DHA density is exactly what the axis exists to balance. Mussels are invertebrates, so they count toward neither side.

Both the omega-3 floor and the mercury budget reward oily fish. Left alone, the optimizer would never choose white species. So a 125 g weekly white-fish floor (one serving at the low end of the SENC 125 to 150 g band) acts as the counterweight.

How the solver sees it

Our diet model is a CP-SAT problem (OR-Tools) in a stateless Python service. The variables are grams of each food in each meal on each day. The fish rules are linear sums over those variables:

mercury:   sum(grams * mercury_ppm)            <= 0.7 * bodyweight_kg
omega-3:   sum(grams * epa_dha_mg / 100)       >= 1750
per fish:  sum(grams of species s)             <= 400    (each species)
white:     sum(grams * white_share)            >= 125

The white-fish floor needs no special solver code. Each food carries a synthetic fish_white coefficient derived from its sub-category’s class, so a recipe contributes exactly its white-ingredient share, and the floor is just another weekly target row. It is also built to degrade safely: if no meal in the window can hold a white-fish candidate (every meal is a quick eat, say), the row is dropped and the week stays feasible.

All of this sits next to the rest of the plan’s hard constraints: per-meal and per-day macros, unit completion, anchors you pinned, exclusions. Fish does not get its own solver. It is part of the same problem, so the solution that satisfies the mercury budget is also one that hits your protein and calories.

What actually binds

We audited this against captured production requests. Achieved intake divided by each constraint’s own limit:

ConstraintAchieved / limit
Mercury budget0.92 to 0.99 (binding)
Omega-3 floor4.9 to 7.4 times over
Fish category floor6.4 times over
White-fish floor2.3 times over
Per-species capsall under cap

Mercury is the only brake. The objective already likes fish (high protein, low saturated fat, low salt, omega-3 dense), so it adds fish until mercury stops it. Every constraint that exists to promote fish is currently redundant in the catalog.

We did not delete them. Removing the per-species caps made saturated fat worse (60 % to 40 % green days), because the caps look slack precisely since they force the spread that keeps every species under its cap. Removing the omega-3 floor cost both saturated fat and salt. A safety net that is inert today is a safety net working. Caveat: that drop-and-re-solve used three seeds and five-day windows, so percentages move in 20-point steps. What carries is qualitative.

Protein is not just grams

The same philosophy applies to the rest of the plate. Grams of protein are not interchangeable, so the solver models two properties of each protein source.

Quality, via DIAAS. The Digestible Indispensable Amino Acid Score is the FAO’s recommended measure of protein quality. Each food’s protein coefficient is multiplied by min(DIAAS, 1.0) before the hard per-day protein floor is checked, so the floor enforces utilizable protein. A gram of lentil protein counts for about 0.60 g. Animal protein is unchanged. Values are deliberately conservative where the literature spans a range:

SourceDIAAS
Milk1.14
Beef1.10
Chicken, turkey1.08
Chickpeas0.70
Lentils, rice0.60

The cap at 1.0 is a conscious conservative choice. A mixed day does enjoy real amino acid complementation, but the model sums per-food coefficients and cannot see which amino acid is limiting. Uncapping would let beef’s surplus in one amino acid offset a deficit in another, so the floor stays honest rather than generous.

Leucine, per meal. Leucine is the trigger for muscle protein synthesis (Norton and Layman 2006), and we enforce a hard floor of about 2.5 g of leucine on every protein-bearing meal. It is a hard constraint because a soft term did nothing: as an objective term it was indistinguishable from zero next to families with far larger coefficients. Muscle is the primary goal, and a primary goal belongs in the constraints. The floor is uniform across meals on purpose: even distribution is the part of protein timing with good support, while slot-specific boosts are not.

What you do with all this

Nothing. You set your goal and preferences; the plan comes back with fish where it helps, white and oily species alternating, a mercury total under your personal budget, and protein that clears the floor in usable terms. The statistics page also shows the mercury budget, so it is visible, not merely enforced.

If you would rather not keep a spreadsheet of milligrams and micrograms in your head, gostrong does this for you.

References

  • EFSA Panel on Dietetic Products (2010; DRV summary 2017). Adequate Intake of 250 mg/day combined EPA+DHA for adults.
  • US EPA. Reference dose for methylmercury: 0.1 µg/kg body weight/day.
  • AESAN (2019). Recomendaciones de consumo de pescado por presencia de mercurio.
  • Norton LE, Layman DK (2006). Leucine regulates translation initiation of protein synthesis in skeletal muscle. J Nutr 136(2):533S-537S.