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:
Review issues:

0032 — Jacobian callbacks are indexed with global state offsets, so they are wrong for any object that is not first

0033 — Auv thrust does not fall off with speed

0034 — Auv yaw and pitch rate damping are independent of the forward speed

0037 — The undulation climbs and dives at 0.7 of u sin(DiveAngle), and the pitch loop needs 40 s to get there

0038 — Auv output ports dereference a null environment during InitialConditionSetup

0041 — SimObject render teardown runs after the Ogre root is destroyed

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 AddXxxForcesma_adSumForcesCalcDerivatives 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. 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.

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_PositiveRudderYawGivesAPositiveYawRate and Auv_PositiveRudderDiveGivesANoseDownMoment:

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.

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 transverse hydrodynamics entirely.
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.
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).

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.xml.

<!--
The AUV survey scenario, exposed for co-simulation.
This is examples/input/AuvSurvey.xml with three objects added and one connection
rewired, and nothing else. Read that file for the scenario itself: the site, the
fields, the vehicle, the autopilot gains, the undulation envelope and the
instruments are all documented there and are identical here.
## The fields come from a file beside this one
The five scalar fields are ScalarField.<Name>.Model = "Parametric" and are read from
auv_ocean_fields.json, which lives beside the non-FMU scenario in
examples/input/. They are three-dimensional: a temperature and salinity front, two
Calanus patches, two mackerel schools, and a cod layer that is SEAFLOOR referenced
and so follows the rolling bottom rather than sitting at a fixed depth.
The ConfigFile path is resolved against the PROCESS working directory and not
against the directory of this file, and the path written below is the one that
works for a run started in playpen/bin, which is where FhSim and the ctest run of
this scenario both start. An instantiated FMU does not start there: it sets its
working directory to its own binaries/<platform> folder, where that path resolves
to nothing. That is what the autocopy export flag in AuvSurvey.fmu.yaml is for; it
is written there with two leading hyphens, which an XML comment may not contain. The
exporter
copies every file a model attribute names into resources/auto_copied/ and rewrites
the attribute to point at the copy, so the fields travel inside the FMU and the
path in the exported model is not the path below. Remove the flag and the model
loads with five unreadable field configurations.
## Causality is inverted
The ExternalLinkStandard object's OUTPUT ports feed FhSim and therefore become FMU
INPUTS; its INPUT ports are fed by FhSim and become FMU OUTPUTS. That is the one
thing about this file which is easy to read backwards. The worked minimal example is
fhsim_base/examples/input/FMU/MassSpring.xml.
## The interface
FMU inputs, both of which default to zero, meaning "do not intervene":
CruiseSpeedOverride m/s. When it is strictly positive it replaces the mission
control's commanded speed. At zero or below, the mission
control commands the speed, including the zero it commands
once the track is flown.
AbortMission A flag. Strictly positive commands zero speed immediately,
whatever the mission control and the override are asking
for. The vehicle coasts to a stop and keeps flying its
undulation; FhSim cannot terminate on a signal, so an abort
is a speed command and the host stops stepping.
FMU outputs, the vehicle state a co-simulation host needs plus one reading from each
instrument:
Pos, Euler, Depth, Altitude, BatteryFraction the vehicle
EndMission the mission control's latch
CtdMeasurement salinity, temperature and depth
SilCamConcentration, SilCamSizeMean the plankton counter
EchoAltitude the measured height over the seabed
Altitude appears twice on purpose and with the same name and units on both sides:
Auv.Altitude is the true height above the seabed, taken straight from the
bathymetry, and Echo.Altitude is the sounder's measurement of it. A host that wants
to see the sensor error subtracts one from the other, and a scenario that wants the
dive control to fly on the measurement instead of on the truth is a one line rewire
of the Dive.Altitude connection.
## The two switches, and why no C++ was written for them
Neither input can be wired straight to a port, because there is no port on the
mission control that means "override the speed" and none that means "abort". Two
fhsim_base Signal/Switch objects express both without a new SimObject:
SpeedSelect passes CruiseSpeedOverride when CruiseSpeedOverride is positive, and
Mission.Speed otherwise, so zero means "leave it to the mission".
AbortGate passes zero when AbortMission is positive, and SpeedSelect.Out
otherwise.
Both are pure functions of their inputs, so they add direct feedthrough. They sit
between the mission control and the speed PID, which is the one place in the control
stack where that is harmless: the mission control has no feedthrough of its own and
the PID output is a member written in AcceptedStep, so the cycle around the vehicle
stays non algebraic.
## The visualisers travel with it
AuvSurvey.fmu.yaml declares variants: [headless, visual], so this model is exported
twice and one of the two is expected to draw something. It therefore carries the same
three environment visualisers as examples/input/AuvSurvey.xml - the Calanus and cod
volumes and the temperature section - and they are documented there and beside the
objects below. They cost the headless variant nothing at run time: they have no
ports, no state and no OdeFcn, they are never stepped, and their one piece of
headless work is the field lookup in FinalSetup.
What this file does NOT get is a <Visualization> observer, and it does not need one.
It has no OBSERVERS block at all, so the legacy default set applies and a live view
is registered whenever one is attached. Adding an OBSERVERS block here to hold the
observer would silence the default FileOutput with it.
## Stepping it
At the FMU boundary the host's communicationStepSize is an exact sampling grid, so
make it an integer divisor of the instrument periods: the CTD samples at 0.0625 s,
the SilCam frames at 0.25 s and the sounder pings at 1.0 s. 0.0625 s divides all
three; 0.0125 s, which is the integrator StepMax, divides all three as well, and is
the value a host should prefer if it can afford the exchanges.
-->
<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.PropertyNames = "MassKg, LengthM, LengthStdM, TargetStrengthRefDb, RefFreqkHz, TsSlopeDbPerOctave"
ScalarField.Calanus.PropertyValues = "0.0000011, 0.0028, 0.0007, -38.9, 200, 12"
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.PropertyNames = "MassKg, LengthM, TargetStrengthRefDb, RefFreqkHz, TsSlopeDbPerOctave"
ScalarField.Mackerel.PropertyValues = "0.35, 0.35, -44.9, 38, 2"
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.PropertyNames = "MassKg, LengthM, TargetStrengthRefDb, RefFreqkHz, TsSlopeDbPerOctave"
ScalarField.Cod.PropertyValues = "2.3, 0.60, -27.5, 38, 0"
/>
<!-- 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 whole upper 45 m and
would sit inside the Calanus box over its entire footprint, so 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. -->
<Lib
LibName = "fishery"
SimObject = "Trawl/PIDController"
Name = "PitchPid"
InitOutput = "0"
ProportionalGain = "-0.6"
IntegratorGain = "-0.05"
DerivativeGain = "-0.8"
MinOut = "-0.35"
MaxOut = "0.35"
MaxDerivativePart = "0.2"
MaxIntegratorPart = "0.2"
/>
<!-- 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. MaxRangeUp is 50 m rather than 150 m because the vehicle flies
between 10 m and 45 m and an up beam longer than the depth samples bins
above the free surface; see FISH-0040. -->
<!-- The co-simulation boundary. Mind the inversion: outputPortNames become the
FMU's inputs and inputPortNames become its outputs. -->
<Lib
LibName = "base"
SimObject = "System/ExternalLinkStandard"
Name = "fmi"
outputPortNames = "CruiseSpeedOverride, AbortMission"
Initial_CruiseSpeedOverride = "0"
Initial_AbortMission = "0"
inputPortNames = "Pos, Euler, Depth, Altitude, BatteryFraction, EndMission, CtdMeasurement, SilCamConcentration, SilCamSizeMean, EchoAltitude"
Size_Pos = "3"
Size_Euler = "3"
Size_Depth = "1"
Size_Altitude = "1"
Size_BatteryFraction = "1"
Size_EndMission = "1"
Size_CtdMeasurement = "3"
Size_SilCamConcentration = "1"
Size_SilCamSizeMean = "1"
Size_EchoAltitude = "1"
/>
<!-- Pass In1 when In2 is positive, otherwise In3. -->
<Lib
LibName = "base"
SimObject = "Signal/Switch"
Name = "SpeedSelect"
PortWidth = "1"
SwitchType = "gt"
/>
<Lib
LibName = "base"
SimObject = "Signal/Switch"
Name = "AbortGate"
PortWidth = "1"
SwitchType = "gt"
/>
<Lib
LibName = "fishery"
SimObject = "Auv/Echosounder"
Name = "Echo"
MountOffset = "0, 0, 0.10"
MountDirection = "0, 0, 1"
PingPeriod = "1.0"
MaxRangeDown = "150"
MaxRangeUp = "50"
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"/>
<!-- The speed reference passes through the two override switches rather than
coming straight from the mission control. -->
<Connection SpeedSelect.In1 = "fmi.CruiseSpeedOverride"
SpeedSelect.In2 = "fmi.CruiseSpeedOverride"
SpeedSelect.In3 = "Mission.Speed"/>
<Connection AbortGate.In1 = "0"
AbortGate.In2 = "fmi.AbortMission"
AbortGate.In3 = "SpeedSelect.Out"/>
<Connection SpeedPid.Reference = "AbortGate.Out" 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"/>
<!-- What the host reads back. These are the ExternalLinkStandard's INPUT ports,
so they are the FMU's outputs. -->
<Connection
fmi.Pos = "Auv.Pos"
fmi.Euler = "Auv.Euler"
fmi.Depth = "Auv.Depth"
fmi.Altitude = "Auv.Altitude"
fmi.BatteryFraction = "Auv.BatteryFraction"
fmi.EndMission = "Mission.EndMission"
fmi.CtdMeasurement = "Ctd.Measurement"
fmi.SilCamConcentration = "SilCam.Concentration"
fmi.SilCamSizeMean = "SilCam.SizeMean"
fmi.EchoAltitude = "Echo.Altitude"
/>
</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>
</Contents>

This SimObject is referred to as Auv/Vehicle