FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
Autonomous underwater vehicle

Six degree-of-freedom rigid body for a torpedo-shaped survey AUV.

+ Collaboration diagram for Autonomous underwater vehicle:
Author
Karl-Johan Reite

A survey AUV modelled as a rigid cylinder in six degrees of freedom, carrying a battery state. The state layout, the constant inverse mass matrix, the AddXxxForces → ma_adSumForces → CalcDerivatives pipeline and the FinalSetup environment lookup all follow TrawlDoorBase, so a reader who knows the trawl door knows this file.

Geometry

The hull is idealised as a solid cylinder of uniform density. Length, Diameter and AverageDensity are the only geometric parameters; the volume, the mass and the inertia are derived from them, so displacement, mass and inertia cannot be configured into an inconsistent combination and the buoyancy trim is the single number AverageDensity:

\begin{eqnarray*} r &=& \frac{D}{2} \\ \nabla &=& \pi r^2 L \\ m &=& \rho_{avg} \nabla \\ I_{xx} &=& \frac{1}{2} m r^2 \\ I_{yy} = I_{zz} &=& \frac{m (3 r^2 + L^2)}{12} \end{eqnarray*}

With the defaults (2.0 m by 0.20 m at 1020 kg/m³ in 1025 kg/m³ water) the vehicle has about 0.31 kg of net buoyancy, so a dead vehicle surfaces.

The mass matrix \(M = \mathrm{diag}(m + A_{11}, \ldots, I_{zz} + A_{66})\) is assembled and inverted once, in the constructor. It stays diagonal because the body origin is placed at the centre of buoyancy and the centre of gravity is carried as the offset COG, whose only effect is a moment.

Gravity and buoyancy

The weight acts at COG and is the entire pitch and roll restoring model. The buoyancy is \(\rho g \nabla f\) applied at the body origin, where \(f\) is the submerged fraction. Because the hull is a cylinder, \(f\) is the exact circular-segment area of a horizontal cylinder rather than the ad-hoc linear ramp used by TrawlDoorBase, so surfacing is \(C^1\) — see SubmergedFraction().

Propulsion, drag and the battery

The thruster delivers \(T = k_T\,\mathrm{clamp}(P_{cmd}, 0, P_{max})\) along body x at ThrusterPos, so a thruster placed off the x axis produces a moment as well. The model is deliberately speed independent: real thrust at fixed shaft power falls with speed, and ThrustPerPower is calibrated at the cruise point rather than being a propeller curve. Because thrust is linear in propulsion power and independent of surge speed, accelerations from rest and speeds away from cruise are overestimated. ComputeThrust() is virtual so that a bollard-pull blend can replace it later without touching any caller.

The hull resists surge with \(F_x = -\tfrac12 \rho A_f C_d |u_{rel}| u_{rel}\) on the frontal area \(A_f = \pi D^2/4\). That is the only component it touches. The transverse resistance is the business of the cross-flow strip theory below, and adding a transverse term here as well would double-count it; AxialDragWrench() is a pure function so that a unit test can pin the other five components to exactly zero.

Cross-flow strip theory and roll damping

The transverse hydrodynamics are a three-section strip model. Each section \(k\) sits at \(\mathbf{r}_k = (\mathrm{SectionX}_k, 0, 0)\) and sees the local relative velocity \(\mathbf{v}_k = \mathbf{v}_{rel} + \boldsymbol{\omega} \times \mathbf{r}_k\). Only the cross-flow components enter — a cross-flow drag is by definition independent of the axial flow — so with \(w_k = \sqrt{v_{ky}^2 + v_{kz}^2 + \varepsilon}\) and \(c_k = \tfrac12 \rho C_{d,cf} D_k L_k\) the section wrench is

\[ \mathbf{F}_k = (0,\; -c_k w_k v_{ky},\; -c_k w_k v_{kz}), \qquad \mathbf{M}_k = \mathbf{r}_k \times \mathbf{F}_k . \]

The tiny \(\varepsilon\) keeps \(w_k\) differentiable at zero cross-flow so the adaptive integrator is not driven to its minimum step there; see kCrossFlowSpeedEpsilon.

One loop delivers three things**: the transverse force, the pitch and yaw moment that its distribution along the hull produces, and — through the \(\boldsymbol{\omega} \times \mathbf{r}_k\) term — pitch and yaw rate damping. No separate \(C_{mq}\) coefficient exists or is needed.

Two consequences of "cross-flow only" are worth stating plainly, because both are easy to expect the other way round:

Strip theory contributes nothing about the x axis, because \(\mathbf{r}_k \times \mathbf{F}_k\) has no x component for an \(\mathbf{r}_k\) on the x axis. Roll is therefore damped by the explicit \(K_p = -(\mathrm{RollDampingLinear} + \mathrm{RollDampingQuadratic}\,|p|)\,p\), and that term is the only thing in the whole class that damps roll.

Slender-body hull lift and the Munk moment

Owner ruling R112 (FISH-0095). Slender-body theory (Munk 1924, NACA Report 184; Jones 1946, NACA Report 835) gives a hull section of cross-flow added mass \(m_a(x) = \rho A(x)\) moving at \(U\) with local cross-flow \(w(x)\) the normal force per unit length \(f(x) = U \frac{d}{dx}\left[m_a(x)\, w(x)\right]\) (ahead, \(w\) in body y or z, force in the same axis). Integrated over a closed body it gives no force and the Munk moment, \(-(\mathbf{v}_r \times M_A \mathbf{v}_r)\), which about z is \(-(A_{22} - A_{11})\,u v\) and about y \((A_{33} - A_{11})\,u w\): it turns the hull broadside to the flow. The model carries it with the Coriolis-centripetal terms (rigid_body::OmegaTermsWrench, from AddedMass, so \(A_{22} = A_{33} = 58\) kg against slender-body theory's \(\rho \pi r^2 L = 64\) kg). A real hull does not close its flow: it separates where the hull ends, and cutting the integral off there leaves the separated end's term, the force

\[ \mathbf{F} = -\rho A_e |u_{rel}| \,\bigl(0,\; v_{rel} + r x_e,\; w_{rel} - q x_e\bigr) \quad \text{at } (x_e, 0, 0), \]

with \(x_e\) the aft end of the aftmost strip section (the forward end of the foremost one going astern) and \(A_e = \pi D_e^2/4\) its section area. For a pure sideslip angle \(\beta\) that is \(\tfrac12 \rho V^2 A_e \sin 2\beta\), the potential term of Allen and Perkins (1951, NACA Report 1048) and Jorgensen (1977, NASA TR R-474), \(C_N = \sin 2\alpha \cos(\alpha/2)\, A_b / A_{ref}\), without its \(\cos(\alpha/2)\) (above 0.97 up to 28 degrees). Their second, viscous term, \(\eta\, C_{dc} \sin^2\alpha\, A_p / A_{ref}\), is the cross-flow strip theory above, with \(\eta = 1\). See SlenderBodyLiftWrench(); no parameter is added, the geometry is the strip theory's.

The tail force stabilises and damps: on the default hull at 1.5 m/s it gives \(N_v = +48\) N·m·s/m and \(N_r = -48\) N·m·s against the Munk moment's \(N_v = -82\) N·m·s/m (and the fins' +30 and −26). The bare hull is still statically unstable (the net \(N_v\) is −34, with the fins −4), but the default vehicle is dynamically stable in sway-yaw and heave-pitch at 0.75 to 2.25 m/s, as torpedo AUVs are: linear eigenvalues at 1.5 m/s −0.67 and −1.84 1/s in sway-yaw, −0.19, −0.67 and −1.66 1/s in heave-pitch (Auv_DefaultVehicleIsDynamicallyStableInSwayYawAndHeavePitch).

Fins

Two fin planes, each one routine call: the dive plane (the stern planes, commanded by RudderDive, working in the body x-z plane) and the yaw plane (commanded by RudderYaw, working in the body x-y plane). Both act at RudderPos, and both the force and the moment are applied there, so moving the fins aft increases their moment in proportion.

The inflow at the fin is the body's relative velocity plus the rotation about the fin arm, \(\mathbf{v}_f = \mathbf{v}_{rel} + \boldsymbol{\omega} \times \mathbf{r}_{fin}\); that term is where the fins' rate damping comes from. Writing \(u_f\) for the axial and \(s_f\) for the in-plane transverse component,

\begin{eqnarray*} \alpha_{flow} &=& \mathrm{atan2}(s_f, u_f) \\ \alpha &=& \mathrm{clamp}(\delta, \pm\,\mathrm{MaxRudderAngle}) + \alpha_{flow} \\ q_{dyn} &=& \tfrac12 \rho\, A_{fin} \left(u_f^2 + s_f^2\right) \\ F_x &=& q_{dyn}\left(-C_d \cos\alpha_{flow} + C_l \sin\alpha_{flow}\right) \\ F_s &=& q_{dyn}\left(-C_l \cos\alpha_{flow} - C_d \sin\alpha_{flow}\right) \end{eqnarray*}

that is, the drag acts along the inflow and the lift across it, both resolved back into the body frame. \(C_l\) and \(C_d\) come from AuvFoil, whose lift curve is \(C^1\) through the stall so that a fin passing through 25 degrees does not pin the adaptive integrator at StepMin.

Two behaviours fall out of that rather than being added on top:

Signs

Both established by experiment, not by inspection, and pinned by Auv_FinSignsArePositiveYawAndNoseDownDive:

The dive-authority check

A vehicle whose COG is too far below the centre of buoyancy simply cannot dive: the righting moment beats the fin moment at every reachable angle, and the symptom is a vehicle that flies level and a dive autopilot stuck at its integrator limit. The constructor therefore warns when

\[ A_{fin}\, q_{dyn}\, C_{l,max}\, |x_{fin}| \le m g |COG_z| \sin(\mathrm{DiveAngle}), \]

naming both assumptions it had to make in the message, because neither belongs to this class:

The check uses RudderClMax, which is the peak of the lift curve and is reached at RudderAoaMax, not at MaxRudderAngle. Since MaxRudderAngle is deliberately inside the stall, the attainable coefficient is about 0.9 of RudderClMax, and the check is correspondingly optimistic by that much. It is written that way on purpose: it is a necessary condition, so it never warns about a vehicle that can in fact dive.

The battery discharges at the clamped propulsion power plus the constant HotelPower while it holds any energy, and stops — rather than going negative — when it is flat. A flat battery also stops the thrust. The discharge is driven by an input port and by the sign of the battery state alone, so the whole battery row of the Jacobian is identically zero, exactly as in Actuator.

Wake

With CastWake the vehicle casts its hull wake and a propeller jet together (feature propeller-jet, owner rulings O12a, O13a, O14a), through an environment::wake::BodyFlowInteraction of a self-propelled body (fhsim_environment doc/user/wakes.md, "A self-propelled body"; the jet law is marenv doc/wake.md § 4.2); the wake parameters are those of fhsim_environment's doc/user/wake-parameters.md. Default CastWake is false: the vehicle casts nothing and its results are unchanged.

In steady cruise thrust balances that drag, so the far field carries about no momentum: the jet's excess cancels the hull wake's deficit in the integral (Auv_SteadyCruiseFarFieldIsMomentumless).

The vehicle reads its water velocity through the BodyFlowInteraction, in OdeFcn, OdeJacobian, InputPortJacobian and the Speed output alike: the current with every other object's wake and jet, never its own (D3), plus the waves. It meets the other objects' wakes with or without CastWake. With the default ExternalWakeUpdate="Step" they are taken once per step at the vehicle's position at the step's start and held for the step, a field whose effect stays below the cutoff dropped; with ExternalWakeUpdate="Evaluation" the vehicle reads the live flow.

Known limitations

Example configuration excerpt

<Lib
LibName = "fishery"
SimObject = "Auv/Vehicle"
Name = "Auv"
Length = "2.0"
Diameter = "0.20"
AverageDensity = "1020"
Rho = "1025"
COG = "0, 0, 0.020"
AddedMass = "3.2, 58, 58, 0.1, 19, 19"
ThrustPerPower = "0.30"
MaxPropulsionPower = "120"
HotelPower = "15"
ThrusterPos = "-1.0, 0, 0"
AxialDragCoeff = "0.25"
CrossFlowDragCoeff = "1.1"
RollDampingLinear = "4.6"
RollDampingQuadratic = "1.0"
RudderArea = "0.016"
RudderAspectRatio = "1.25"
RudderPos = "-0.85, 0, 0"
RudderClMax = "0.8"
RudderAoaMax = "0.436"
RudderCd0 = "0.02"
RudderOswald = "0.9"
MaxRudderAngle = "0.35"
InitialBattery = "1.8e6"
/>

All four input ports must be connected; a constant is the usual choice while the control stack is not yet wired:

<Connection
Auv.ThrustPower = "0"
Auv.RudderYaw = "0"
Auv.RudderDive = "0"
Auv.ExternalForce = "0,0,0"
/>

Input Ports

Name Width Description
ThrustPower 1 Commanded propulsion power [W], clamped to [0, MaxPropulsionPower] before it is used.
RudderYaw 1 Commanded deflection of the fin pair that turns the vehicle in the horizontal plane [rad], that is the pair whose chord lies in the body x-y plane. Named by effect, not by fin plane. A positive command gives a positive yaw rate, that is a turn to starboard. Clamped to plus or minus MaxRudderAngle.
RudderDive 1 Commanded deflection of the fin pair that pitches the vehicle in the vertical plane [rad], that is the stern planes, whose chord lies in the body x-z plane. Named by effect, not by fin plane. A positive command gives a nose-down, that is negative, pitch moment and therefore a dive. Clamped to plus or minus MaxRudderAngle.
ExternalForce 3 An external force in the NED frame [N], applied at the body origin and therefore contributing no moment.

Output Ports

Name Width Description
Pos 3 Position of the body origin in NED [m].
Vel 3 Velocity of the body origin in NED [m/s].
Quater 4 Attitude quaternion, scalar first [q0, q1, q2, q3].
Euler 3 Roll, pitch and yaw [rad], from the ZYX convention.
Heading 1 Yaw angle [rad], the third element of Euler.
Pitch 1 Pitch angle [rad], the second element of Euler. Positive is nose up.
Depth 1 Depth below the mean free surface [m], positive down.
Speed 1 Forward speed through the water [m/s], the body-x component of the velocity relative to the fluid. This, not the speed over ground and not the magnitude of the velocity, is what a speed autopilot regulates.
Altitude 1 Height of the body origin above the seafloor [m].
SeaDepth 1 Depth of the seafloor below the surface at the vehicle position [m].
Battery 1 Remaining battery energy [J].
BatteryFraction 1 Remaining battery energy as a fraction of InitialBattery.
Thrust 1 The thrust actually delivered along body x [N], which is zero while the battery is flat.

Configuration parameters

Name Width Description
Length 1 Hull length [m] (Default: 2.0).
Diameter 1 Hull diameter [m] (Default: 0.20).
AverageDensity 1 Mean density of the whole vehicle [kg/m³] (Default: 1020). Below Rho the vehicle is positively buoyant. This one number is the buoyancy trim.
Rho 1 Density of the sea water [kg/m³] (Default: 1025).
COG 3 Centre of gravity in the body frame [m], relative to the centre of buoyancy at the origin (Default: "0,0,0.020"). The vertical component is the entire pitch and roll restoring model.
AddedMass 6 Added mass and added inertia on the diagonal, [A11, A22, A33, A44, A55, A66] in kg and kg·m² (Default: "3.2, 58, 58, 0.1, 19, 19"). A44 is floored well above zero so that the mass matrix stays invertible for a body of revolution.
ThrustPerPower 1 Thrust delivered per watt of propulsion power [N/W] (Default: 0.30). Calibrated at the cruise point, 9.0 N from 30 W.
MaxPropulsionPower 1 The largest propulsion power the thruster accepts [W] (Default: 120, four times cruise). Commands are clamped to [0, this].
HotelPower 1 The constant payload, computer and navigation load [W] (Default: 15). It is drawn whenever the battery holds any energy.
ThrusterPos 3 Position of the thruster in the body frame [m] (Default: "-1.0, 0, 0"). An axial thruster on the x axis contributes no moment, but the moment is computed anyway so that an offset thruster stays correct.
AxialDragCoeff 1 Hull drag coefficient on the frontal area pi D²/4 [-] (Default: 0.25). Gives 9.0 N at 1.5 m/s, which matches published REMUS-class drag.
CrossFlowDragCoeff 1 Two-dimensional cross-flow drag coefficient of a hull section [-] (Default: 1.1, a circular cylinder at Re of order 1e5). Set it to zero to remove the viscous cross-flow drag; the slender-body hull lift and the Munk moment stay.
SectionX 3 Body-frame x of the centroid of each strip-theory section [m] (Default: Length/3, 0, -Length/3, that is the centroids of three equal thirds).
SectionDiameter 3 Cross-flow diameter of each section [m] (Default: Diameter for all three). Override it together with SectionLength to taper a non-cylindrical hull; the defaults are the cylinder the rest of the class assumes. The aftmost section's diameter is also the base of the slender-body hull lift.
SectionLength 3 Length of each section [m] (Default: Length/3 for all three). The sum is not forced to equal Length, so a tapered hull can leave gaps or overlap.
RollDampingLinear 1 Linear roll damping [N·m·s] (Default: 4.6, chosen to critically damp the 12.6 N·m/rad roll mode of the default trim). Strip theory contributes nothing about the x axis, so this term and RollDampingQuadratic are the whole roll damping.
RollDampingQuadratic 1 Quadratic roll damping [N·m·s²] (Default: 1.0).
RudderArea 1 Planform area of one fin plane [m²] (Default: 0.016, that is two 0.10 by 0.08 m fins). Set it to zero to remove the fins entirely, which is what the hull-only regression inputs do.
RudderAspectRatio 1 Aspect ratio of a fin [-] (Default: 1.25). It enters the induced drag alone, not the lift curve.
RudderPos 3 Position of both fin planes in the body frame [m] (Default: "-0.85, 0, 0"). Both the force and the moment act here, so moving the fins aft increases the control moment in proportion.
RudderClMax 1 Peak lift coefficient of a fin [-] (Default: 0.8).
RudderAoaMax 1 Angle of attack at which the peak lift is reached [rad] (Default: 0.436, that is 25 degrees). The lift curve is C1 through this angle, by construction; see AuvFoil.
RudderCd0 1 Profile drag coefficient of a fin at zero lift [-] (Default: 0.02).
RudderOswald 1 Oswald span efficiency of the fin induced drag [-] (Default: 0.9).
MaxRudderAngle 1 Mechanical stop on the commanded deflection [rad] (Default: 0.35, that is 20 degrees, inside the 25 degree stall). Commands are clamped to plus or minus this before the flow angle is added.
DiveAngleForAuthorityCheck 1 The dive angle the startup dive-authority warning is evaluated at [rad] (Default: 0.349, that is 20 degrees, the default DiveAngle of AuvUndulateDiveControl). It is a parameter because the dive angle belongs to the dive controller, not to the vehicle, so hard-coding it would make the warning unfalsifiable for a differently configured controller. Set it to zero to silence the check.
InitialBattery 1 Battery energy at the start of the mission [J] (Default: 1.8e6, that is 0.5 kWh).
CastWake 1 The vehicle casts its hull wake and propeller jet (see Wake); the other wake parameters (Wake*, ExternalWakeUpdate) are listed in fhsim_environment's doc/user/wake-parameters.md. (Default: false)
PropellerDiameter 1 Diameter of the propeller whose jet the vehicle casts [m], > 0; read only with CastWake. (Default: 0.6 Diameter)

Initial conditions

Name Width Description
Pos 3 Position in NED [m] (Default: "0,0,0").
Quater 4 Attitude quaternion, scalar first (Default: "1,0,0,0").
LocalVel 3 Velocity in the body frame [m/s] (Default: "0,0,0").
Omega 3 Angular velocity in the body frame [rad/s] (Default: "0,0,0").
Battery 1 Battery energy [J] (Default: InitialBattery).

Full example file

AuvSurvey.xml is the whole stack in one model: the environment, this vehicle, the three PID loops that fly it, the dive and mission guidance and all three instruments. It is also the regression input tests/in/AuvSurvey/AuvSurvey_in.xml, byte for byte, so the example and the test cannot drift apart; AuvSurvey_Test asserts that they are identical. A co-simulation variant of the same scenario, with the FMU boundary added, is examples/input/FMU/AuvSurvey/model.xml.

<!--
The complete AUV survey scenario: a virtual ocean, a vehicle, an autopilot, a
guidance stack and three scientific instruments, in one model.
A survey AUV undulates between waypoints over a rolling seabed at about 140 m,
sampling a stratified water column with a CTD, counting Calanus with a silhouette
camera and writing a three frequency echogram to file. Everything in it is already
covered by a focused regression case of its own under tests/in/Auv/; this file is
the one place where the whole stack runs together, and it is also the regression
input tests/in/AuvSurvey/AuvSurvey_in.xml, byte for byte, so the example and the
test cannot drift apart. AuvSurvey_Test asserts that.
## The site
About 68 degrees north off Lofoten, in outer Vestfjorden, in June. Every number in
the environment below is only defensible relative to a stated site and season, so
here it is stated: bottom around 140 m, a Norwegian Coastal Current surface layer
over Norwegian Atlantic Water, a subsurface Calanus band in early summer, mackerel
on their epipelagic feeding migration above it, and a demersal cod aggregation near
the bottom.
## The fields, and where they come from
Every scalar field here is ScalarField.<Name>.Model = "Parametric", read out of
auv_ocean_fields.json beside this file. A parametric field is a depth profile plus a
list of features - a Gaussian blob, a horizontal front, a depth band - so it is a
function of north, east and depth and not of depth alone. The five entries are:
SeaTemperature 9.5 degC June coastal water over a 20 m to 60 m thermocline down
to 7.4 degC and the 6.5 degC Atlantic core at 400 m, with a front
and a warm lens on top of it.
SeaSalinity 33.8 PSU coastal water over Atlantic water above 35.0 PSU below
80 m, with the same front. The salinity front shares the origin
and the heading of the temperature front and carries the opposite
sign, so the cooler side is the saltier, Atlantic side: it is one
water mass boundary and not two unrelated ones.
Calanus A band centred at 28 m with a 9 m standard deviation, peaking near
2.2e-3 kg/m3, over a weak background, with two patches on top.
Mackerel Held in the upper 20 m by its background profile, with two loose
schools centred at 12 m and 10 m, peaking near 6e-2 kg/m3.
Cod Demersal, and the one field whose meaning is different in kind.
It has its own section below.
The survey box is 300 m by 100 m at the origin, and the features are laid out over a
far wider region than that: the Calanus patches are centred 600 m and 1500 m north,
the mackerel schools 400 m and 1700 m north, the front 1000 m out. So for the most
part this mission flies through the flanks of the structure and not through its
cores. Inside the box the front is very nearly a constant offset, about -0.53 degC
and +0.34 PSU, and the nearer Calanus patch adds at most some 1.5e-5 kg/m3 on top of
the band, against a band peak of 2.2e-3. That is the intent and not an oversight:
the file describes a region, this mission samples one corner of it, and a mission
that ran north to 1500 m would fly through a patch.
The one feature the box does reach into is the first mackerel school, 400 m north
and 200 m east with sigmas of 150 m and 90 m: at the north-east corner of the track
it still holds about 2.2e-2 kg/m3, a third of its peak, at its own depth. The school
and the mackerel background stay above 20 m, clear of the Calanus band (FISH-0042).
## Cod follows the seabed
The Cod entry sets "z_reference": "seafloor", so every vertical coordinate in it is
a HEIGHT ABOVE THE BOTTOM, positive up, and the layer rides the rolling bathymetry
instead of sitting at a fixed depth. It peaks near 1.2e-2 kg/m3 about 5 m above the
bottom, wherever the bottom happens to be. That is what a demersal aggregation is,
and it is precisely what a depth-only field cannot say: the previous version of this
file pinned the cod into a fixed 126 m to 145 m band and called it a bottom layer,
which it was only on average and never at a given point.
All three species fields also set "mask_below_seabed", so their concentration is
exactly zero under the bottom and the sounder does not integrate fish through rock.
## ConfigFile, and the working directory it is resolved from
ScalarField.<Name>.ConfigFile is opened with the path exactly as written, relative to
the PROCESS working directory and not to the directory this file sits in. Both ways
the scenario is run start in playpen/bin - the example run below, and the ctest run
of tests/in/AuvSurvey/AuvSurvey_in.xml, which is this file byte for byte and which
CMake gives WORKING_DIRECTORY ${PLAYPEN_DIR}/bin - so one relative path serves both:
cd build/Release/playpen/bin
./FhSim ../examples/input/fishery/AuvSurvey.xml
That is why the path below points into the installed playpen tree and not into the
source tree. Run it from anywhere else and the path has to move with you.
A Parametric field must use ConfigFile and not an inline Config: every XML attribute
in this tree is passed through the expression evaluator, which is not bracket aware
and mangles a JSON array of numbers. See fhsim_environment issue ENV-0019.
## Watching it run
The same file, run with FhVis instead of FhSim, draws itself:
cd build/Release/playpen/bin
./FhVis ../examples/input/fishery/AuvSurvey.xml
The vehicle is a yellow torpedo with fins that deflect to the commanded RudderDive
and RudderYaw, the echosounder draws its beam as a translucent cone out to
MaxRangeDown, the seabed is the SumOfSines surface the Cod field is
referenced to, and three visualisers in the OBJECTS block below draw the ocean the
instruments are measuring: the Calanus layer and the demersal cod as ray-marched
volumes, and a vertical temperature section running 1900 m north through the water
mass front. See the comment above those three objects for the sizing.
The live view exists only because of <Visualization frameRate="25"/> in the OBSERVERS
block. An OBSERVERS block is honoured exactly as written and there is one here for
the FileOutput, so without that line FhVis would run this model headless.
The camera starts at NED (-30, 0, 0) looking at the origin, which is beside the first
waypoint and above the vehicle's 20 m start depth. Steer it with the arrow keys and
the numeric keypad.
## Why the species are what they are
The three species are chosen to separate by frequency, which is the whole reason a
three frequency sounder is worth simulating. Each field names its Species, a key of the
echosounder Parameter library (fhsim_fishery data/acoustics/parameter_library.json,
copied from the echosounder sensor repository), and its mean LengthM; the library gives
the target strength per frequency, the weight per length and the length spread.
The consequence worth knowing before reading the echogram: at the design Calanus peak
the layer alone is Sv = -101.0 dB at 38 kHz, -81.4 dB at 120 kHz and -73.2 dB at
200 kHz. The modelled noise floor of this sounder reaches -101 dB at about 16 m of
range at 38 kHz, so Calanus alone is invisible there beyond that, while at 200 kHz it
survives far beyond the vehicle's reach. That contrast between frequencies, not blanket
visibility, is what the echogram is for.
## The Calanus band holds Calanus
The mackerel field stays above 20 m and the cod field below 80 m above the bottom, so
the 26 m to 31 m band is Calanus at 120 kHz and 200 kHz (FISH-0042). At 38 kHz Calanus
is below the noise floor and the tails of the mackerel schools, so the band's rise from
38 kHz to 200 kHz, about 24 dB, is a little short of the 27.8 dB of Calanus alone.
## The seabed is pinned
Environment.RandomSeed defaults to a time based seed, and the default bathymetry is
a sum of randomly phased structures rather than a flat bottom. Both are pinned here,
so the seabed is the same rolling surface on every run. See FISH-0036.
## Undulation envelope
The dive control flies the vehicle between 10 m and 45 m rather than down to the
80 m default, for two reasons. The band has to contain the Calanus layer at 28 m and
the mackerel above it, which is what the instruments are here to find; and a
commanded dive angle is not a flight path angle, so the vehicle makes only about
0.36 m/s of vertical speed at the default 0.349 rad command. A 35 m band is about
97 s per leg, so a 600 s mission is five legs. See FISH-0037.
DepthHysteresis is 4 m, not the 1 m default. One metre is the minimum for the
switching logic alone; it is not enough once the loop is closed, because the vehicle
makes 2 to 3 m of depth while the pitch loop swings over, and the depth then leaves
the commanded band at every turn.
## The gain signs
Heading positive, pitch negative, speed positive: each gain takes the sign of the
plant it drives. PidController.cpp forms error = measurement - reference and
sfh::filters::PID then forms -kP*error, so the two inversions cancel and the
composition is the standard convention. See FISH-0035.
SpeedPid.InitOutput is a feed forward and not a tuning knob. 30 W is the cruise
power of this parameter set at the commanded 1.5 m/s; at a different commanded speed
it must be changed with it, or the loop spends tens of seconds unwinding an
integrator that started in the wrong place.
## Neutral buoyancy on purpose
AverageDensity is 1025, the same as Rho. Depth hold in this stack is pitch hold, so
at the trimmed default of 1020 the vehicle rises at about 0.115 m/s regardless of
what the pitch loop does, and the undulation envelope would not hold.
## A calm sea and no current
Waves.NumWaves is zero and the current is zero, which makes this the clean case. A
sea state is a one line change, and a shallow vehicle then sees the wave orbital
velocity through the hydrodynamics and the autopilots see it through Speed and
Pitch; that is intended behaviour and not a defect. StepMax of 0.0125 s resolves a
7 s peak period 560 times over.
## StepMax divides every instrument period
0.0125 s is not the 0.05 s the plan suggested, and the difference is not about
accuracy. A sensor fires on the first accepted step at or after its due time, so an
integrator step that does not divide a sample period makes the samples late by up to
one step: at 0.05 s the CTD, which samples at 0.0625 s, is up to 37 ms late and
SampleClock says so once, in a warning naming the StepMax it wants. 0.0125 s divides
0.0625 s, the SilCam's 0.25 s frame period and the sounder's 1 s ping alike, so
every instrument fires on its own grid. It is also the number a co-simulation host
should pick for communicationStepSize.
-->
<Contents>
<OBJECTS>
<Lib
LibName = "environment"
SimObject = "Environment"
Name = "Env"
RandomSeed = "20240601"
Waves.Model = "Component"
Waves.NumWaves = "0"
Current.Model = "Constant"
Current.Vector = "0, 0, 0"
Bathymetry.Model = "SumOfSines"
Bathymetry.Depth = "140"
Bathymetry.NumStructures = "10"
Bathymetry.StructureLengthMin = "150"
Bathymetry.StructureLengthMax = "600"
Bathymetry.StructureMaxHeight = "6"
ScalarField.Names = "SeaTemperature, SeaSalinity, Calanus, Mackerel, Cod"
ScalarField.SeaTemperature.Quantity = "Temperature"
ScalarField.SeaTemperature.Unit = "degC"
ScalarField.SeaTemperature.Model = "Parametric"
ScalarField.SeaTemperature.ConfigFile = "../examples/input/fishery/auv_ocean_fields.json"
ScalarField.SeaTemperature.ConfigKey = "SeaTemperature"
ScalarField.SeaSalinity.Quantity = "Salinity"
ScalarField.SeaSalinity.Unit = "PSU"
ScalarField.SeaSalinity.Model = "Parametric"
ScalarField.SeaSalinity.ConfigFile = "../examples/input/fishery/auv_ocean_fields.json"
ScalarField.SeaSalinity.ConfigKey = "SeaSalinity"
ScalarField.Calanus.Quantity = "SpeciesConcentration"
ScalarField.Calanus.Unit = "kg/m^3"
ScalarField.Calanus.Model = "Parametric"
ScalarField.Calanus.ConfigFile = "../examples/input/fishery/auv_ocean_fields.json"
ScalarField.Calanus.ConfigKey = "Calanus"
ScalarField.Calanus.Species = "calanus_finmarchicus"
ScalarField.Calanus.PropertyNames = "LengthM"
ScalarField.Calanus.PropertyValues = "0.0028"
ScalarField.Mackerel.Quantity = "SpeciesConcentration"
ScalarField.Mackerel.Unit = "kg/m^3"
ScalarField.Mackerel.Model = "Parametric"
ScalarField.Mackerel.ConfigFile = "../examples/input/fishery/auv_ocean_fields.json"
ScalarField.Mackerel.ConfigKey = "Mackerel"
ScalarField.Mackerel.Species = "mackerel"
ScalarField.Mackerel.PropertyNames = "LengthM"
ScalarField.Mackerel.PropertyValues = "0.35"
ScalarField.Cod.Quantity = "SpeciesConcentration"
ScalarField.Cod.Unit = "kg/m^3"
ScalarField.Cod.Model = "Parametric"
ScalarField.Cod.ConfigFile = "../examples/input/fishery/auv_ocean_fields.json"
ScalarField.Cod.ConfigKey = "Cod"
ScalarField.Cod.Species = "cod"
ScalarField.Cod.PropertyNames = "LengthM"
ScalarField.Cod.PropertyValues = "0.60"
/>
<!-- ConfigKey is given on every field even though it defaults to the field name,
because one JSON document holds all five entries and the mapping from an
XML field name to a key in that document is worth being able to read here.
The Cod entry is the seafloor referenced one; see the header. -->
<!-- ================================================================
The environment, drawn.
Three fhsim_environment visualisers, all of them standalone: they have no
ports and no connections, they find the Environment through the shared
"Environment" resource rather than by its name, and they find their field by
the name it is listed under in ScalarField.Names. That lookup and the
time-invariance check happen in VisualBakedScalarField::FinalSetup, which
runs in the HEADLESS binary too, so a typo in FieldName below fails the
regression run and not only the live view. That is deliberate.
Each one bakes its field once, at startup, into a texture and costs nothing
per frame afterwards, so the resolutions below are a startup cost and not a
frame cost. They are kept modest all the same - about 200 k cells per volume
against the 3.5 M of fhsim_environment's own ScalarVolumeBlobs demo - because
the bake is CPU work and this scenario is usually watched over software GL.
ReferenceLength is the path length AlphaRange is quoted over. The default of
1 m makes any ray crossing more than a few metres of visible field opaque, so
it is set here to the vertical scale of each structure: 20 m for the Calanus
band, 10 m for the thinner cod layer.
Mackerel is deliberately NOT drawn. Its field fills the upper 20 m, directly
above the Calanus box over its entire footprint, so seen from the side the two
ray-marched volumes would blend into one wash and neither would be readable.
Adding a third <Lib> for it is a copy of the Calanus block with FieldName,
ColorRange and VisibleRange changed, if that is the picture wanted.
Two occlusion artefacts of VisualScalarVolume are documented and accepted
upstream and are not defects: a ray that passes through the seabed still
blends its below-seabed samples over the seafloor unless the field masks them
(all three species fields here set mask_below_seabed, so they do not), and
while the camera is INSIDE a box the fragment is depth-tested at the far box
face, so the seafloor erases the volume across its whole footprint. Backing
the camera out of the box restores it.
================================================================ -->
<!-- The Calanus layer over the survey box. The band is centred at 28 m with a 9 m
standard deviation, so a 0..60 m box holds all of it; 1.25 m of vertical
spacing resolves the band, 5 m horizontally is plenty for a field that has no
horizontal structure worth the name inside this box. VisibleRange starts at
3.0e-4, just above the 1e-4 to 3e-4 background, so the ambient water is clear
and only the layer is drawn. -->
<Lib
LibName = "environment"
SimObject = "VisualScalarVolume"
Name = "CalanusVolume"
FieldName = "Calanus"
BoxCentre = "150, 50, 30"
BoxSize = "420, 260, 60"
Resolution = "84, 52, 48"
ReferenceLength = "20"
ColorRange = "3.0e-4, 2.2e-3"
VisibleRange = "3.0e-4, 2.6e-3"
AlphaRange = "0.02, 0.30"
/>
<!-- The demersal cod layer. This is the one that shows what a seafloor-referenced
field buys: the layer is 5 m above the BOTTOM, so it rides the rolling
bathymetry, and the box is a flat 110..154 m slab that the layer wanders
through rather than a band the layer fills. 1 m of vertical spacing, because
the band's sigma is 2.5 m. -->
<Lib
LibName = "environment"
SimObject = "VisualScalarVolume"
Name = "CodVolume"
FieldName = "Cod"
BoxCentre = "150, 50, 132"
BoxSize = "420, 260, 44"
Resolution = "84, 52, 44"
ReferenceLength = "10"
ColorRange = "1.0e-3, 1.2e-2"
VisibleRange = "1.0e-3, 1.6e-2"
AlphaRange = "0.03, 0.45"
/>
<!-- A vertical temperature section along the track, from the surface to the
seabed. It is 1900 m long and the survey box is 300 m of that on purpose: the
water mass front sits 1000 m out and inside the box it is very nearly a
constant -0.53 degC offset, so a slice cropped to the box would show the
thermocline and nothing else. Run north far enough and the front, and the
warm lens on top of it, are both in the picture, and it is then visible that
this mission samples one corner of a much larger structure.
It sits at the EASTERN edge of the survey box, flush with the far face of
the two volumes, and not down the middle of it. That is not cosmetic: the
slice is nearly opaque where the field is strong, so anywhere it stands
between the camera and the vehicle it is a wall rather than a section. At the
far face it is a backdrop, and from the default camera - which is south of
the box at East 0, looking north - it is off to the right and seen at a
glancing angle. 4 m by 1 m sampling is 66.5 k cells, a tenth of either
volume. -->
<Lib
LibName = "environment"
SimObject = "VisualScalarPlane"
Name = "TemperatureSection"
FieldName = "SeaTemperature"
Position = "600, 180, 70"
LocalX = "1, 0, 0"
LocalY = "0, 0, 1"
Size = "1900, 140"
Resolution = "475, 140"
ColorRange = "6.5, 9.8"
VisibleRange = "6.2, 10.4"
AlphaRange = "0.45, 0.80"
/>
<Lib
LibName = "fishery"
SimObject = "Auv/Vehicle"
Name = "Auv"
Length = "2.0"
Diameter = "0.20"
AverageDensity = "1025"
Rho = "1025"
COG = "0, 0, 0.020"
AddedMass = "3.2, 58, 58, 0.1, 19, 19"
ThrustPerPower = "0.30"
MaxPropulsionPower = "120"
HotelPower = "15"
ThrusterPos = "-1.0, 0, 0"
AxialDragCoeff = "0.25"
CrossFlowDragCoeff = "1.1"
RollDampingLinear = "4.6"
RollDampingQuadratic = "1.0"
RudderArea = "0.016"
RudderAspectRatio = "1.25"
RudderPos = "-0.85, 0, 0"
RudderClMax = "0.8"
RudderAoaMax = "0.436"
RudderCd0 = "0.02"
RudderOswald = "0.9"
MaxRudderAngle = "0.35"
InitialBattery = "1.8e6"
/>
<!-- A short three leg survey box: 300 m north, 100 m east, 300 m south. About
700 m of track, that is about 470 s at cruise, so the mission finishes well
inside the 600 s run and EndMission has time to latch. RecoveryPosition is
the last waypoint, so no return leg is added. -->
<Lib
LibName = "fishery"
SimObject = "Auv/UndulateMissionControl"
Name = "Mission"
WaypointsX = "300, 300, 0"
WaypointsY = " 0, 100, 100"
RecoveryPosition = "0, 100"
WaypointRadius = "15"
CruiseSpeed = "1.5"
BatteryAtEndOfMission = "2.7e5"
LoopWaypoints = "0"
UpdatePeriod = "1.0"
/>
<Lib
LibName = "fishery"
SimObject = "Auv/UndulateDiveControl"
Name = "Dive"
MaxDepth = "45"
MinDepth = "10"
MinSeafloorDistance = "15"
DiveAngle = "0.349"
DepthHysteresis = "4.0"
UpdatePeriod = "1.0"
/>
<!-- Heading. POSITIVE gains; Ki deliberately zero, because a symmetric vehicle
needs no steady rudder and an integrator winds up through the saturated part
of a large step. -->
<Lib
LibName = "fishery"
SimObject = "Trawl/PIDController"
Name = "HeadingPid"
InitOutput = "0"
ProportionalGain = "0.4"
IntegratorGain = "0"
DerivativeGain = "0.6"
MinOut = "-0.35"
MaxOut = "0.35"
MaxDerivativePart = "0.2"
MaxIntegratorPart = "0.1"
/>
<!-- Pitch. NEGATIVE gains, because a positive RudderDive lowers the pitch.
IntegratorGain -0.08 as in tests/in/Auv/Auv_Undulate_in.xml (R112, FISH-0095):
with the Munk moment and the hull lift the standing deflection at DiveAngle is
0.093 rad rather than 0.20, and -0.15 overshot each DiveAngle step by 27 %. -->
<Lib
LibName = "fishery"
SimObject = "Trawl/PIDController"
Name = "PitchPid"
InitOutput = "0"
ProportionalGain = "-0.6"
IntegratorGain = "-0.08"
DerivativeGain = "-0.8"
MinOut = "-0.35"
MaxOut = "0.35"
MaxDerivativePart = "0.2"
MaxIntegratorPart = "0.3"
/>
<!-- Speed. POSITIVE gains. InitOutput 30 W is the cruise feed forward for the
commanded 1.5 m/s; change it with CruiseSpeed. -->
<Lib
LibName = "fishery"
SimObject = "Trawl/PIDController"
Name = "SpeedPid"
InitOutput = "30"
ProportionalGain = "200"
IntegratorGain = "10"
DerivativeGain = "0"
MinOut = "0"
MaxOut = "120"
MaxIntegratorPart = "120"
/>
<!-- The CTD head is on the nose, ahead of the hull wake. The time constants and
the noise are an SBE49 FastCAT. NoiseSeed is non zero, so the instrument is
realistic rather than perfect, and the hand rolled generators make it
reproducible across compilers all the same. -->
<Lib
LibName = "fishery"
SimObject = "Auv/CTD"
Name = "Ctd"
TemperatureFieldName = "SeaTemperature"
SalinityFieldName = "SeaSalinity"
MountOffset = "0.8, 0, 0"
SamplePeriod = "0.0625"
TemperatureTimeConstant = "0.07"
ConductivityTimeConstant = "0.06"
DepthTimeConstant = "0.02"
NoiseSeed = "7"
/>
<!-- The silhouette camera images 1.43e-4 cubic metres per frame, so a single
frame is almost pure noise: at the design Calanus peak the rate is 0.22
particles per frame after the capture and detection efficiencies. Forty
frames, that is 10 s and about 15 m of track, brings it to about 9 counts
and a 33 percent counting error. That is why the aggregation exists. -->
<Lib
LibName = "fishery"
SimObject = "Auv/SilCam"
Name = "SilCam"
SpeciesFieldName = "Calanus"
MountOffset = "0.6, 0, 0"
FrameRate = "4.0"
FramesPerOutput = "40"
SampleVolume = "0.040, 0.055, 0.065"
CaptureEfficiency = "0.85"
DetectionProbability = "0.92"
FalsePositiveRate = "0.05"
SizeErrorStdRelative = "0.08"
NumSizeBins = "8"
SizeBinMin = "0.0005"
SizeBinMax = "0.0060"
NoiseSeed = "11"
/>
<!-- MountDirection must be given. SensorFrame defaults a beam axis to body x,
which is 90 degrees wrong for a nadir sounder.
NoiseSeed is non zero on purpose, and it is not only about realism: with the
seed at zero the instrument adds no range noise, no speckle AND no noise
floor, and the noise floor is precisely what makes the Calanus layer
invisible at 38 kHz and visible at 120 and 200 kHz.
PingPeriod is 1 s rather than the 0.5 s default. One hertz is a realistic
survey rate, it halves a file that would otherwise reach about 7.5 MB over
this mission, and StepMax of 0.0125 s still puts 80 integrator steps in a
ping interval. -->
<Lib
LibName = "fishery"
SimObject = "Auv/Echosounder"
Name = "Echo"
MountOffset = "0, 0, 0.10"
MountDirection = "0, 0, 1"
PingPeriod = "1.0"
MaxRangeDown = "150"
RayStepSize = "1.0"
RayTolerance = "0.01"
SpeciesFieldNames = "Calanus, Mackerel, Cod"
BinSize = "1.0"
SvPrecision = "2"
FlushEveryNPings = "20"
WriteFile = "1"
FileName = "AuvSurvey_echogram.csv"
NoiseSeed = "13"
/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection Mission.Pos = "Auv.Pos" Mission.Battery = "Auv.Battery" Mission.Heading = "Auv.Heading"/>
<Connection Dive.Depth = "Auv.Depth" Dive.Altitude = "Auv.Altitude"/>
<Connection HeadingPid.Reference = "Mission.Heading" HeadingPid.Measurement = "Auv.Heading"/>
<Connection PitchPid.Reference = "Dive.PitchRef" PitchPid.Measurement = "Auv.Pitch"/>
<Connection SpeedPid.Reference = "Mission.Speed" SpeedPid.Measurement = "Auv.Speed"/>
<!-- Two cycles, neither of them algebraic: Mission, Dive and PidController all
return output members written only in AcceptedStep, so nothing in either
cycle has direct feedthrough. -->
<Connection
Auv.RudderYaw = "HeadingPid.Out"
Auv.RudderDive = "PitchPid.Out"
Auv.ThrustPower = "SpeedPid.Out"
Auv.ExternalForce = "0,0,0"
/>
<!-- The instruments ride the vehicle. Each one rotates its own mount offset and
beam axis out of the body frame, so the pose is all they need. -->
<Connection Ctd.Pos = "Auv.Pos" Ctd.Quater = "Auv.Quater"/>
<Connection SilCam.Pos = "Auv.Pos" SilCam.Quater = "Auv.Quater"/>
<Connection Echo.Pos = "Auv.Pos" Echo.Quater = "Auv.Quater"/>
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
Auv.Pos = "0,0,20"
Auv.Quater = "1,0,0,0"
Auv.LocalVel = "1.5,0,0"
Auv.Omega = "0,0,0"
/>
</INITIALIZATION>
<SIMULATION>
<Timing TStart="0" TEnd="600"/>
<Integrator Method="RK45_i" NumCores="1">
<StepControl StepMax="0.0125" StepMin="1e-8" AbsTol="1e-4" RelTol="1e-4"/>
</Integrator>
</SIMULATION>
<OBSERVERS>
<!-- A range, not a list: TOutput is start:delta:end. -->
<FileOutput TOutput="0:0.5:600" Select="objects:all"/>
<!-- Without this the scenario never draws. An OBSERVERS block is honoured
exactly as written, so once one is present the legacy default set - which is
where the live view used to come from - is gone, and FhVis runs the model
headless. In a headless binary the element is a silent no-op: FhSim never
attaches a live view, makeVisualizationObserverFromModel returns nothing and
nothing is registered, which is why this file can carry the observer and
still be the regression input byte for byte. -->
<Visualization frameRate="25"/>
</OBSERVERS>
</Contents>

This SimObject is referred to as Auv/Vehicle