|
FhSim
3.1.0
Marine systems simulation
|
A wake is the disturbance a structure leaves in the flow downstream of itself. In fhsim_environment 3.0 every wake is registered with the EnvironmentProvider, and every SimObject that asks the environment for a water velocity gets the waked flow. This page is the user's summary: how to place a wake by hand, the net-cage wake, how a simulated object casts its own wake (the lumped wake of phase F of the net-hydrodynamics programme), who sees a wake, the one setup rule, and how to look at a wake. The model itself (kernel, lumped sources, composition, gridded wakes, blending, threading and known limitations) is described in marenv's wake manual, doc/wake.md in the marenv repository (page Wake manual of the marenv documentation), and why other objects see one lumped source per object in marenv's doc/adr/0002-lumped-external-wake-sources.md. The vocabulary is fixed in CONTEXT.md.
A wake field returns, at a point, a velocity deficit d in [0, 1] and the source velocity u_s, the velocity of the structure that casts it. The waked current is
\[ \mathbf u = \mathbf u_{amb} + d\,(\mathbf u_s - \mathbf u_{amb}). \]
doc/wake.md § 4). A net's private per-panel field, whose panels are normalised by their local shaded inflow, is multiplied onto that composition, d_tot = 1 − (1 − D)(1 − d_own). A lumped source and its surface and seabed images compose by the same rule.Before 3.0 a wake returned a "wake factor" through GetCurrentVelocityFactor. That call is gone; a wake written against it must be rewritten to return a deficit (see marenv's doc/adr/0001-wake-deficit-in-the-source-frame.md).
PorousDiscWakeSource places one analytic porous-disc wake for a structure that is not simulated, such as an upstream cage in a farm. It registers a marenv::wake::SourceSetWake under its own name in FinalSetup and publishes one elementary source into it, with blend time 0, so it follows the rule of every other wake source (owner ruling R125). The wake is marenv's elementary kernel: a momentum-theory near wake that becomes a Gaussian far wake widening at the expansion rate k, with its surface and seabed images.
| Parameter | Size | Meaning |
|---|---|---|
Position | 3 | Source centroid, NED [m], at the setup time. Default 0, 0, 0. The source translates from here with SourceVelocity. |
Area or Diameter | 1 | The structure's own area [m²], or the footprint diameter D [m] with Area = πD²/4. Exactly one is required. |
MomentumDeficit or ThrustCoefficient | 1 | M = drag/(½ρ|U|²) [m²], or C_T with M = C_T·Area. Exactly one is required. Above C_T = M/Area = 0.96 the area is widened to M/0.96, so the wake carries the whole M, and a line is logged at setup (R125, as the casters, QF6). |
FlowDirection | 3 | Fixed wake axis, NED; normalised. If absent, the axis follows the relative flow at the source (below). |
SourceVelocity | 3 | u_s, NED [m/s]: the source and its wake translate with it, and it drags the water at d·u_s (R125). Default 0, 0, 0 (moored). |
WakeExpansion | 1 | Expansion rate k. Default 0.05, taken from the wind-farm literature and not calibrated; marenv's manual gives the sensitivity band 0.03 to 0.08. |
WakeXStartFactor | 1 | x_start = max(WakeXStartFactor·D, 0.02 m): no wake closer than this downstream; the wake ramps in by a smoothstep over 0.079 D behind x_start (to the Eq. 11 meter 0.579 D for the default; owner ruling R-b). Default 0.5. |
Size and strength have no defaults: they describe the structure you are standing in for, and the library has no number it could defend for them.
Wake axis. With FlowDirection the axis is fixed and the wake is published in FinalSetup, with the source at Position at that time; the first PreOdeFcn re-reads the seabed-image depth below the source and publishes the wake again, logged, when a bathymetry set after this object's FinalSetup (EnvironmentProvider::SetBathymetry from an object listed later) changed it (ENV-0056). Without it the axis follows the relative flow at the source, u_amb − u_s, with u_amb the ambient current from GetAmbientCurrent (never the waked flow, which would include this wake itself). The axis is re-read once before every major time step, in PreOdeFcn, so every stage of a step sees the same wake; the source is published again at that step's time, where it is then, with the seabed image below it. Until the first PreOdeFcn, while the relative flow at the source is below 1 mm/s, and while a current field fails there, the followed wake casts nothing; a clamped current (OUT_OF_RANGE_INACCURATE, a depth profile beyond its layers) is usable (R122).
From examples/input/PorousDiscWakeSource.xml, two cages 150 m upstream of a site in a 0.5 m/s current towards north, one following the current and one with a fixed axis turned 20° east:
Run it headless from a build without visualisation (cd build/no_vis/Release/playpen/bin && ./FhSim ../../../../../examples/input/PorousDiscWakeSource.xml), or with FhVis to see the two VisualFieldPlanes and the VisualWakeSources of the example.
The source moves only at the constant SourceVelocity: an object whose motion changes, or a net that should cast the wake of its own panels, casts its wake itself (below).
Standing in for a simulated object. To reproduce the frozen lumped wake of a moored body (below), copy M, the area and the rear centre from its last rebuild log line ("M = … m^2, area …
m^2, rear centre (…) m") into MomentumDeficit, Area and Position, and set WakeXStartFactor="0": a lumped wake starts at the rear face (QF7), while the default 0.5 puts the start half a diameter downstream of Position. Both use solidity 0, the surface and seabed images, the momentum-flux composition and the widening above C_T = 0.96 (QF6, R125), so the wake is the same except within 0.02 m of the rear face: the disc keeps the kernel's minimum x_start of 0.02 m (KernelParams::xStartMin), where the lumped wake starts at 0. A body moving at a constant velocity is stood in for by also copying the source velocity of that log line into SourceVelocity, with Position the rear centre at the setup time: the disc translates with it (R125), but does not follow any later change of the body's motion.
NetCageWakeField is a legacy (pre-3.0), test-only geometric wake of a moored cage, kept for TestNetcageWake and its pinned numbers; it carries no momentum and no images. Since owner ruling R127 its header is internal to the test models (src/testmodels) and not installed. It takes a clamped ambient current (OUT_OF_RANGE_INACCURATE) as usable, as every other consumer (R122), and casts no wake where the current lookup fails. New code casts a lumped wake (below) or places a PorousDiscWakeSource. It is a full-wake zone behind the cage in which the deficit is MaxDeficit, a linear taper to zero over InitialBoundaryLayer at its edge, a wake that spreads at a half-angle WakeAngleDeg, and behind the full-wake zone a deficit that falls with the square of the wake width. It is configured through the test SimObject TestNetcageWake.
In 3.0 it follows the deficit convention:
MaxDeficit (formerly MaxWakeFactor) is the deficit d in the full-wake zone, in [0, 1]; 0.2 reduces the current there to 80 %. A value outside [0, 1] is a setup error.DeficitCutoff (formerly WakeFactorCutoff) is the deficit below which the far wake is cut off; it sizes the wake's bounding box. It must be > 0; 0 or less is a setup error. It is unrelated to the cutoff of marenv's gridded wakes.GetAmbientCurrent), asked for at every query. Below 1 mm/s the cage casts no wake.Position; points above the sea surface are never waked.WakeAngleDeg must lie in (0°, 90°); FullWakeDiameter must be positive and InitialBoundaryLayer and FullWakeDepth not negative, all finite (ENV-0058; 0° gave a full-deficit wake of infinite length).(from examples/input/TestNetcageWake.xml).
A simulated object with CastWake="true" computes its wake from its own force and registers it itself. Every other object sees one lumped source per casting object: one elementary source whose momentum deficit is the object's force along its relative flow and whose width is its frontal extent (marenv MakeLumpedSource; marenv ADR 0002, owner rulings R34, R36). Casting objects:
| SimObject | Library | Force and frontal area |
|---|---|---|
| TestWakeBody | fhsim_environment | sphere_hydro's drag ½ρC_dA|u|u of a sphere, A = πD²/4, through a SphereCaster; a test and demonstration model of the one casting path. |
| TestWakeCluster | fhsim_environment | The summed drags of its spheres (sphere_hydro::BodyLoad), shaded by each other unless WakeEvaluation="None", and the frontal rectangle of the spheres; a test and demonstration model of a multi-body object (below). |
Net/Disk, Net/Sphere | fhsim_marine_elements (MARE-0127) | The body's own drag law; see that library's methods manual. |
Adaptive/CablePendulum (the ball) | fhsim_base | The ball's sphere drag, through a SphereCaster; the cable casts nothing. |
Trawl/CenterWeight | fhsim_fishery | The weight's sphere drag, through a SphereCaster. |
Net/NetStructure, Net/NetStructureWithConstraints | fhsim_marine_elements (MARE-0100, MARE-0114, MARE-0129) | The sum of the panel forces of its marching build and the frontal rectangle of its nodes (PanelSourceSet). The net also keeps a private per-panel field for its own panels, which is not registered. |
A body SimObject (a sphere, a disc, a weight) uses environment::wake::BodyFlowInteraction, the standard helper. It reads CastWake, Wake* and ExternalWakeUpdate, finds the Environment, holds the body's BodyWake and gives the water velocity without the body's own wake, the submergence of its centre (body_submergence::Submergence, R66) and the wave particle acceleration (R67). The body keeps its drag, inertia and shape; a shape caster (below) answers the wake caster for it.
A self-propelled body (a ship, an AUV; fhsim_base's Tanker and fhsim_fishery's Auv from feature propeller-jet) casts its hull wake and a propeller jet together, so that its far field carries about no net momentum when thrust balances drag (marenv ADR 0003, owner rulings O12a, O13a). It implements environment::wake::PropelledObject: a WakeCastingObject whose CurrentLoad is the bare-hull resistance, plus PropellerJet(T, X), a PropellerJetInput of the propeller centre, the shaft direction (the direction of the thrust on the body), the diameter D_p, the effective thrust T_eff = T (1 − t) and the advance speed u_a. It declares itself in FinalSetup:
With CastWake the jet is a marenv jet field (marenv::wake::PropellerJet, marenv doc/wake.md § 4.2), registered as <name>/jet and built by an internal detail::PropellerJetCaster. It has its own schedule on the body's wake parameters (A self-propelled body): the same WakeUpdate, build times, blend, filter and re-arm as the hull; Freeze converges on |ΔT_eff|/T_eff, and a frozen jet also re-arms when |ΔT_eff|/T_eff against the T_eff it was frozen with exceeds WakeRearmTolerance (owner ruling 2026-10-12). Each rebuild publishes the body's input with the filtered source velocity (the jet translates with the body between rebuilds), the surface image and the seabed image below the propeller, as the hull's lumped field. T_eff ≤ 0 casts nothing. The body never sees its own hull wake or jet (D3): its query leaves out both (WakeQuery::excludeJet). A jet cannot be cast with WakeGroup: that is a setup error. Without CastWake nothing is registered and nothing changes.
The jet belongs to the body's BodyWake: BodyFlowInteraction::FinalSetup hands the body to BodyWake::Register(environment, &body), and Wake()->Jet() is the registered jet, Wake()->JetCaster() a read-only view of its schedule and last input. The body must outlive the BodyWake (a member of the SimObject). Any BodyWake can cast a jet this way, a source set's too (SourceSetBodyWake::Register(environment, &propeller); owner ruling Q9, 2026-10-14): its jet is updated after the set's cast and take.
Use a BodyWake directly (below) for a source set (An object of many sources), for several wake parts in one object, or for a body that needs more than BodyFlowInteraction gives.
A SimObject takes part in wakes through one environment::wake::BodyWake per wake-casting part, the one public path (ENV-0102); BodyFlowInteraction holds one for a single body. The caster behind it (detail::WakeCaster, fed one body through detail::SingleBodySource, or a source set) is internal. The life cycle:
| Where | Call |
|---|---|
| constructor | SingleBodyWake(Creator, GetName(), rho) for a body, SourceSetBodyWake(Creator, GetName(), rho) for a source set: reads CastWake, Wake* and ExternalWakeUpdate (Wake parameters); rho is the fluid density of the object's load law, refused when not positive with CastWake |
FinalSetup | Register(Creator): finds the Environment (FindEnvironment); a missing one is a setup error naming the object |
PreOdeFcn | Update(T, X, caster) for a body (sampled at caster.Reference(T, X); nothing with a null X), Update(T, X, set) for a source set: the rebuild when due, then the take of the other objects' wakes at the sample points |
OdeFcn, output ports | WaterVelocity(T, point, pos, vel) (waked current plus waves) or WakedCurrent(...) (no waves, R62) |
A body hands Update a WakeCastingObject: what its caster asks of it.
Reference(T, X): the object's reference point, where the free stream is sampled, and its velocity (unfiltered).CurrentLoad(T, X, freeStream, flowDir, force, frontalArea, rearCentre): the force of the object's own law at the given free stream (no waves), its frontal area normal to flowDir and the centre of its rear face. double only, never throws.Both are asked only from PreOdeFcn, once per step, at the step's start: T is the step's start time and X the accepted state. A caster may read its owner's input ports at (T, X), so it sees the inputs of the step start and never those of an integrator stage (fhsim_workspace plan E2).
A sphere or a disc needs no code of its own for it: the shape casters answer both from the shape law and the state.
| Shape caster | Built from | Answers with |
|---|---|---|
environment::sphere_hydro::SphereCaster(sphere, positionIndex, velocityIndex) | a sphere_hydro::Sphere (ρ, D, C_d, cable diameter) and the state indices of the centre and the velocity | sphere_hydro::CurrentLoad: the drag, πD²/4, the rear face D/2 downstream |
environment::disk_hydro::DiskCaster(disk, positionIndex, velocityIndex) | a disk_hydro::Disk and the same indices; the axis from SetAxis(direction) | disk_hydro::CurrentLoad at the held axis: the drag, the face ellipse plus the edge rectangle, the rear reach |
The disc's axis is not a state of every disc (fhsim_marine_elements' Net/Disk aligns it with the force on it), so the caster holds it: call SetAxis in PreOdeFcn right before BodyWake::Update; a zero or non-finite direction gives +z, as Net/Disk does for a zero force.
Every flow lookup of the object itself goes through the BodyWake, whose Query() excludes the object's own registered field: the wake frame does not rotate, so after a reversal of the relative flow between rebuilds the object could otherwise lie in its own wake (ENV-0047). Before Register there is no environment: WaterVelocity and WakedCurrent throw std::logic_error then, and an Update before Register does nothing but log that once (ENV-0085, ENV-0102).
One rebuild of a body's lumped wake (src/environment/SingleBodySource.h):
WakeFilterTime): the source velocity u_s.EnvironmentProvider::GetWakedCurrent with WakeQuery::exclude and excludeJet, below); U_ref = free stream − u_s. A current clamped at the end of a depth profile (OUT_OF_RANGE_INACCURATE) is usable; if a current field fails at the reference point, the object casts no wake (ENV-0040, owner ruling R122; below).CurrentLoad at that free stream, along ê_U = U_ref/|U_ref|.MakeLumpedSource gives M = max(0, F·ê_U)/(½ρ|U_amb − u_s|²), normalised by the ambient dynamic pressure at the reference point, the current without any wake (lead ruling R87), so a shielded body's wake carries the drag it feels; lift is dropped. When M/A_f > 0.96 the area is widened to M/0.96 and a line is logged; when |U_ref| < 1 mm/s, F·ê_U ≤ 0 or the frontal area is not valid the object casts no wake (logged). With Periodic and WakeRelaxation α < 1, M ← αM + (1 − α)M_prev (marenv RelaxLumpedSource), except that a rebuild with no relative flow, no valid frontal area or a non-finite load resets M to 0 at once (relaxing needs the flow axis), and so does a wake dropped by a failed current lookup (below); the blend still smooths the switch in time. A rebuild with F·ê_U ≤ 0 at a valid flow is relaxed, toward 0.SourceSetWake with x_start = 0, so the wake starts at the rear face, translating from the rebuild on with the published velocity (u_s, or 0 when the rebuild that freezes snaps it, R50), blended over WakeBlendTime. One log line per rebuild gives M, the area, the rear centre, the relative flow and the source velocity.While the ambient current lookup at the reference point fails (ENV-0082, owner ruling R122 (ii)), the object builds no wake and no rebuild is counted, so a Freeze round neither ends nor freezes on it; the caster only looks the current up again every step. The wake published last is dropped at once (an empty publish with blend time 0, the one unblended switch), but only once no blend runs, since a publish during a blend is refused (R22); until then it stays. One line names the position when the failure starts and one says when the lookup recovers; if the wake was dropped, the recovery makes a rebuild due at once (Freeze re-arms, logged; Off builds once more; Periodic rebuilds and restarts its period), counted and blended as usual.
The force in step 3 is evaluated at the object's own state velocity, while U_ref and the field's source velocity use the filtered u_s. For a fixed, moored or steadily moving object the two agree; during an acceleration M is slightly inconsistent with U_ref. This is kept and documented (owner answer QF12).
Nothing enters the Jacobian: CurrentLoad is the double evaluation of the law and the field depends on time only. Between rebuilds the field translates with u_s and does not rotate; a turning object or a turning current is followed by rebuilding (Periodic, or Freeze's re-arm, below).
An object made of many sources that shade each other is a environment::wake::WakeSourceSet, handed to a BodyWake of BodyWakeKind::SourceSet (ENV-0093, ENV-0095). Velocities are filtered on carriers, the points that move; each source is the mean of 1 to 3 carriers and reports its load at its local, shaded inflow (EvaluateSource, a SourceLoad). The caster keeps a private field of one elementary source per source, built in one marching pass in flow order and sampled only by the object's own points (through BodyWake::Query()), and registers a lumped field of the summed forces for every other object. Its parameters add WakeEvaluation, WakeLength, WakeGrid and WakeXStartFactor (The private field of a source set). A step with a non-finite carrier is skipped, as for a body (ENV-0074). The rebuild, in full: src/environment/WakeCaster.h. A body's caster is this caster with one source and no private field (WakeEvaluation::None, ENV-0094). WakeEvaluation="None" selects that mode for a source set too (ENV-0098): no private field, the sources do not shade each other, the lumped field of their summed loads only.
Two adapters make an object a source set:
WakePanelSource (a net, ENV-0102; moved from fhsim_marine_elements): the nodes are carriers, each panel a source of its three nodes with the load of the structure's panel load law (PanelEvaluation), the panels and then the cable elements are sample points, and the rebuild line names the cable drag the lumped source leaves out (QF9);IndexedWakeCastingObject for an object of rigid bodies, one source per body (below, "Multi-body objects").**Grid or Direct for the private field** (ENV-0108, owner ruling 2026-10-13: document only). WakeEvaluation defaults to Grid for every source set, an IndexedWakeCastingObject included; only a WakeGroup defaults to Direct, and a consumer that casts a few bodies sets Direct itself (fhsim_marine_elements' trawl rig, TrawlCable and RundDorg, does). The grid is sized from the sources, cells up to two mean source diameters along the flow, so x_start (0.5 D) is a fraction of a cell, and it is anchored at the area-weighted mean source position. A body's own deposit at the nodes around it enters its sample at its centroid by trilinear interpolation, by an amount that depends on where it sits in the node lattice. For a sparse set (a few bodies tens of metres apart) the Grid result therefore depends on the order of the bodies in the input and breaks a mirror symmetry the physics has: a symmetric trawl rig's door positions after 1 s differ by 0.1 to 5 mm between the mirror images and the two input orders with Grid, as much as the wake effect itself, and within 2e-5 m with Direct. Use Direct for a few bodies far apart (cheap: N² per build, N per sample, and exact; it also leaves a body's own source out, Who sees the wake); keep Grid for a net, whose dense panels make the cells comparable to the panels and whose direct sum would cost N per sample (no net is known to be affected). The mechanism is not isolated further.
environment::wake::WakeSchedule decides when a caster rebuilds; every caster shares it. Its parameters (WakeUpdate, WakeBuildTime, WakeIterations, WakeIterInterval, WakeTolerance, WakePeriod, WakeRelaxation, WakeBlendTime, WakeRearmTolerance, WakeRearmDistance, WakeFilterTime), with the kernel's WakeExpansion and WakeCutoff, their defaults and their setup errors are listed in The common parameters. What they do:
A rebuild that is due while the previous blend still runs waits, and the wait is logged once (R22).
With WakeBlendTime = 0 a fresh rebuild has full weight from its own start (marenv's BlendWeight returns 1 for a zero duration), so a caster that rebuilds later in the same PreOdeFcn pass sees a different upstream wake than one that rebuilds earlier: the result depends on FhSim's PreOdeFcn order, which is arbitrary (design §2.4). A positive blend time removes the dependence, because a fresh publish starts at zero weight and only the previous wake is seen until it fades in.
Freeze and a changing current (ENV-0039, owner ruling R42 item 37). A frozen wake is re-armed by either of two drifts, each against the relative flow speed at the freeze (at least 1 mm/s) with the same WakeRearmTolerance:
EnvironmentProvider::GetAmbientCurrent at the reference point for a body, at the area-weighted mean node position for a net; never the waked flow, R15): a moored aquaculture cage, a mooring buoy or a fish farm under a tidal or turning current, a current that strengthens or slackens, and a towed net that tows into a current of another speed or direction.The ambient current is low-passed with WakeFilterTime, as u_s is, so a current that jumps re-arms a little after the jump, and the filtered value can still be moving when the new round freezes; that can start one or two further rounds until it settles (a 90° turn of a 0.3 m/s current with the defaults: six rebuilds after the turn). A structure frozen in still water (relative flow speed 0) re-arms once a current of WakeRearmTolerance × 1 mm/s appears, so a current ramping up from zero makes Freeze rebuild every time the current has grown by WakeRearmTolerance of the flow at the last freeze. Periodic remains the choice when the current changes all the time, e.g. a tidal cycle resolved over the whole run; Freeze keeps the wake fixed between the changes. A source set (a net) reads further parameters (WakeEvaluation, WakeGrid, WakeLength, WakeXStartFactor, The private field of a source set); they act on its private field only.
Freeze and a structure that moves against its frozen wake (owner ruling R50; deep review wake-casting.md A1). A frozen wake moves at the source velocity of its last build for ever. Two guards keep it with the structure:
WakeRearmTolerance × the relative flow speed (at least 1 mm/s), the wake is published with u_s = 0 (logged "snapped to 0
at the freeze"). A moored cage or buoy that freezes while the transient still leaves a few mm/s in its filtered velocity keeps its wake in place; before R50 the shipped cage example's wake drifted 5.8 m in an hour and lost 88 % of its self-shading.WakeRearmDistance × √(frontal area) apart re-arms Freeze (logged "moved … from its
frozen wake"). A towed body or net whose speed differs from the frozen u_s by less than WakeRearmTolerance would otherwise slide away from its wake without limit (a 2.2 % lag at 2 m/s: 37 m after 900 s).Rows of casting structures: a farm (owner ruling R121). A structure sees the casting structures upstream of it through their lumped wakes, so a change reaches one row further per rebuild: the third row's lumped wake depends on the second row's, which depends on the first row's. With Freeze, an N-row farm therefore needs WakeIterations ≥ N, so that the last row rebuilds after the rows upstream of it have settled; the default 4 (owner ruling R130) covers up to four rows, a longer farm sets it. Every caster converges on the wake it casts for the others: a body on |ΔM|/M, a net on the larger of its panels' max|ΔR| and its lumped |ΔM|/M, so a row does not freeze while its lumped wake still changes; a round that runs out of iterations is logged "without converging".
Freeze and an object that oscillates (ENV-0109, owner ruling 2026-10-13: recommend, do not change the default). Freeze assumes the object settles. An object that never does (trawl doors that swing, a rig or a cage in waves, a body whose speed oscillates) changes its loads by more than WakeTolerance between rebuilds WakeIterInterval apart, so no round converges ("wake frozen at
t = … s after N rebuilds without converging"), and an oscillation of the filtered source velocity beyond WakeRearmTolerance of the flow re-arms the next round at once. Freeze then rebuilds about every WakeIterInterval, with a blend and three log lines per round (fhsim_marine_elements' TrawlCableWakes fixture and fhsim_fishery's SteadyTow with CastWake: about 60 re-arms in 300 s): the cost and the wakes of a 5 s Periodic schedule without its predictability. For such an object set **WakeUpdate="Periodic"** with a WakePeriod that suits its motion (a fraction of the time the object takes to move x_start against its wake; at least WakeBlendTime), and WakeRelaxation below 1 if the rebuilds should damp the oscillation of M. The default stays Freeze, which suits a structure that settles (a moored cage in a steady current, a net towed at a steady speed); The common parameters.
**WakeBuildTime stays 0** (lead decision R51). The first build is then on the undeformed initial structure and at least one rebuild follows during settling; with the snap and the distance re-arm of R50 the transient's residual velocity no longer carries the frozen wake away. A later WakeBuildTime (the cage example uses 20 s) still saves the settling rebuilds.
TestWakeBody is a kinematic sphere that moves at a constant TowVelocity (its Velocity state) from its initial Position (zero holds it fixed) and casts a lumped wake with CastWake, through a BodyWake and a sphere_hydro::SphereCaster, with no wake code of its own (ENV-0102): the reference of the bare BodyWake path. A body SimObject uses BodyFlowInteraction, the standard path (A single body: BodyFlowInteraction), which holds such a BodyWake. Its drag F = ½ρC_dA|u|u on u = u_water − TowVelocity, A = πD²/4 (sphere_hydro::DragForce), does not act on its motion. M = C_dA for this law; the source sits D/2 downstream of the centre, where its wake starts, so the centre is never inside the body's own wake. Its ports are Position, WaterVelocity (the waked flow at the centre, waves included, its own field excluded, BodyWake::WaterVelocity) and Force. Parameters: Diameter (1 m), DragCoefficient (0.5, a test value), Rho (1025 kg/m³), TowVelocity (0, 0, 0) and the wake parameters of a body (Wake parameters). The test input is tests/in/TestWakeBody/TestWakeBody_in.xml.
The EnvironmentProvider is a marenv::EnvironmentFacade, and the wake is applied inside it.
The one rule. The water velocity a SimObject receives is the live ambient current, with the composition of every registered wake field except the asker's own (plus its private field) applied, plus the live waves. A selector, marenv::WaterVelocityTerms, drops the waves and/or the wakes; the default, Full, keeps all three terms, so a SimObject need not know that wakes or waves exist (a propeller jet counts as a wake). The plain query knows no asker and composes every registered field, exact and live: it is the reference of the rule. A helper (BodyWake, BodyFlowInteraction, ElementFlowInteraction) leaves the asker's own wake out and may hold the wake part for the step at a cutoff tier (The wakes, once per step: ExternalWakeUpdate): bounded approximations of the same rule (R46, R47), and ExternalWakeUpdate="Evaluation" gives the reference bit for bit. So plain queries and helpers differ only by the asker's own wake and by declared accuracy settings, never by which terms are in. All of them end in marenv's SumWaterVelocity, one arithmetic; a helper's held wake reaches it through GetWaterVelocity(time, pos, parts, held, point, vel).
| Query | Waked? |
|---|---|
GetWaterVelocity(time, pos, terms, vel) | As terms says: Full (current, wakes, waves; = GetParticleVelocity), NoWaves (current, wakes; = GetCurrentVelocity), NoWakes (current, waves), CurrentOnly (= GetAmbientCurrent). Exact and live. |
GetParticleVelocity | Yes: waked current plus wave particle velocity (Full). |
GetCurrentVelocity | Yes: waked current (NoWaves). New code asks GetWaterVelocity(…, NoWaves, …). |
PointEnvironmentQuery | Yes, in its current and particle velocities. |
GetWakedFlow | Yes; also returns the ambient current, d_tot and the number of contributing wake fields. |
GetWakedFlow with a marenv::wake::WakeQuery | Yes, with the registered field query.exclude and the registered jet query.excludeJet skipped and the unregistered field query.own added. A casting object's own flow query. |
GetWakedCurrent(time, pos, query, vel) | The waked current only, never the waves, composed as the query says. The free stream a wake caster needs at its own reference point. |
GetAmbientCurrent | No: the sum of the current fields without any wake (CurrentOnly). For a wake field that needs the flow at its own source. |
BodyWake::WaterVelocity(T, point, pos, terms, vel), BodyFlowInteraction::WaterVelocity(T, position, terms, velocity) | As terms says, the object's own wake always left out; without terms, Full. |
GetWakeDeficit | Returns d_tot alone; no jet is sampled. |
So every SimObject that asks for a water velocity sees every registered wake, without knowing it exists: in fhsim_marine_elements the cables, Sphere, Disk, the net structures and the trawl cable constraint sets, and a second cage downstream of the first. The wake applies even when no current field is configured (the ambient current is then zero, and a moving source still drags water along).
What is registered is the lumped wake of each casting object. A casting net's own panels and cables also see its private per-panel field, through their query; no other SimObject does, so a separate SimObject inside a net's volume sees no internal wake of that net (owner answer QF8). Every BodyWake excludes its own lumped field from the object's own flow query (BodyWake::Query(), ENV-0047): fhsim_marine_elements' casting nets, Net/Disk, Net/Sphere, fhsim_base's Adaptive/CablePendulum, fhsim_fishery's Trawl/CenterWeight, TestWakeBody and TestWakeCluster. WakeQuery::own must not name a registered field, or that field counts twice (marenv; documented, not checked).
Propeller jets. A self-propelled body's jet (A self-propelled body: hull wake and propeller jet) is a registered jet field, not a wake field: the jets' excess velocities are summed and added after the deficits, u = u_amb + d_tot (u_s − u_amb) + Σ Δu_jet (marenv ADR 0003). It belongs to the wakes term, so every query with the wakes (Full, NoWaves) sees it and NoWakes, CurrentOnly do not. Every receiver sees the other objects' jets: bodies, nets and their cables, cables through an ElementFlowInteraction, trawl doors; the helpers take them with the wakes, once per step under ExternalWakeUpdate="Step" (held while a jet blends, R136) or live under Evaluation, at the receiver's cutoff tier relative to each jet's velocity scale. Its owner's query leaves the jet out (query.excludeJet), and so does an ElementFlowInteraction built from the owner. A wake caster's free stream at a rebuild is the ambient current with the other objects' wake fields and jets (owner ruling 2026-10-12): a body, a net or a group in another object's jet builds its source inflow, its load and its relative flow U_ref from the jetted flow; its own jet is left out (D3). The R87 normalisation keeps the ambient current without any wake or jet, so a jet enters M only through the shaded dynamic pressure where that is the larger (R123). Without a registered jet every result is bit for bit that of before.
A caster never sees its own wake. A SimObject that holds a BodyFlowInteraction (or a BodyWake) asks it for its water velocity, also for the special terms, e.g. m_flow.WaterVelocity(T, position, marenv::WaterVelocityTerms::NoWaves, velocity); never Environment()->GetParticleVelocity or GetWaterVelocity. The plain query would see the object's own lumped field: zero at a compact body's own reference point (the lumped wake starts at its rear face), but not after a frozen wake is overtaken or the body turns (BodyFlowInteraction.WaterVelocityExcludesTheOwnWakeAtAPointBehindTheBody).
A source-set body leaves its own private sources out (ENV-0107, owner ruling 2026-10-13, "direct-only exclusion"). A source-set object (net panels through PanelSourceSet, the bodies of an IndexedWakeCastingObject such as trawl doors and clump weights, the members of a WakeGroup) composes its private field in, but each sample point leaves out its own sources: those that carry every carrier of the point. A body owns its own source, a net's panel its own panel, a cable element of a net the panels of the edge it lies on (none when it lies off the panels); a point at a node would own the panels that carry the node. Without this, a body that has moved more than x_start (half a source diameter by default) downstream of its frozen elementary source felt its own deficit, a drag drop with no physical cause; Freeze re-arms on the mean position of the whole object (WakeRearmDistance × √(W·H)), which for a wide rig is far more than a door's x_start, and not at all when the bodies oscillate about a fixed mean. The other sources' frozen wakes still lag by the same distance until the next rebuild.
Only WakeEvaluation="Direct" leaves the own sources out. A Grid private field stores only the summed log ratio of all sources at its nodes, so one source's share cannot be taken out; with Grid a source-set body can still see its own private source after overtaking its frozen wake (and its own interpolated deposit; The private field of a source set, ENV-0108). Use Direct for a few bodies far apart (a trawl rig, a WakeGroup, the default there) and keep the rebuilds frequent enough for a net.
The other objects' registered wakes and a casting object's own private field change only at their rebuilds (blended over WakeBlendTime) and through the query point, yet a live query composes them again at every Runge-Kutta stage, at every analytical Jacobian and at every call of a numerical Jacobian (deep review wake-performance.md A2). environment::wake::ExternalWakes takes that composition once per integrator step instead, in PreOdeFcn at the step's start time and positions, and reuses it in every evaluation of the step: the other objects' wakes (owner decision R46) and the object's own private field, sampled after them (owner ruling R135). The ambient current and the waves stay live.
ExternalWakeUpdate | The other objects' wakes and the own private field are composed |
|---|---|
Step (default) | once per step per sample point, held for the step |
Evaluation | in every evaluation, as before R46 (the reference) |
While a field blends after a rebuild, the take holds the samples of its two snapshots at the step's start positions, and every evaluation weighs them with the blend weight of its own time (marenv WakeField::SampleParts, owner ruling R136): the held wake follows the blend's C¹ smoothstep within the step, as the live query does, so a multistep integrator sees no jump at its step boundaries, and a retried step composes the same samples at its own times. Outside a blend the take holds the finished composition.
The difference to Evaluation is the motion of a sample point within one step against the metres-wide lumped wakes and against the frame of the own private field (which translates with the source velocity u_s: |v_p − u_s| h per step): of order 1e-4 relative (accepted, R46 and R135). It grows with the step size, so a large implicit or StableSolver step lags more; Evaluation stays the live reference. An integrator with one evaluation per step at the step's start (Euler_i, StableSolver_i) evaluates at the take and gives the numbers of Evaluation. A numerical Jacobian then sees no ∂u_wake/∂x of the wakes, which the analytical Jacobians leave out anyway; with the own field held, the analytical Jacobian of a casting net is consistent with its right-hand side within a step. The other objects' fields are those published when the object's PreOdeFcn runs, so a caster that rebuilds later in the same pass is seen one step late, within its blend; the object's own caster updates before its take, so its rebuild is held from that step on. An object that casts no private field and is alone in the environment, or whose query excludes the only registered field, has nothing to hold and gives the numbers of Evaluation bit for bit.
The take composes only the registered fields that may reach the sphere around the object's sample points (marenv SelectWakes, exact tier, owner decision R47), held with WakeSelection::Hold: bit for bit the composition of every field, without the cost of the fields that cannot reach the object. ExternalWakes::Take(environment, T, points, query, cutoff) with cutoff > 0 selects by the cutoff tier instead (R47): it also leaves out a field whose deficit stays below cutoff over the sphere, so the composed deficit changes by less than cutoff per field left out. The default, cutoff = 0, is the exact tier; no shipped receiver uses it: every BodyWake passes its R47 tier (ReceiverCutoff), its WakeCutoff when it casts, else 1e-4.
ExternalWakes::WaterVelocity gives the waked flow with the wave particle velocity; ExternalWakes::WakedCurrent gives the same waked current without the waves (owner ruling R62), for a model that takes the wave kinematics per node and averages them to a panel centroid (EnvironmentProvider::GetWakedCurrent with the held composition underneath). WaterVelocity = WakedCurrent + the wave particle velocity, bit for bit; both keep the sum of the current fields that answer with a usable status, a clamped depth profile included (owner ruling R122, ENV-0064).
The parameter is read by every BodyWake (Receiving): fhsim_marine_elements' Net/NetStructure, Net/NetStructureWithConstraints, Net/Disk and Net/Sphere, fhsim_base's Adaptive/CablePendulum, fhsim_fishery's Trawl/CenterWeight, TestWakeBody and TestWakeCluster.
A cable meets the other objects' wakes at one point per element but casts none. environment::wake::ElementFlowInteraction (ElementFlowInteraction.h, feature cable-flow, 2026-10-13; before it WakeReceiver, fhsim_marine_elements MARE-0223, owner ruling R-c) is the element counterpart of BodyFlowInteraction, with the contract of a net's panels and cable elements: it reads ExternalWakeUpdate (Receiving), finds the Environment in FinalSetup (FinalSetup(name, creator)), takes the other objects' wakes once per step in PreOdeFcn at the points of the step start (Take(T, points), 3 coordinates per point) with the reach selection on the sphere around the points, and answers WakedCurrent (no waves) or WaterVelocity (with them) per point in OdeFcn. The points are the cable's (Cables: the element points). The ambient current and the waves stay live; a point that moved within the step keeps the deficit of its start position, applied to the live current at its position, as a panel does.
A cable of its own takes at the non-casting cutoff tier 1e-4 (kNonCastingCutoff, R47) and reads no WakeCutoff. A cable that belongs to a body is built from the body's BodyWake, ElementFlowInteraction(const BodyWake& owner): it receives with the owner's query (BodyWake::Query(), so it does not feel the owner's own wake or jet), at the owner's cutoff tier (ReceiverCutoff: its WakeCutoff when it casts) and with the owner's ExternalWakeUpdate (ENV-0110). With ExternalWakeUpdate="Evaluation" it composes every registered field (but the owner's own) live in every evaluation: bit for bit GetCurrentVelocity and GetParticleVelocity for a cable of its own.
WakeReceiver (WakeReceiver.h) is removed (3.2.1, ENV-0111); its ReceiveAs(query) took 1e-4 whatever the owner (ENV-0110).
The cost it removes: a live query samples every registered field at every element in every evaluation (about 90 ns per field with no seabed, 210 ns with a seabed image, 360 ns in a blend), so a farm of 1,500 cable elements and 50 fields spent tens of ms per step; the take samples the reaching fields once per step.
environment::wake::BodyWake (ENV-0095, design R3; the one public path since ENV-0102, The interface: BodyWake) bundles the above for a SimObject: the settings, the object's density, the caster with CastWake, ExternalWakes at the sample points and the receiver's cutoff tier (R47). The four calls of The interface: BodyWake are the whole path: SingleBodyWake(Creator, GetName(), rho) or SourceSetBodyWake(Creator, GetName(), rho) in the constructor (it reads the settings of its kind; settings of the other kind are a setup error), Register(Creator) in FinalSetup, Update in PreOdeFcn and WaterVelocity or WakedCurrent in OdeFcn. A body passes Update(T, X, caster), sampled at caster.Reference(T, X); a source set passes itself, Update(T, X, set), its sample points the means of WakeSourceSet::SamplePointCarriers. Both do nothing with a null X, so the caller needs no if (X) guard. The body must outlive the BodyWake at the same address (a member of the SimObject, or a std::optional emplaced once): a WakeGroup keeps a pointer to it, and a temporary is refused at compile time.
Two further forms stay for special cases. Update(T, X, body, centre) samples at a centre of the caller's (a null body only receives and needs no X; with a body that casts, a null X does nothing). The two-step form, ReadBodyWakeSettings(creator, kind) once and then SingleBodyWake(name, settings, rho, logger) (or SourceSetBodyWake) with Register(environment), lets N BodyWakes of one object share one read of the settings under names with a suffix. Field() and LastSource() report the cast wake of either kind; Caster() is a read-only CasterView (CasterView.h) of the caster's schedule and last build (a group member's shows the group's caster).
A SimObject made of several rigid bodies (a cluster of floats, a string of buoys, a mooring with clump weights) has three ways to cast and receive wakes (ENV-0098, design R4). Every other object sees what the SimObject registers; the bodies see each other as the option says:
| Option | What the other objects see | What the bodies see of each other | Limits |
|---|---|---|---|
1. One BodyWake (BodyWakeKind::Single) per body, names with a suffix, the settings read once (ReadBodyWakeSettings) | N lumped sources, N registered fields | each other's lumped wakes, which reach one body further per rebuild | a chain of N bodies along the flow needs WakeIterations ≥ N under Freeze; with WakeBlendTime="0" one pass works, but the result depends on FhSim's PreOdeFcn order; N log streams; each body re-arms Freeze on its own √(πD²/4), a few centimetres for a small float in waves |
2. IndexedWakeCastingObject with WakeEvaluation="None" and one BodyWake (BodyWakeKind::SourceSet) | one lumped source of the summed loads | nothing: no shading inside the object | a chain across the flow is cast as one axisymmetric source at its frontal rectangle (no caster publishes a line source) |
3. IndexedWakeCastingObject with WakeEvaluation="Grid" or "Direct" (the default Grid) and one BodyWake (BodyWakeKind::SourceSet) | one lumped source of the summed, shaded loads | the private field of one elementary source per body, built in one pass in flow order, so the shading inside the object converges in one build | Direct is the reference and cheaper for a few bodies, Grid for many; the solid-body near wake below |
environment::wake::IndexedWakeCastingObject asks four things of the object: NumBodies(), Body(X, i) (centre and velocity, CastState(position, velocity)), BodyRadius(i) (the source's area π r² and its share of the frontal rectangle, which is widened by r) and BodyLoad(X, i, bodyVel, waterVel), the body's SourceLoad at an inflow. For a sphere, environment::sphere_hydro::BodyLoad(sphere, bodyVel,
waterVel) gives the drag at that inflow, M = MomentumDeficit() = C_d A (1 − d/(πD)), which is F·ê/(½ρ|U|²) for a drag along the flow, the area πD²/4 and solidity 0. Call BodyWake::Update(T, X, object) in PreOdeFcn and BodyWake::WaterVelocity(T, i, pos, vel) for body i in OdeFcn. TestWakeCluster is the worked example. Separate body SimObjects (a string of Net/Spheres) cast as option 3, or 2, through a WakeGroup (below, "Wake groups").
Limits of options 2 and 3:
WakeXStartFactor 0.5 the wake starts half a diameter downstream, at a sphere's rear face. Whether it holds 1 to 4 diameters behind a solid sphere, and for overlapping bodies (spacing below one diameter), has not been validated. The private deficits compose by the product rule, as a net's panels do (owner rulings R88, R95).WakeCastingObject takes its load at its state velocity (QF12). The two agree in steady motion.For every option:
marenv::IsUsable(status). The casters and BodyWake do it; a model that also calls GetAmbientCurrent, GetWakedFlow or GetParticleVelocity itself treats OK and OUT_OF_RANGE_INACCURATE (a current clamped at the end of a depth profile) as usable and any other status as no answer (owner ruling R122).cable_hydrodynamics::MorisonInertiaForce is for slender members only: it drops the axial part of the inertia force. A sphere takes its added mass and its Froude-Krylov plus added-mass excitation from environment::sphere_hydro (AddedMass, ExcitationFactor) and its wet fraction from environment::body_submergence (SphereWetFraction, gated by MaxWaveElevation(), owner rulings R66, R67).TestWakeCluster is a kinematic cluster of NumBodies spheres of one Diameter that move at a constant TowVelocity (zero holds them fixed), body i initially at i × Spacing unless the initial condition <Name>.Position sets the 3 × NumBodies centres. Each sphere's drag is sphere_hydro::BodyLoad's, F = ½ρC_dA|u|u on u = u_water − TowVelocity; it does not act on the motion. With CastWake the cluster casts as option 3 (Grid or Direct) or option 2 (None). Its ports are Position, WaterVelocity (per sphere, the other objects' wakes and the cluster's private field included, waves included) and Force, each 3 × NumBodies. Parameters: NumBodies (3), Spacing (2, 0, 0 m), Diameter (1 m), DragCoefficient (0.5, a test value), Rho (1025 kg/m³), TowVelocity (0, 0, 0) and the wake parameters of a source set. The test input is tests/in/TestWakeCluster/TestWakeCluster_in.xml.
A WakeGroup SimObject casts the wake of separate sphere and disc SimObjects as option 3 (Direct by default, or 2, WakeEvaluation="None") casts that of one SimObject's bodies (fhsim_workspace plan wake-groups-simobject.md, option B). A body with CastWake="true" and WakeGroup="<name>" casts no wake of its own; its BodyWake joins the group in Register, and the group is a IndexedWakeCastingObject over the members' shape casters (Radius() and BodyLoad() of SphereCaster and DiskCaster):
Grid, O(N²) with the default Direct, whose direct sum costs 15 ns per member and query (0.14 ms per build for 50 spheres).WakeIterations ≥ N (R121). Inside the group the deficits compose by the product rule, between separate casting objects by the momentum-flux rule: switching grouping on changes the results (owner ruling 2026-10-12: accepted). A member never feels the group's lumped field (its query skips it, ENV-0047). Spheres and discs may share a group (owner ruling 2026-10-12).WakeEvaluation is Direct by default** for a group, where a source set has Grid (owner ruling 2026-10-12): over a long, sparse group the sized default grid smears the members' wakes upstream (a chain of 50 spheres 3 m apart: the unshaded front sphere lost 20 % of its drag), and the direct sum is exact and cheap for the few bodies of a group. Grid and None remain an explicit choice.Wake* of a source set, Wake groups); CastWake must be true on it. A member's own schedule parameters do not act; its ExternalWakeUpdate and receiver WakeCutoff do. Every member has the group's density (a setup error otherwise).PreOdeFcn at a new time rebuilds the group from every member's state, and every member takes after it, so the result does not depend on the order in the input (FhSim calls PreOdeFcn in that order; tests WakeGroup_Test.cpp), as long as the members' states do not change within the PreOdeFcn pass (the second caveat below). The members hand over their bodies in their first PreOdeFcn, so the group casts from the second step on. A disc's axis is the one its SimObject set before the rebuild, the previous step's when the group or an earlier member rebuilt first. A member that hands over no body (a null body, or no Update) keeps the group from casting; the group logs a warning naming it once.IStateUpdater in the same PreOdeFcn pass after the group updated is not seen by the group until the next time: then the result depends on whether that object runs before or after the first of the group and its members.NetStructure (a source set) with WakeGroup is a setup error.Wake fields are registered in FinalSetup, and nothing may query a velocity in FinalSetup. FhSim calls FinalSetup of the SimObjects in turn, so a SimObject that asks for a velocity there sees the wakes of only those sources whose FinalSetup has already run. Query velocities from the first time step on (PreOdeFcn or OdeFcn). The registry of wake fields is not synchronised: register and remove wake fields during setup only, never while other threads query.
VisualFieldPlane (or VisualFieldVolume) with Quantity = "Deficit" colours a plane by the combined deficit d_tot of all registered wakes (GetWakeDeficit, which leaves the jets out), in [0, 1]; it was called WakeFactor before 3.0. Quantity = "Speed" with Terms = "Jet" shows the propeller jets alone, with Terms = "Wake, Jet" everything the wakes and jets add to the current, and the default Terms the waked flow itself. A quantity of the wakes and jets alone is sampled again only when a wake changes (Refresh Auto is OnChange: a rebuild, a blend, or a wake that travels with a moving source, at most every RefreshPeriod); the Wake part also follows the current, which OnChange takes as steady. VisualWakeSources draws where the wakes come from: each source as a cross of its footprint diameter, each jet axis and line source as a line, each gridded wake's grid as a box. Both show the registered wakes only: a net's private per-panel field is not registered, so the wake inside and just behind a casting net is not on the plane, and only its lumped source is drawn. From the example:
A PorousDiscWakeSource or a NetCageWakeField costs one evaluation of its formula per velocity query, and each registered wake adds one.
Measured costs are from the net-hydrodynamics benchmark (WP-B6, MARE-0102), reported on the page "Net hydrodynamics: benchmark of load laws and wake evaluation" in fhsim_marine_elements (doc/user/validation/benchmark.md, Doxygen page fhsim_marine_elements_net_benchmark, § 10); marenv's wake manual (doc/wake.md § 5.4 "Cost") has the tables. The Doxygen builds share no tag file, so neither page is linked from here. One core of a shared machine, CPU time: ratios are good to about ±10 %, absolute times to about ±35 %.
The defaults are unchanged: the owner kept WakeCutoff 1e-3 and WakeGrid 96 × 48 × 48, both user-settable (owner answer to Q12, 2026-09-27, after the phase F re-benchmark; marenv MENV-0008). The benchmark proposed WakeCutoff 1e-5 with the default grid and gridded evaluation kept, and for Periodic runs with a short WakePeriod cutoff 1e-4 or, for a small net, the direct evaluation.
The figures above are from before phase F, when a net registered its per-panel field. Since then (the benchmark page's § 11, WP-F6, MARE-0125) a net's gridded field is private and covers only the structure, and other objects sample one lumped source per casting object: one query of a lumped field costs 0.035–0.038 µs, and a second cage 20 m behind a first gets its drag within 0.5 % of the direct evaluation at every grid and cutoff. F6 recommended WakeCutoff 1e-4 with the default grid; the owner kept 1e-3 (Q12).