|
FhSim
3.1.0
Marine systems simulation
|
A wake is the disturbance a structure leaves in the flow downstream of itself. In fhsim_environment 4.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 4.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. 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 VisualFlowPlanes 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-4.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 4.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 in 4.0:
| SimObject | Library | Force and frontal area |
|---|---|---|
| TestWakeBody | fhsim_environment | Quadratic drag ½ρC_dA|u|u of a sphere-like body, A = πD²/4; a test and demonstration model. |
| 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 4.0 (MARE-0127) | The body's own drag law; see that library's methods manual. |
Net/NetStructure, Net/NetStructureWithConstraints | fhsim_marine_elements 4.0 (MARE-0100, MARE-0114, MARE-0129) | The sum of the panel forces of its marching build and the frontal rectangle of its nodes. The net also keeps a private per-panel field for its own panels, which is not registered. |
A SimObject casts a lumped wake by implementing environment::wake::WakeCastingObject and owning one environment::wake::LumpedWakeCaster:
Reference(X): the object's reference point, where the free stream is sampled, and its velocity (unfiltered).CurrentLoad(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, no side effects, never throws.LumpedWakeCaster(name, settings, rho) takes the SimObject name (the field is registered under it), the wake settings from ReadWakeSettings and the object's fluid density Rho; it throws std::invalid_argument if rho is not positive. Call Register in FinalSetup, Update in PreOdeFcn, and nothing in OdeFcn.LumpedWakeCaster::OwnQuery(), the query that excludes its own registered field (EnvironmentProvider::GetWakedFlow(T, pos, query, flow), ExternalWakes::Take and WaterVelocity): 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).One rebuild (include/fhsim_environment/LumpedWakeCaster.h):
WakeFilterTime): the source velocity u_s.EnvironmentProvider::GetWakedCurrent with WakeQuery::exclude, 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 implements environment::wake::WakeSourceSet and owns one environment::wake::WakeCaster (ENV-0093, moved from fhsim_marine_elements' net caster, which is now a wrapper over it). 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 (WakeCaster::Query()), and registers a lumped field of the summed forces for every other object. Its parameters are those of the schedule below plus WakeEvaluation, WakeLength, WakeGrid and WakeXStartFactor (ReadSourceSetWakeSettings). A step with a non-finite carrier is skipped, as for LumpedWakeCaster (ENV-0074). The rebuild, in full: include/fhsim_environment/WakeCaster.h. LumpedWakeCaster is this caster with one source and no private field (WakeEvaluation::None, ENV-0094), bit for bit its build of before. 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. An object of rigid bodies implements the simpler IndexedWakeCastingObject, one source per body (below, "Multi-body objects").
environment::wake::WakeSchedule decides when a caster rebuilds; it is shared by LumpedWakeCaster and WakeCaster. environment::wake::ReadWakeSettings reads the common parameters of every caster (contract C3; defaults by lead ruling R27, choices, not fitted values). Every parameter is read whether or not CastWake is set, and the checks run only with CastWake; without it, each one given in the input is logged as "ignored: CastWake is not true" (ENV-0070), since a receiver with, say, WakeUpdate="Periodic" would otherwise run without a word:
| Parameter | Default | Meaning | Setup error when |
|---|---|---|---|
CastWake | false | Register and build the wake. | – |
WakeUpdate | Freeze | Off: one build at WakeBuildTime, never rebuilt (once more after a failed current lookup dropped the wake, ENV-0082). Freeze: a build at WakeBuildTime and up to WakeIterations more until converged, then frozen; re-armed when the source velocity or the ambient current at the source drifts. Periodic: a build every WakePeriod. | not Off, Freeze or Periodic |
WakeBuildTime | 0 s | Time of the first build. | not finite |
WakeIterations | 4 | Freeze: rebuilds after the first (owner ruling R130; 2 before). An N-row farm of casting structures needs N or more (owner ruling R121, below), so the default covers up to four rows. | negative |
WakeIterInterval | 5 s | Freeze: time between those rebuilds. | ≤ 0 |
WakeTolerance | 0.01 | Freeze converges when the caster's change measure is below it: |ΔM|/M for a lumped caster; for a source set with a private field (a net) the larger of max|ΔR| at the sources (a net's panel centroids) and the |ΔM|/M of its lumped source (owner ruling R121). | ≤ 0 |
WakePeriod | 10 s | Periodic: time between rebuilds. | ≤ 0 |
WakeRelaxation | 1 | Periodic: α in (0, 1], M = αM_new + (1 − α)M_old. | outside (0, 1] |
WakeBlendTime | 2 s | Smoothstep cross-fade of each rebuild; with 0, casters that rebuild in the same step see each other in PreOdeFcn order (arbitrary; a positive blend time removes the dependence). | negative, or above WakePeriod (Periodic) or WakeIterInterval (Freeze) (lead ruling R22) |
WakeRearmTolerance | 0.1 | Freeze: drift of the filtered source velocity, or of the filtered ambient current at the source, from its value at the freeze, relative to the relative flow speed then (at least 1 mm/s), that re-arms Freeze (logged). | ≤ 0 |
WakeRearmDistance | 0.1 | Freeze: distance between the structure's reference point and its frozen wake frame, as a fraction of the structure's size √(frontal area), that re-arms Freeze (logged; owner ruling R50). | ≤ 0 |
WakeFilterTime | 5 s | Low-pass time constant of the source velocity and of the ambient current the re-arm watches; 0 does not filter. | negative |
WakeExpansion | 0.05 | Kernel expansion rate k. | ≤ 0 |
WakeCutoff | 1e-3 | Two roles. (1) Grid: the deficit below which a gridded field (a net's private field) truncates a footprint; a lumped wake has no grid. (2) With CastWake: the receiver cutoff tier of the other objects' wakes (owner decision R47): fhsim_marine_elements' casting Net/Disk, Net/Sphere and nets leave out, each step, a registered field whose deficit stays below it at their points. Without CastWake the tier is 1e-4 and this parameter is ignored (and logged so). | outside (0, 1) |
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, ReadSourceSetWakeSettings); 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".
**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-like body that moves at a constant TowVelocity from its initial Position (zero holds it fixed) and casts a lumped wake with CastWake. Its drag F = ½ρC_dA|u|u on u = u_water − TowVelocity, A = πD²/4, 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 by OwnQuery()) and Force. Parameters: Diameter (1 m), DragCoefficient (0.5, a test value), Rho (1025 kg/m³), TowVelocity (0, 0, 0) and the wake parameters above. The test input is tests/in/TestWakeBody/TestWakeBody_in.xml.
The EnvironmentProvider is a marenv::EnvironmentFacade, and the wake is applied inside it:
| Query | Waked? |
|---|---|
GetParticleVelocity | Yes: waked current plus wave particle velocity. |
GetCurrentVelocity | Yes: waked current. |
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 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. For a wake field that needs the flow at its own source. |
GetWakeDeficit | Returns d_tot alone. |
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). fhsim_marine_elements' casting nets, Net/Disk, Net/Sphere and TestWakeBody exclude their own lumped field from their own flow query (LumpedWakeCaster::OwnQuery(), ENV-0047). WakeQuery::own must not name a registered field, or that field counts twice (marenv; documented, not checked).
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 SelectReachingWakeFields, exact tier, owner decision R47): 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: fhsim_marine_elements' receivers pass their R47 tier, their WakeCutoff when they cast, 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 fhsim_marine_elements' Net/NetStructure, Net/NetStructureWithConstraints, Net/Disk and Net/Sphere. TestWakeBody samples its flow only for its WaterVelocity and Force output ports, outside the step, and queries live.
environment::wake::BodyWake (ENV-0095, design R3) bundles the above for a SimObject: the settings (ReadBodyWakeSettings(creator, kind), read once, so N BodyWakes of one object may share them under names with a suffix), the caster with CastWake (LumpedWakeCaster for BodyWakeKind::Single, WakeCaster for BodyWakeKind::SourceSet), ExternalWakes at the sample points and the receiver's cutoff tier (R47). Build it with the SimObject's name, the settings and the log; call Register(environment, rho) in FinalSetup, Update in PreOdeFcn (a body passes its WakeCastingObject and centre, or a null object to only receive; a source set passes itself, its sample points are the means of WakeSourceSet::SamplePointCarriers) and WaterVelocity or WakedCurrent in OdeFcn. fhsim_marine_elements' bodies and nets use it.
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.
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.
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.
VisualFlowPlane with ComputeFunction = "WakeDeficit" colours a plane by the combined deficit d_tot of all registered wakes (GetWakeDeficit), in [0, 1]; it was called WakeFactor before 4.0. It shows the lumped 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. VelocityMagnitude and CurrentMagnitude show the waked flow itself. 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).