feat: Add Density profile plot and cancelable jobs

Adds a Density Plot to the Pipette, it uses the temp and
salinity at node together with surface pressure.

Also fixes the matrix titles for oceanbox/Poseidon#30 and
adds a cancel button as part of oceanbox/Poseidon#41.
This commit is contained in:
2025-11-18 18:01:36 +01:00
parent f8940c9220
commit 847270877a
18 changed files with 299 additions and 35 deletions

View File

@@ -15,7 +15,7 @@ plugins:
- "src/Atlantis/src/**.fsproj" - "src/Atlantis/src/**.fsproj"
- "src/Sorcerer/src/**.fsproj" - "src/Sorcerer/src/**.fsproj"
- "src/DataAgent/src/**.fsproj" - "src/DataAgent/src/**.fsproj"
- "src/ServerPack/src/*.fsproj" - "src/ServerPack/src/**.fsproj"
- "src/Interfaces/**.fsproj" - "src/Interfaces/**.fsproj"
- - '@semantic-release/exec' - - '@semantic-release/exec'
- generateNotesCmd: "echo ${nextRelease.version} > VERSION" - generateNotesCmd: "echo ${nextRelease.version} > VERSION"
@@ -29,7 +29,7 @@ plugins:
- "src/Atlantis/src/**.fsproj" - "src/Atlantis/src/**.fsproj"
- "src/Sorcerer/src/**.fsproj" - "src/Sorcerer/src/**.fsproj"
- "src/DataAgent/src/**.fsproj" - "src/DataAgent/src/**.fsproj"
- "src/ServerPack/src/*.fsproj" - "src/ServerPack/src/**.fsproj"
- "src/Interfaces/**.fsproj" - "src/Interfaces/**.fsproj"
analyzeCommits: analyzeCommits:

View File

@@ -277,6 +277,7 @@ type Prop =
| Temp | Temp
| Salt | Salt
| Zeta | Zeta
| Dens
| Speed | Speed
| Conc2D | Conc2D
| Conc3D | Conc3D
@@ -292,6 +293,7 @@ type Prop =
| Temp -> "temp" | Temp -> "temp"
| Salt -> "salt" | Salt -> "salt"
| Zeta -> "zeta" | Zeta -> "zeta"
| Dens -> "dens"
| Speed -> "speed" | Speed -> "speed"
| Conc2D -> "concentration2D" | Conc2D -> "concentration2D"
| Conc3D -> "concentration3D" | Conc3D -> "concentration3D"
@@ -308,6 +310,7 @@ type Prop =
| Temp -> "Temperature" | Temp -> "Temperature"
| Salt -> "Salinity" | Salt -> "Salinity"
| Zeta -> "Tide" | Zeta -> "Tide"
| Dens -> "Density (σt(T, S, 0)"
| Speed -> "Current" | Speed -> "Current"
| Conc2D -> "Concentration2D" | Conc2D -> "Concentration2D"
| Conc3D -> "Concentration3D" | Conc3D -> "Concentration3D"
@@ -324,6 +327,7 @@ type Prop =
| Temp -> 0.0, 15.0 | Temp -> 0.0, 15.0
| Salt -> 27.5, 35.0 | Salt -> 27.5, 35.0
| Zeta -> -1.5, 1.5 | Zeta -> -1.5, 1.5
| Dens -> 20.0, 27.0
| Speed -> 0.0, 2.0 | Speed -> 0.0, 2.0
| Conc2D -> 0.0, 1.0 | Conc2D -> 0.0, 1.0
| Conc3D -> 0.0, 1.0 | Conc3D -> 0.0, 1.0
@@ -342,6 +346,7 @@ type Prop =
| Temp -> -5.0, 45.0 | Temp -> -5.0, 45.0
| Salt -> 0.0, 50.0 | Salt -> 0.0, 50.0
| Zeta -> -5.0, 5.0 | Zeta -> -5.0, 5.0
| Dens -> 15.0, 28.0
| Speed -> 0.0, 5.0 | Speed -> 0.0, 5.0
| Conc2D -> 0.0, 100.0 | Conc2D -> 0.0, 100.0
| Conc3D -> 0.0, 100.0 | Conc3D -> 0.0, 100.0
@@ -360,6 +365,7 @@ type Prop =
| Temp -> 1.0 | Temp -> 1.0
| Salt -> 1.0 | Salt -> 1.0
| Zeta -> 0.1 | Zeta -> 0.1
| Dens -> 0.1
| Speed -> 0.1 | Speed -> 0.1
| Conc2D -> 0.1 | Conc2D -> 0.1
| Conc3D -> 1.0 | Conc3D -> 1.0
@@ -377,6 +383,7 @@ type Prop =
| Temp -> "°C" | Temp -> "°C"
| Salt -> "psu" | Salt -> "psu"
| Zeta -> "m" | Zeta -> "m"
| Dens -> "kg/m3-1000"
| Speed -> "m/s" | Speed -> "m/s"
| Conc2D -> "1/km2" | Conc2D -> "1/km2"
| Conc3D -> "1/km2" | Conc3D -> "1/km2"
@@ -394,6 +401,7 @@ type Prop =
| Speed -> elem | Speed -> elem
| Temp | Temp
| Salt | Salt
| Dens
| Bathy | Bathy
| Temp | Temp
| Salt | Salt
@@ -414,6 +422,7 @@ type Prop =
| "zeta" -> Prop.Zeta | "zeta" -> Prop.Zeta
| "temp" -> Prop.Temp | "temp" -> Prop.Temp
| "salt" -> Prop.Salt | "salt" -> Prop.Salt
| "dens" -> Prop.Dens
| "speed" -> Prop.Speed | "speed" -> Prop.Speed
| "concentration2D" -> Prop.Conc2D | "concentration2D" -> Prop.Conc2D
| "concentration3D" -> Prop.Conc3D | "concentration3D" -> Prop.Conc3D

View File

@@ -20,6 +20,7 @@ let inboxDialog
markAsRead: Guid -> unit markAsRead: Guid -> unit
deleteMessage: Guid -> unit deleteMessage: Guid -> unit
postMessage: InboxItem -> unit postMessage: InboxItem -> unit
cancelJob: int -> unit
unread: int // can flip between +/- N to indicate the need for a reload unread: int // can flip between +/- N to indicate the need for a reload
currentDrifters: Map<Guid, SimArchive> currentDrifters: Map<Guid, SimArchive>
|}) = |}) =
@@ -66,16 +67,32 @@ let inboxDialog
let doDelete _ = let doDelete _ =
let table = document.getElementById "inbox-table" let table = document.getElementById "inbox-table"
let items: InboxItem[] = table?items let selectedSet : Guid JS.Set = table?selectedSet
items let items: InboxItem array = table?items
|> Array.iter (fun item -> async {
if Set.contains item.id selected then let toDelete =
arg.deleteMessage item.id) items
loadMessages () |> Array.filter (fun item -> selectedSet.has(item.id))
|> Array.map (fun item -> item.id)
console.debug("Deleting", toDelete.Length, "messages")
for id in toDelete do
arg.deleteMessage id
// Clear selection immediately
selectedSet.clear()
setSelected Set.empty
// Wait a bit for server to process, then reload
do! Async.Sleep 200
let! mbox = Remoting.inboxApi().getMessages ()
table?items <- mbox
} |> Async.StartImmediate
let doRead selected _ = let doRead selected _ =
let table = document.getElementById "inbox-table" let table = document.getElementById "inbox-table"
let items: InboxItem[] = table?items let items: InboxItem array = table?items
items items
|> Array.iter (fun item -> |> Array.iter (fun item ->
console.log selected console.log selected
@@ -221,6 +238,30 @@ let inboxDialog
</a> </a>
""" """
let cancelButton =
let jobId, canCancel =
match item.type' with
| MessageType.Progress ->
let progressMsg = decodeProgressMessage item.content
let canCancel = progressMsg.job <> -1
progressMsg.job, canCancel
| MessageType.Drifters | MessageType.Job ->
let job = decodeJobMessage item.content
let canCancel = (job.status = JobStatus.Running || job.status = JobStatus.Waiting) && job.job <> -1
job.job, canCancel
| _ ->
-1, false
let doCancelJob _ =
if canCancel then arg.cancelJob jobId
html $"""
<sp-action-button ?disabled={not canCancel} @click={Ev(fun e -> e.stopPropagation(); doCancelJob())}>
<sp-icon-close slot="icon"></sp-icon-close>
<sp-tooltip placement="top" self-managed>Cancel simulation</sp-tooltip>
</sp-action-button>
"""
if item.content = "" then if item.content = "" then
html $""" html $"""
<sp-table-cell></sp-table-cell> <sp-table-cell></sp-table-cell>
@@ -239,7 +280,10 @@ let inboxDialog
<sp-table-cell @click={Ev(chooseItem item)} style="max-width: 120px; align-items: center; display: flex"> <sp-table-cell @click={Ev(chooseItem item)} style="max-width: 120px; align-items: center; display: flex">
{formatMsg item.unread inactive (item.created.ToString "dd/MM/yyyy")} {formatMsg item.unread inactive (item.created.ToString "dd/MM/yyyy")}
</sp-table-cell> </sp-table-cell>
<sp-table-cell @click={Ev(_.stopPropagation())} style="max-width: 60px">{downloadButton}</sp-table-cell> <sp-table-cell @click={Ev(_.stopPropagation())} style="max-width: 100px; display: flex; gap: 4px">
{downloadButton}
{cancelButton}
</sp-table-cell>
""" """
Hook.useEffectOnce (fun () -> Hook.useEffectOnce (fun () ->
@@ -285,7 +329,7 @@ let inboxDialog
<sp-table-head-cell sortable sort-direction="desc" sort-key="created" style="max-width: 120px"> <sp-table-head-cell sortable sort-direction="desc" sort-key="created" style="max-width: 120px">
Created Created
</sp-table-head-cell> </sp-table-head-cell>
<sp-table-head-cell style="max-width: 60px">PDF</sp-table-head-cell> <sp-table-head-cell style="max-width: 100px">Actions</sp-table-head-cell>
</sp-table-head> </sp-table-head>
</sp-table> </sp-table>

View File

@@ -26,6 +26,7 @@ open Petimeter.Inbox
open Remoting open Remoting
open Sorcerer.Types open Sorcerer.Types
open Utils open Utils
open Atlantis.Shared
let private hmr = HMR.createToken () let private hmr = HMR.createToken ()
@@ -1757,6 +1758,21 @@ let update cmd model =
} }
model, Cmd.OfAsync.perform delete () id model, Cmd.OfAsync.perform delete () id
| CancelJob jobId ->
let cancel () =
async {
let driftersApi = driftersJobApi ()
match! driftersApi.cancelJob jobId with
| Ok msg ->
console.log $"[Mapster]: Job {jobId} cancelled : {msg}"
let note = Note.warn msg
return SetNotification note
| Error err ->
console.error $"[Mapster]: Failed to cancel job {jobId}: {err}"
let note = Note.error err
return SetNotification note
}
model, Cmd.OfAsync.perform cancel () id
| SetIdentity identityOpt -> | SetIdentity identityOpt ->
match identityOpt with match identityOpt with
| Some id -> | Some id ->
@@ -2216,6 +2232,7 @@ let MapAppElement () =
markAsRead = fun id -> Hub.Action.Inbox (Hub.InboxMsg.MarkRead id) |> (HubMsg >> dispatch) markAsRead = fun id -> Hub.Action.Inbox (Hub.InboxMsg.MarkRead id) |> (HubMsg >> dispatch)
deleteMessage = fun id -> Hub.Action.Inbox (Hub.InboxMsg.Delete id) |> (HubMsg >> dispatch) deleteMessage = fun id -> Hub.Action.Inbox (Hub.InboxMsg.Delete id) |> (HubMsg >> dispatch)
postMessage = fun _ -> Hub.Action.Inbox (Hub.InboxMsg.Post (testMsg count)) |> (HubMsg >> dispatch) postMessage = fun _ -> Hub.Action.Inbox (Hub.InboxMsg.Post (testMsg count)) |> (HubMsg >> dispatch)
cancelJob = CancelJob >> dispatch
unread = model.inboxUnread unread = model.inboxUnread
currentDrifters = model.availableDrifters currentDrifters = model.availableDrifters
|} |}

View File

@@ -98,7 +98,7 @@ type ProbeView =
static member props view : Prop array = static member props view : Prop array =
match view with match view with
| DepthProfile -> [| Prop.Temp; Prop.Salt; Prop.Speed |] | DepthProfile -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Dens |]
| TimeSeries -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Zeta |] | TimeSeries -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Zeta |]
| RosePlots -> [| Prop.Speed |] | RosePlots -> [| Prop.Speed |]
@@ -482,6 +482,7 @@ type Msg =
| SetSimPolicies of DriftersPolicy[] | SetSimPolicies of DriftersPolicy[]
| RenameDriftersArchive of System.Guid * string | RenameDriftersArchive of System.Guid * string
| DeleteArchive of System.Guid | DeleteArchive of System.Guid
| CancelJob of int
| SetPlumeModel of PlumeModel option | SetPlumeModel of PlumeModel option
| ShowReleases of bool | ShowReleases of bool

View File

@@ -589,6 +589,9 @@ let private OceanPlotControls model dispatch =
<sp-action-button value="{string Prop.Speed}"> <sp-action-button value="{string Prop.Speed}">
Speed Speed
</sp-action-button> </sp-action-button>
<sp-action-button value="{string Prop.Dens}">
Density
</sp-action-button>
</sp-action-group> </sp-action-group>
{GraphRangeSlider model dispatch} {GraphRangeSlider model dispatch}

View File

@@ -656,8 +656,9 @@ module ReactLib =
let style = {| minHeight = "416px"; width = "100%" |} let style = {| minHeight = "416px"; width = "100%" |}
let config = {| responsive = true; editable = false |} let config = {| responsive = true; editable = false |}
let layout = {| let layout = {|
xaxis = {| side = "top" |} xaxis = {| side = "top"; title = {| text = "Receiver"; standoff = 120 |} |}
yaxis = {| autorange = "reversed" |} yaxis = {| autorange = "reversed"; title = {| text = "Sender"; standoff = 120 |} |}
margin = {| t = 100; b = 50; l = 100; r = 50 |}
|} |}
let traces = newHeatMap siteNames siteNames weights let traces = newHeatMap siteNames siteNames weights
@@ -718,7 +719,9 @@ module ReactLib =
editable = false editable = false
|} |}
let layout = {| let layout = {|
xaxis = {| side = "top" |} xaxis = {| side = "top"; title = {| text = "Receiver"; standoff = 120 |} |}
yaxis = {| title = {| text = "Sender"; standoff = 120 |} |}
margin = {| t = 100; b = 50; l = 100; r = 50 |}
|} |}
let traces = newCageHeatMap groupNames' cageNames weights' let traces = newCageHeatMap groupNames' cageNames weights'
@@ -731,6 +734,7 @@ module ReactLib =
/> />
""" """
// NOTE(simkir): !! These cannot be partially applied! Or, the exposing Component must be curried, so not take any // NOTE(simkir): !! These cannot be partially applied! Or, the exposing Component must be curried, so not take any
// arguments. // arguments.
// //

View File

@@ -18,9 +18,13 @@ let private fetchPropData (archiveId: System.Guid) (frame: FrameIdx) (gridIdx: G
match prop with match prop with
| Prop.Temp -> fvcom.Node.GetTemp archiveId frame | Prop.Temp -> fvcom.Node.GetTemp archiveId frame
| Prop.Salt -> fvcom.Node.GetSalinity archiveId frame | Prop.Salt -> fvcom.Node.GetSalinity archiveId frame
| Prop.Dens -> fvcom.Node.GetDensity archiveId frame
| Prop.Speed -> fvcom.Element.GetSpeed archiveId frame | Prop.Speed -> fvcom.Element.GetSpeed archiveId frame
| _ -> fun _ -> async.Return [||] | _ -> fun _ -> async.Return [||]
console.debug("[DepthPlots] Fetching %s data for archive %s, frame %d, idx %d", string prop, string archiveId, frame, idx)
let! res = fetch idx |> Async.StartAsPromise let! res = fetch idx |> Async.StartAsPromise
console.debug("[DepthPlots] Received %s data: length=%d, data=%o", string prop, res.Length, res)
return res return res
} }
@@ -322,7 +326,10 @@ let View
fun updatedStats -> fun updatedStats ->
console.debug ("[DepthPlots] Stats changed %o", updatedStats) console.debug ("[DepthPlots] Stats changed %o", updatedStats)
if updatedStats.propType = Undefined then if updatedStats.propType = Undefined then
console.warn "[DepthPlots] Stats prop type is Undefined!" if probeProp = Atlantis.Types.Prop.Dens then
console.debug "[DepthPlots] Density selected - statistics not available, showing instant data only"
else
console.warn "[DepthPlots] Stats prop type is Undefined!"
else else
console.debug ("[DepthPlots] Stats prop type is %s", string updatedStats.propType) console.debug ("[DepthPlots] Stats prop type is %s", string updatedStats.propType)
do FetchMetrics updatedStats |> dispatch do FetchMetrics updatedStats |> dispatch

View File

@@ -445,6 +445,22 @@ module Handlers =
} }
|> Async.AwaitTask |> Async.AwaitTask
let private cancelJob (ctx: HttpContext) jobId =
let user = getUserName ctx
Log.Information ("User {username} cancels job {}", user, jobId)
let actorId = ActorId user
task {
try
let proxy = ActorProxy.Create<IDriftersActor>(actorId, "DriftersActor")
return! proxy.Cancel jobId
with exn ->
Log.Error $"cancelJob: {exn.Message}"
Log.Verbose $"cancelJob: %A{exn}"
return Error exn.Message
}
|> Async.AwaitTask
let driftersApi (ctx: HttpContext) : Api.Drifters = { let driftersApi (ctx: HttpContext) : Api.Drifters = {
startDrifters = runDrifters ctx startDrifters = runDrifters ctx
startPostdrift = runPostdrift ctx startPostdrift = runPostdrift ctx
@@ -455,6 +471,7 @@ module Handlers =
getDriftersInput = getDriftersInput ctx getDriftersInput = getDriftersInput ctx
renameArchive = renameArchive ctx renameArchive = renameArchive ctx
retireArchive = retireArchive ctx retireArchive = retireArchive ctx
cancelJob = cancelJob ctx
} }
let inboxApi (ctx: HttpContext) : Api.Inbox = { let inboxApi (ctx: HttpContext) : Api.Inbox = {

View File

@@ -246,7 +246,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
// } // }
interface IJobActor with interface IJobActor with
member this.Cancel() = this.cancel () member this.Cancel(jobId) = this.cancel jobId
member this.HandleJobEvent(job) = this.handleJobEvent job member this.HandleJobEvent(job) = this.handleJobEvent job
member this.Remove(jobid) = this.remove jobid member this.Remove(jobid) = this.remove jobid
member this.RemoveById(aid: Guid) = this.removeById aid member this.RemoveById(aid: Guid) = this.removeById aid

View File

@@ -156,9 +156,32 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
else else
Task.FromResult () Task.FromResult ()
member this.cancel() = member this.cancel(jobId: int) =
Log.Debug $"cancel (not implemented yet): {this.myId}" Log.Debug $"[JobActor.Cancel]: Cancel job %i{jobId}: %s{this.myId}"
task { return true } task {
try
if this.jobs.ContainsKey jobId then
let! cancelled = slurm.CancelJob jobId
if cancelled then
let updatedJob = { this.jobs[jobId] with status = JobStatus.Failed }
this.jobs[jobId] <- updatedJob
do! this.StateManager.SetStateAsync (this.jobsKey, this.jobs)
// Publish cancellation to pubsub so clients are immediately notified
do! client.PublishEventAsync<JobInfo> ("pubsub", "hipster", updatedJob)
// Post status to inbox when job is cancelled successfully
do! this.postStatus MessageType.Job updatedJob
return Ok "Simulation cancelled"
else
return Error "Failed to cancel job"
else
return Error "Job not found"
with exn ->
Log.Error $"[JobActor.cancel]: Cancel job {jobId} failed: {exn.Message}"
return Error exn.Message
}
member this.handleJobEvent(job: SlurmJobStatusMsg) = member this.handleJobEvent(job: SlurmJobStatusMsg) =
let job' = let job' =
@@ -182,26 +205,31 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
task { task {
match job' with match job' with
| None ->
Log.Debug $"[JobActor.handleJobEvent] No job found with id: %d{job.jobId}"
Log.Debug $"[JobActor.handleJobEvent] jobs: %A{this.jobs}"
return ()
| Some j -> | Some j ->
match job.messageType with match job.messageType with
| "progress" -> do! this.postProgress job | "progress" -> do! this.postProgress job
| _ -> | _ ->
let status = LanguagePrimitives.EnumOfValue (int job.content) let status = LanguagePrimitives.EnumOfValue (int job.content)
let j' = { j with status = status }
do! update j'
let jobType = job.jobType |> inboxMessageTypeFromString // Don't update if job is already cancelled (Failed)
if j.status = JobStatus.Failed then
if status = JobStatus.Completed then Log.Debug $"[JobActor.handleJobEvent] Ignoring status update for cancelled job {job.jobId}: current={j.status}, new={status}"
// Delay for Atlantis to update on job completion
System.Threading.Thread.Sleep 1000
do! this.postStatus jobType j'
else else
do! this.postStatus MessageType.Job j' let j' = { j with status = status }
| None -> do! update j'
Log.Debug $"[JobActor.handleJobEvent] No job found with id: %d{job.jobId}"
Log.Debug $"[JobActor.handleJobEvent] jobs: %A{this.jobs}" let jobType = job.jobType |> inboxMessageTypeFromString
return ()
if status = JobStatus.Completed then
// Delay for Atlantis to update on job completion
System.Threading.Thread.Sleep 1000
do! this.postStatus jobType j'
else
do! this.postStatus MessageType.Job j'
} }
member this.remove(jobid) = member this.remove(jobid) =

View File

@@ -71,7 +71,7 @@ type PlumeActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
submitJob () submitJob ()
interface IJobActor with interface IJobActor with
member this.Cancel() = this.cancel () member this.Cancel(jobId) = this.cancel jobId
member this.HandleJobEvent(job) = this.handleJobEvent job member this.HandleJobEvent(job) = this.handleJobEvent job
member this.Remove(jobid) = this.remove jobid member this.Remove(jobid) = this.remove jobid
member this.RemoveById(aid: Guid) = this.removeById aid member this.RemoveById(aid: Guid) = this.removeById aid

View File

@@ -189,4 +189,17 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
match Decode.Auto.fromString<SlurmJobsResponse> rsp' with match Decode.Auto.fromString<SlurmJobsResponse> rsp' with
| Ok job -> return Array.tryHead job.jobs |> Option.map (fun q -> getJobStatus q.job_state) | Ok job -> return Array.tryHead job.jobs |> Option.map (fun q -> getJobStatus q.job_state)
| Error _ -> return None | Error _ -> return None
}
member this.CancelJob(jobId: int) =
task {
try
// Cancel or signal job
// https://slurm.schedmd.com/rest_api.html#slurmV0044DeleteJob
let! rsp = client.DeleteAsync $"job/%i{jobId}"
// Returns 200 no succesful cancel
return rsp.IsSuccessStatusCode
with exn ->
Log.Error $"Cancel job %i{jobId} failed: %s{exn.Message}"
return false
} }

View File

@@ -57,6 +57,7 @@ module Api =
getDriftersInput: Guid -> Async<Result<DriftersInput option, string>> getDriftersInput: Guid -> Async<Result<DriftersInput option, string>>
renameArchive: Guid -> string -> Async<Result<unit, string>> renameArchive: Guid -> string -> Async<Result<unit, string>>
retireArchive: Guid -> Async<Result<unit, string>> retireArchive: Guid -> Async<Result<unit, string>>
cancelJob: int -> Async<Result<string, string>>
} }
type Inbox = type Inbox =

View File

@@ -50,7 +50,6 @@ type PostdriftJob = {
type IJobActor = type IJobActor =
inherit IActor inherit IActor
abstract Cancel: unit: unit -> Task<bool>
abstract Remove: job: int -> Task<bool> abstract Remove: job: int -> Task<bool>
abstract RemoveById: aid: Guid -> Task<bool> abstract RemoveById: aid: Guid -> Task<bool>
abstract Clear: unit: unit -> Task abstract Clear: unit: unit -> Task
@@ -59,6 +58,7 @@ type IJobActor =
abstract GetActiveJobs: aid: Guid -> Task<JobInfo[]> abstract GetActiveJobs: aid: Guid -> Task<JobInfo[]>
abstract GetFenceRadius: unit: unit -> Task<float> abstract GetFenceRadius: unit: unit -> Task<float>
abstract CheckFence: aid: Guid * pos: (float * float) list -> Task<bool> abstract CheckFence: aid: Guid * pos: (float * float) list -> Task<bool>
abstract Cancel: job: int -> Task<Result<string, string>>
type IDriftersActor = type IDriftersActor =
inherit IJobActor inherit IJobActor

View File

@@ -39,6 +39,7 @@ module Api =
GetBathymetry: Guid -> NodeIdx -> Async<single> GetBathymetry: Guid -> NodeIdx -> Async<single>
GetTemp: Guid -> FrameIdx -> NodeIdx -> Async<single array> GetTemp: Guid -> FrameIdx -> NodeIdx -> Async<single array>
GetSalinity: Guid -> FrameIdx -> NodeIdx -> Async<single array> GetSalinity: Guid -> FrameIdx -> NodeIdx -> Async<single array>
GetDensity: Guid -> FrameIdx -> NodeIdx -> Async<single array>
} }
type Element = { type Element = {

View File

@@ -446,6 +446,15 @@ module Fvcom =
Fvcom.salinityAtNode aid t n Fvcom.salinityAtNode aid t n
|> Result.defaultValue Array.empty |> Result.defaultValue Array.empty
} }
GetDensity =
fun aid t n ->
async {
let logData = {| aid = aid; n = n |}
use _ = observer.trace ("GetDensity", "{@log_data}", logData)
return
Fvcom.densityAtNode aid t n
|> Result.defaultValue Array.empty
}
} }
let elementApi (ctx: HttpContext) : Fvcom.Element = let elementApi (ctx: HttpContext) : Fvcom.Element =

View File

@@ -214,6 +214,96 @@ let private queryGridIndexCache (aid: Guid) (grid: ExtendedGrid) =
} }
|> Async.Start |> Async.Start
/// Compute density of seawater from salinity, temperature, and pressure
let private dens (temp: float) (salt: float) (pres: float) =
// From Bjørn Aadlandsvik https://github.com/bjornaa/seawater/blob/master/seawater/density.py
/// Density of seawater at zero pressure
let dens0 (salt: float) (temp: float) =
// --- Define constants ---
let a0 = 999.842594
let a1 = 6.793952e-2
let a2 = -9.095290e-3
let a3 = 1.001685e-4
let a4 = -1.120083e-6
let a5 = 6.536332e-9
let b0 = 8.24493e-1
let b1 = -4.0899e-3
let b2 = 7.6438e-5
let b3 = -8.2467e-7
let b4 = 5.3875e-9
let c0 = -5.72466e-3
let c1 = 1.0227e-4
let c2 = -1.6546e-6
let d0 = 4.8314e-4
// --- Computations ---
// Density of pure water
let SMOW = a0 + (a1 + (a2 + (a3 + (a4 + a5 * temp) * temp) * temp) * temp) * temp
// More temperature polynomials
let RB = b0 + (b1 + (b2 + (b3 + b4 * temp) * temp) * temp) * temp
let RC = c0 + (c1 + c2 * temp) * temp
SMOW + RB * salt + RC * (salt ** 1.5) + d0 * salt * salt
/// Secant bulk modulus
let seck salt temp pres =
// --- Pure water terms ---
let h0 = 3.239908
let h1 = 1.43713e-3
let h2 = 1.16092e-4
let h3 = -5.77905e-7
let AW = h0 + (h1 + (h2 + h3 * temp) * temp) * temp
let k0 = 8.50935e-5
let k1 = -6.12293e-6
let k2 = 5.2787e-8
let BW = k0 + (k1 + k2 * temp) * temp
let e0 = 19652.21
let e1 = 148.4206
let e2 = -2.327105
let e3 = 1.360477e-2
let e4 = -5.155288e-5
let KW = e0 + (e1 + (e2 + (e3 + e4 * temp) * temp) * temp) * temp
// --- Seawater, P = 0 ---
let SR = salt ** 0.5
let i0 = 2.2838e-3
let i1 = -1.0981e-5
let i2 = -1.6078e-6
let j0 = 1.91075e-4
let A = AW + (i0 + (i1 + i2 * temp) * temp + j0 * SR) * salt
let f0 = 54.6746
let f1 = -0.603459
let f2 = 1.09987e-2
let f3 = -6.1670e-5
let g0 = 7.944e-2
let g1 = 1.6483e-2
let g2 = -5.3009e-4
let K0 =
KW
+ (f0 + (f1 + (f2 + f3 * temp) * temp) * temp + (g0 + (g1 + g2 * temp) * temp) * SR)
* salt
// --- General expression ---
let m0 = -9.9348e-7
let m1 = 2.0816e-8
let m2 = 9.1697e-10
let B = BW + (m0 + (m1 + m2 * temp) * temp) * salt
K0 + (A + B * pres) * pres
// Convert to bar
let pres = 0.1 * pres
(dens0 salt temp) / (1.0 - pres / seck salt temp pres)
let nLayers (aid: Guid) = let nLayers (aid: Guid) =
let f ds _ = Fvcom.getNumSiglay ds let f ds _ = Fvcom.getNumSiglay ds
dataAgent.eval (f, aid) dataAgent.eval (f, aid)
@@ -472,6 +562,26 @@ let zetaAtNode aid t n =
let f ds t = Fvcom.Singular.readZeta ds n t let f ds t = Fvcom.Singular.readZeta ds n t
dataAgent.eval (f, aid, t) dataAgent.eval (f, aid, t)
let densityAtNode aid t n =
monad {
let! temp = tempAtNode aid t n
let! salt = salinityAtNode aid t n
let! h = bathymetryAtNode aid n
let! zeta = zetaAtNode aid t n
let! siglay = siglay aid n
return
Array.map3 (fun t_val s_val sigma ->
let depth = abs (sigma * h + zeta) |> float
let t_val = float t_val
let s_val = float s_val
// NOTE: Pressure set to surface pressure which is
// approx 0 using σ_t(T, S, 0)
let pres = 0
(dens t_val s_val pres - 1000.) |> single
) temp salt siglay
}
let getDepthsAtNode aid node = let getDepthsAtNode aid node =
monad { monad {
let! h = bathymetryAtNode aid node let! h = bathymetryAtNode aid node