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:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|}
|
|}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user