feat: format and tests

This commit is contained in:
2024-08-16 15:57:55 +02:00
parent 5c676b6638
commit 8f4c25face
14 changed files with 810 additions and 238 deletions

13
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas": {
"version": "6.3.10",
"commands": [
"fantomas"
],
"rollForward": false
}
}
}

29
.editorconfig Normal file
View File

@@ -0,0 +1,29 @@
root = true
[*]
indent_style = space
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.fsproj]
indent_size = 2
[**/*.{fs,fsx}]
indent_size = 4
max_line_length = 80
fsharp_alternative_long_member_definitions = true
fsharp_blank_lines_around_nested_multiline_expressions = false
fsharp_keep_max_number_of_blank_lines = 1
fsharp_max_dot_get_expression_width = 60
fsharp_max_function_binding_width = 100
fsharp_max_infix_operator_expression = 80
fsharp_max_record_number_of_items = 2
fsharp_max_record_width = 40
fsharp_multi_line_lambda_closing_newline = true
fsharp_multiline_bracket_style = stroustrup
fsharp_record_multiline_formatter = number_of_items
fsharp_space_around_delimiter = true
fsharp_space_before_member = true
fsharp_space_before_parameter = true
fsharp_space_before_uppercase_invocation = false

View File

@@ -4,6 +4,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Common -->
<PackageVersion Include="Expecto" Version="10.2.1" />
<PackageVersion Include="Oceanbox.FvcomKit" Version="5.5.3" />
<PackageVersion Include="SDSLite-O" Version="2.7.2" />
<PackageVersion Include="Serilog" Version="4.0.1" />

View File

@@ -7,6 +7,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CLI", "src\CLI\CLI.fsproj",
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gridding", "src\Gridding\Gridding.fsproj", "{05D9B830-0CD6-4E59-9AD3-A196B82D60A8}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{2A0E708C-02D9-409D-8201-DA291AB8B85E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{05D9B830-0CD6-4E59-9AD3-A196B82D60A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05D9B830-0CD6-4E59-9AD3-A196B82D60A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05D9B830-0CD6-4E59-9AD3-A196B82D60A8}.Release|Any CPU.Build.0 = Release|Any CPU
{2A0E708C-02D9-409D-8201-DA291AB8B85E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A0E708C-02D9-409D-8201-DA291AB8B85E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A0E708C-02D9-409D-8201-DA291AB8B85E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A0E708C-02D9-409D-8201-DA291AB8B85E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{80DF5F74-4690-4D8C-AA45-9E8758705192} = {A42886E3-3635-45C7-AFB9-3271A447B3A0}

64
Tests/Tests.fs Normal file
View File

@@ -0,0 +1,64 @@
open Expecto
open Oceanbox.FvcomKit
open Gridding.Polygon
let createGrid (nodes: Grid.Node array) (elems: Grid.Elem array) =
{
Elem = elems
Nodes = nodes
BBox = nodes |> Grid.calcBBox
}
: Grid.Grid
let minimalGrid: Grid.Grid =
// Simple square
let nodes: Grid.Node array = [| (1., 1.); (1., 2.); (2., 2.); (2., 1.) |]
let elems: Grid.Elem array = [| (0, 1, 2); (0, 2, 3) |]
(nodes, elems) ||> createGrid
let withInnerPoint: Grid.Grid =
let nodes: Grid.Node array = [|
(1., 1.)
(1., 2.)
(1., 3.)
(2., 3.)
(3., 3.)
(3., 2.)
(3., 1.)
(2., 1.)
(2., 2.)
|]
let elems: Grid.Elem array = [|
(0, 1, 7)
(1, 8, 7) // bottom-left square
(7, 8, 6)
(8, 5, 6) // bottom-right square
(1, 2, 8)
(2, 3, 8) // top-left square
(8, 3, 5)
(3, 4, 5) // top-right square
|]
(nodes, elems) ||> createGrid
[<Tests>]
let coastlineTests =
testList "Coastline Tests" [
testCase "Minimal grid separate coastline"
<| fun () ->
let neighborIndex = Grid.makeNeighborIndex minimalGrid
// Expect all nodes are "coastal nodes"
let expected = ([| 0; 1; 2; 3 |], [||])
let res = separateCoastline neighborIndex minimalGrid
Expect.equal expected res "all should be part of coastline"
testCase "Grid with inner points separate coastline"
<| fun () ->
let neighborIndex = Grid.makeNeighborIndex withInnerPoint
// Inner point should not be a coastal node
let expected = ([| 0; 1; 2; 3; 4; 5; 6; 7 |], [| 8 |])
let res = separateCoastline neighborIndex withInnerPoint
Expect.equal expected res "inner point should be separated"
]
[<EntryPoint>]
let main args = runTestsInAssemblyWithCLIArgs [] args

18
Tests/Tests.fsproj Normal file
View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" />
<PackageReference Include="Oceanbox.FvcomKit" />
<PackageReference Include="SDSLite-O" />
<PackageReference Include="Serilog" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\Gridding\Gridding.fsproj" />
</ItemGroup>
</Project>

410
Tests/packages.lock.json Normal file
View File

@@ -0,0 +1,410 @@
{
"version": 2,
"dependencies": {
"net8.0": {
"Expecto": {
"type": "Direct",
"requested": "[10.2.1, )",
"resolved": "10.2.1",
"contentHash": "nV9ACIccY2cv23KvtMfflQdMPhF7oTXf0dRMz1Jq6GuQjLnuf+zBj4IlBzmZEz9VeCrCOWxdnt/5VFCrGcn+pA==",
"dependencies": {
"FSharp.Core": "7.0.200",
"Mono.Cecil": "[0.11.4, 1.0.0)"
}
},
"FSharp.Analyzers.Build": {
"type": "Direct",
"requested": "[0.3.0, )",
"resolved": "0.3.0",
"contentHash": "twj4mNJLOEjHK6OQJCAbix2d8ODG5RySASejQ/v2fLgoqyRqYsXmceFaXb17ZE1pNrfenjwv/xpR+GembufaZA=="
},
"G-Research.FSharp.Analyzers": {
"type": "Direct",
"requested": "[0.9.3, )",
"resolved": "0.9.3",
"contentHash": "V83sHl7ubgQ9HLjxto/rZbgNKGMMpHZ+E6tmWm1JAGfpNUaYWnKdAkpdKxxooHmE/804pIAPuVTpna5xLeX1dw=="
},
"Ionide.Analyzers": {
"type": "Direct",
"requested": "[0.9.0, )",
"resolved": "0.9.0",
"contentHash": "yCJHdPHJef/Yl7iPED8lRoxPjM1HXx1tielT7lp40FxOKURiLGziyhxfZqbo4WTYVKdSxOJv3+s2+ArJYuirvg=="
},
"Oceanbox.FvcomKit": {
"type": "Direct",
"requested": "[5.5.3, )",
"resolved": "5.5.3",
"contentHash": "Rt9S3BF/4wjqNLlTpqnsRUqezA1NloY+MDMVMRYXCoACuXdDMAsuyT/YCCOKx2KDc1IZD4QDgWKuTUS4r4F6bQ==",
"dependencies": {
"FSharp.Core": "8.0.100",
"FSharp.Data": "6.3.0",
"FSharpPlus": "1.5.0",
"FsPickler": "5.3.2",
"KDTree": "1.4.1",
"MathNet.Numerics.FSharp": "5.0.0",
"ProjNet.FSharp": "5.2.0",
"SDSlite.Oceanbox": "2.7.3",
"Serilog": "3.1.1",
"Serilog.Sinks.Console": "5.0.1",
"Serilog.Sinks.Seq": "6.0.0",
"Thoth.Json.Net": "11.0.0"
}
},
"SDSLite-O": {
"type": "Direct",
"requested": "[2.7.2, )",
"resolved": "2.7.2",
"contentHash": "UzRh4jECBP4Ee/6lDHRowiEMDhT0tPuJd5Fr1Y2lwvKx4Sfk3MzaZBaKuaeJliwvuUm6wEupHoGELWhQVTuwBw==",
"dependencies": {
"DynamicInterop": "0.9.1"
}
},
"Serilog": {
"type": "Direct",
"requested": "[4.0.1, )",
"resolved": "4.0.1",
"contentHash": "pzeDRXdpSLSsgBHpZcmpIDxqMy845Ab4s+dfnBg0sN9h8q/4Wo3vAoe0QCGPze1Q06EVtEPupS+UvLm8iXQmTQ=="
},
"DynamicInterop": {
"type": "Transitive",
"resolved": "0.9.1",
"contentHash": "n21+Hd+tceX8lgaOosPV+Pne+YqnZUd5RLW3OhnsVxWRzYXiAIAKmKweHIePYeY+fmcn3N5tjkJyQUccFuL3bg=="
},
"Fable.Core": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "w6M1F0zoLk4kTFc1Lx6x1Ft6BD3QwRe0eaLiinAqbjVkcF+iK+NiXGJO+a6q9RAF9NCg0vI48Xku7aNeqG4JVw==",
"dependencies": {
"FSharp.Core": "4.7.1"
}
},
"FSharp.Core": {
"type": "Transitive",
"resolved": "8.0.100",
"contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg=="
},
"FSharp.Data": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "nzbYXC+erJeSgBadHTbQqb3J4pTNQ3NCuKIFpuMjAFDdF+oZLYGWMrOEXK3XpfKfs9G//D6rZkK6m2MS59CWHQ==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Csv.Core": "6.3.0",
"FSharp.Data.Html.Core": "6.3.0",
"FSharp.Data.Http": "6.3.0",
"FSharp.Data.Json.Core": "6.3.0",
"FSharp.Data.Runtime.Utilities": "6.3.0",
"FSharp.Data.WorldBank.Core": "6.3.0",
"FSharp.Data.Xml.Core": "6.3.0"
}
},
"FSharp.Data.Csv.Core": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "esvZUiizgxPlzYQSKRy7VhwY1CRXRQuuwDkfP4ajCcUayHOl4Ny+Tb8HRplA4kcIVEdkAiQW2+xRIEAFP/jl/g==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Runtime.Utilities": "6.3.0"
}
},
"FSharp.Data.Html.Core": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "tvG2LYcVvRD+BCN1pnjljgYl8V6q7w7ZMZ45KtTmK6kQtYxzDA+Wmqd4ZU1qh1btPwxp/+KVLoXcVU07Ltm/FQ==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Csv.Core": "6.3.0",
"FSharp.Data.Runtime.Utilities": "6.3.0"
}
},
"FSharp.Data.Http": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "8NcHLQpqXD0cx9SIBoNGfCroBx/gyAR3aR20Ax+TQGG7lUi+HSi3FD/Y0PBHiHIJQHBPsGKmJ9ADz3vqdXwtnw==",
"dependencies": {
"FSharp.Core": "5.0.1"
}
},
"FSharp.Data.Json.Core": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "vKqMe/FagHZJRqyQMyg2Af3xeDldO9w4SN6awq8KoxIf963GqT4w2E7FpH9VNNbMzH6+91rz54uTwwC0huD+qQ==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Http": "6.3.0",
"FSharp.Data.Runtime.Utilities": "6.3.0"
}
},
"FSharp.Data.Runtime.Utilities": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "9hKUvcqn/sC6SrVz1Rjeswtn62mG4OMgFh3/dAJD6bgeaMuMjj6wS9So/DtQXwgOGk44icn+QN659OoFtDKDlw==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Http": "6.3.0"
}
},
"FSharp.Data.WorldBank.Core": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "kW52EsL4oquFohn0i7mopdWilNKmJlDmdaM2pA1QN8frrX+K731mkg44dtASG7kFQeccwfpq3uJb2Dtpo/VIaQ==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Http": "6.3.0",
"FSharp.Data.Json.Core": "6.3.0",
"FSharp.Data.Runtime.Utilities": "6.3.0"
}
},
"FSharp.Data.Xml.Core": {
"type": "Transitive",
"resolved": "6.3.0",
"contentHash": "P+ktC5XRZF+P2BgGj9J6ygviu8AQ9CSSmrNT/9Ye815+pEaPN0Vj6u+tc8TvNxLlee24VKIC0j3x4TVSBVtqAQ==",
"dependencies": {
"FSharp.Core": "5.0.1",
"FSharp.Data.Http": "6.3.0",
"FSharp.Data.Json.Core": "6.3.0",
"FSharp.Data.Runtime.Utilities": "6.3.0"
}
},
"FSharpPlus": {
"type": "Transitive",
"resolved": "1.5.0",
"contentHash": "s7pkW1pmUiPDhFC7VqlBpex0QbLPnPuY5gKOrVG7ewWtvJPYs3hhHAcDNzthpNkqZ5iRVPKQSMngAwYBRtKMyw==",
"dependencies": {
"FSharp.Core": "6.0.6"
}
},
"FsPickler": {
"type": "Transitive",
"resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": {
"FSharp.Core": "4.3.2",
"System.Reflection.Emit.Lightweight": "4.3.0"
}
},
"KdTree": {
"type": "Transitive",
"resolved": "1.4.1",
"contentHash": "yWbb35v/V9y88SLLMUPTlAN3pQEoPhDfZf9PApFnlU4kLtwVQ75U9vW5mW4/alQnLBuLKWBKcy4W5xK95mYsuA=="
},
"MathNet.Numerics": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "pg1W2VwaEQMAiTpGK840hZgzavnqjlCMTVSbtVCXVyT+7AX4mc1o89SPv4TBlAjhgCOo9c1Y+jZ5m3ti2YgGgA=="
},
"MathNet.Numerics.FSharp": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "lKYhd68fReW5odX/q+Uzxw3357Duq3zmvkYvnZVqqcc2r/EmrYGDoOdUGuHnhfr8yj9V34js5gQH/7IWcxZJxg==",
"dependencies": {
"FSharp.Core": "6.0.2",
"MathNet.Numerics": "5.0.0"
}
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"Microsoft.NETCore.Targets": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
},
"Mono.Cecil": {
"type": "Transitive",
"resolved": "0.11.4",
"contentHash": "IC1h5g0NeJGHIUgzM1P82ld57knhP0IcQfrYITDPXlNpMYGUrsG5TxuaWTjaeqDNQMBDNZkB8L0rBnwsY6JHuQ=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "11.0.2",
"contentHash": "IvJe1pj7JHEsP8B8J8DwlMEx8UInrs/x+9oVY+oCD13jpLu4JbJU2WCIsMRn5C4yW9+DgkaO8uiVE5VHKjpmdQ=="
},
"ProjNET": {
"type": "Transitive",
"resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
},
"ProjNet.FSharp": {
"type": "Transitive",
"resolved": "5.2.0",
"contentHash": "sYSePg/0sVo16Fk3r7okVSga6i9GAN0kkjt1haEXVw25SF8A4S3Gcpf5+6lgknBGdYiZBmJ+3S6v5g1WSSCp2g==",
"dependencies": {
"FSharp.Core": "8.0.100",
"FSharp.Data": "6.3.0",
"FSharpPlus": "1.5.0",
"ProjNet": "2.0.0"
}
},
"SDSLite.Oceanbox": {
"type": "Transitive",
"resolved": "2.7.3",
"contentHash": "tmTPsEUmQhwaCzHwuSw7he2FfjcVpZ/Sy2ewfTwm1IKnwOZazKouTS5t4LNUpaGtjK1o/gdfz1b+0KxXnUl97g==",
"dependencies": {
"DynamicInterop": "0.9.1"
}
},
"Serilog.Formatting.Compact": {
"type": "Transitive",
"resolved": "2.0.0",
"contentHash": "ob6z3ikzFM3D1xalhFuBIK1IOWf+XrQq+H4KeH4VqBcPpNcmUgZlRQ2h3Q7wvthpdZBBoY86qZOI2LCXNaLlNA==",
"dependencies": {
"Serilog": "3.1.0"
}
},
"Serilog.Sinks.Console": {
"type": "Transitive",
"resolved": "5.0.1",
"contentHash": "6Jt8jl9y2ey8VV7nVEUAyjjyxjAQuvd5+qj4XYAT9CwcsvR70HHULGBeD+K2WCALFXf7CFsNQT4lON6qXcu2AA==",
"dependencies": {
"Serilog": "3.1.1"
}
},
"Serilog.Sinks.File": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==",
"dependencies": {
"Serilog": "2.10.0"
}
},
"Serilog.Sinks.PeriodicBatching": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "NDWR7m3PalVlGEq3rzoktrXikjFMLmpwF0HI4sowo8YDdU+gqPlTHlDQiOGxHfB0sTfjPA9JjA7ctKG9zqjGkw==",
"dependencies": {
"Serilog": "2.0.0"
}
},
"Serilog.Sinks.Seq": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "LtxlH5xE3ZPxmCYL5+I8tPzytnR91xfFFIIUIcpoGq69a45eyFkrVMonApww+B08a0I++GfM7jP1oB6GBhOR1w==",
"dependencies": {
"Serilog": "3.1.1",
"Serilog.Formatting.Compact": "2.0.0",
"Serilog.Sinks.File": "5.0.0",
"Serilog.Sinks.PeriodicBatching": "3.1.0"
}
},
"System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.3",
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Emit.ILGeneration": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
"dependencies": {
"System.Reflection": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Emit.Lightweight": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
"dependencies": {
"System.Reflection": "4.3.0",
"System.Reflection.Emit.ILGeneration": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"Thoth.Json.Net": {
"type": "Transitive",
"resolved": "11.0.0",
"contentHash": "ugheFKMHRO3ReobCENha5J6uexPrp+Bn2d+WEcFbXaA77sNBWtTlx2StB+7lX8prMqdvO5uqlPeHlg+9dSpkNg==",
"dependencies": {
"FSharp.Core": "4.7.2",
"Fable.Core": "3.1.6",
"Newtonsoft.Json": "11.0.2"
}
},
"gridding": {
"type": "Project",
"dependencies": {
"Oceanbox.FvcomKit": "[5.5.3, )",
"SDSLite-O": "[2.7.2, )",
"Serilog": "[4.0.1, )"
}
}
}
}
}

View File

@@ -21,12 +21,13 @@
with pkgs;
{
devShells.default = mkShell {
nativeBuildInputs = [
netcdf
dotnet-sdk_8
nativeBuildInputs = [
fsautocomplete
netcdf
dotnet-sdk_8
];
LD_LIBRARY_PATH = lib.makeLibraryPath [netcdf];
};
LD_LIBRARY_PATH = lib.makeLibraryPath [ netcdf ];
};
}
);
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Util.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,141 +1,145 @@
open System
open System.IO
open Argu
open Oceanbox.FvcomKit
open Microsoft.Research.Science.Data
open Gridding.Polygon
open Serilog
open Serilog.Events
[<NoAppSettings>]
type Arguments =
| [<MainCommand; ExactlyOnce; Last>] Input_Grid of path: string
| [<AltCommandLine("-o")>] Output_Grid of path: string option
| [<AltCommandLine("-n")>] No_Reduce
| [<AltCommandLine("-q")>] Quiet
| [<AltCommandLine("-v")>] Verbose
interface IArgParserTemplate with
member arg.Usage =
match arg with
| Input_Grid _ -> "specify input netcdf grid file."
| Output_Grid _ -> "write result to file; optionally path to the netcdf file can be specified."
| No_Reduce -> "do not perform reduction on the grid."
| Quiet -> "disable all logging."
| Verbose -> "enable verbose logging."
let configureSerilog level =
LoggerConfiguration()
.MinimumLevel.Is(level)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Console()
.CreateLogger()
let colorizer =
function
| ErrorCode.HelpText -> None
| _ -> Some ConsoleColor.Red
let errorHandler = ProcessExiter(colorizer = colorizer)
[<EntryPoint>]
let main argv =
let parser = ArgumentParser.Create<Arguments>(programName = "greduce", errorHandler = errorHandler)
let args = parser.ParseCommandLine argv
let logLevel =
match args.Contains(Verbose), args.Contains(Quiet) with
| true, false -> LogEventLevel.Verbose
| false, true -> LogEventLevel.Error
| true, true ->
printfn "Verbose and silent mode both specified, logging verbosely"
LogEventLevel.Verbose
| false, false -> LogEventLevel.Information
Log.Logger <- configureSerilog logLevel
// Whether to perform reduction. Defaults to true
let shouldReduce = args.Contains(No_Reduce) |> not
// Default to a filepath, but don't write to file if flag is not passed
let outPath =
if args.Contains(Output_Grid) then
args.GetResult(Output_Grid, defaultValue = None)
|> Option.defaultValue "testfile.nc"
|> Some
else
None
// Path of input file. Main argument and mandatory
let path = args.GetResult(Input_Grid)
Log.Information $"Using input file: %s{path}"
let ds =
DataSetUri.Create path
|> DataSet.Open
let grd = ds |> Fvcom.getGrid
let neighborIndex =
grd.ToGrid () |> Grid.makeNeighborIndex
let coastNodes, innerNodes = separateCoastline neighborIndex (grd.ToGrid ())
coastNodes
|> Array.tryFind (fun n ->
Map.find n neighborIndex.NodesAroundNode
|> Array.length > 4)
|> function
| Some n -> Log.Verbose $"Found non edge node: {n}"
| None -> Log.Verbose "No non edge node found"
Log.Verbose $"Coast nodes: {coastNodes.Length} \t Inner nodes: {innerNodes.Length}"
Log.Verbose "Polygons: "
let polygons = groupByPolygon coastNodes neighborIndex
polygons
|> Array.iter (fun n -> Log.Verbose $"{n.Length}")
// Assume longest polygon is boundary
let boundary =
polygons
|> Array.maxBy (_.Length)
let rest =
polygons
|> Array.filter (fun p -> p <> boundary)
let boundaryStrings =
getPolygonString boundary grd
let restStrings =
rest
|> Array.map (fun p -> getPolygonString p grd)
|> Array.concat
File.WriteAllLines("boundary.txt", boundaryStrings)
File.WriteAllLines("islands.txt", restStrings)
let innerNodes =
if shouldReduce then
Log.Verbose "Reducing inner nodes..."
innerNodes |> Gridding.Reduce.reduceNodes
else
Log.Verbose "Proceeding without reducing"
innerNodes
if shouldReduce then
Log.Verbose $"reduced nodes: {innerNodes.Length}, total nodes: {grd.Nodes.Length}"
let innerNodeStrings =
innerNodes
|> Array.map (fun nIdx ->
grd.Nodes[nIdx]
|> fun n -> $"{fst n} {snd n}")
File.WriteAllLines("points.txt", innerNodeStrings)
match outPath with
| None -> ()
| Some p ->
if File.Exists p then File.Delete p
Gridding.Fvcom.writeReducedGrid ([ coastNodes; innerNodes ] |> Array.concat) grd ds p
Log.Verbose $"Wrote results to {p}"
Log.Information "Done!"
0
open System
open System.IO
open Argu
open Oceanbox.FvcomKit
open Microsoft.Research.Science.Data
open Gridding.Polygon
open Serilog
open Serilog.Events
[<NoAppSettings>]
type Arguments =
| [<MainCommand; ExactlyOnce; Last>] Input_Grid of path: string
| [<AltCommandLine("-o")>] Output_Grid of path: string option
| [<AltCommandLine("-n")>] No_Reduce
| [<AltCommandLine("-q")>] Quiet
| [<AltCommandLine("-v")>] Verbose
interface IArgParserTemplate with
member arg.Usage =
match arg with
| Input_Grid _ -> "specify input netcdf grid file."
| Output_Grid _ ->
"write result to file; optionally path to the netcdf file can be specified."
| No_Reduce -> "do not perform reduction on the grid."
| Quiet -> "disable all logging."
| Verbose -> "enable verbose logging."
let configureSerilog level =
LoggerConfiguration()
.MinimumLevel.Is(level)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Console()
.CreateLogger()
let colorizer =
function
| ErrorCode.HelpText -> None
| _ -> Some ConsoleColor.Red
let errorHandler = ProcessExiter(colorizer = colorizer)
[<EntryPoint>]
let main argv =
let parser =
ArgumentParser.Create<Arguments>(
programName = "greduce",
errorHandler = errorHandler
)
let args = parser.ParseCommandLine argv
let logLevel =
match args.Contains(Verbose), args.Contains(Quiet) with
| true, false -> LogEventLevel.Verbose
| false, true -> LogEventLevel.Error
| true, true ->
printfn "Verbose and silent mode both specified, logging verbosely"
LogEventLevel.Verbose
| false, false -> LogEventLevel.Information
Log.Logger <- configureSerilog logLevel
// Whether to perform reduction. Defaults to true
let shouldReduce = args.Contains(No_Reduce) |> not
// Default to a filepath, but don't write to file if flag is not passed
let outPath =
if args.Contains(Output_Grid) then
args.GetResult(Output_Grid, defaultValue = None)
|> Option.defaultValue "testfile.nc"
|> Some
else
None
// Path of input file. Main argument and mandatory
let path = args.GetResult(Input_Grid)
Log.Information $"Using input file: %s{path}"
let ds = DataSetUri.Create path |> DataSet.Open
let grd = ds |> Fvcom.getGrid
let neighborIndex = grd.ToGrid() |> Grid.makeNeighborIndex
let coastNodes, innerNodes = separateCoastline neighborIndex (grd.ToGrid())
coastNodes
|> Array.tryFind (fun n ->
Map.find n neighborIndex.NodesAroundNode |> Array.length > 4
)
|> function
| Some n -> Log.Verbose $"Found non edge node: {n}"
| None -> Log.Verbose "No non edge node found"
Log.Verbose
$"Coast nodes: {coastNodes.Length} \t Inner nodes: {innerNodes.Length}"
Log.Verbose "Polygons: "
let polygons = groupByPolygon coastNodes neighborIndex
polygons |> Array.iter (fun n -> Log.Verbose $"{n.Length}")
// Assume longest polygon is boundary
let boundary = polygons |> Array.maxBy (_.Length)
let rest = polygons |> Array.filter (fun p -> p <> boundary)
let boundaryStrings = getPolygonString boundary grd
let restStrings =
rest |> Array.map (fun p -> getPolygonString p grd) |> Array.concat
File.WriteAllLines("boundary.txt", boundaryStrings)
File.WriteAllLines("islands.txt", restStrings)
let innerNodes =
if shouldReduce then
Log.Verbose "Reducing inner nodes..."
innerNodes |> Gridding.Reduce.reduceNodes
else
Log.Verbose "Proceeding without reducing"
innerNodes
if shouldReduce then
Log.Verbose
$"reduced nodes: {innerNodes.Length}, total nodes: {grd.Nodes.Length}"
let innerNodeStrings =
innerNodes
|> Array.map (fun nIdx ->
grd.Nodes[nIdx] |> fun n -> $"{fst n} {snd n}"
)
File.WriteAllLines("points.txt", innerNodeStrings)
match outPath with
| None -> ()
| Some p ->
if File.Exists p then
File.Delete p
Gridding.Fvcom.writeReducedGrid
([ coastNodes; innerNodes ] |> Array.concat)
grd
ds
p
Log.Verbose $"Wrote results to {p}"
Log.Information "Done!"
0

8
src/CLI/Util.fs Normal file
View File

@@ -0,0 +1,8 @@
module CLI.Util
open Oceanbox.FvcomKit
let writeMakaiMesh (grid: Fvcom.FvcomGrid) =
grid.Nodes
|> Array.map (fun (x, y) -> $"{x} {y}")
|> Array.append [| $"{grid.Nodes.Length}" |]

View File

@@ -6,16 +6,15 @@ open ProjNet.FSharp
let private projection = CoordSys.UTMn 33
let private utm2ll =
makeTransform projection CoordSys.WGS84
let private utm2ll = makeTransform projection CoordSys.WGS84
/// <summary>
/// Return an array containing only the items on the
/// given indices
/// </summary>
/// <param name="nodes">Indices to keep</param>
/// <param name="arr">Array to reduce</param>
let private reduce (nodes: Grid.NodeIdx []) (arr: 'T []) =
let private reduce (nodes: Grid.NodeIdx[]) (arr: 'T[]) =
nodes |> Array.map (fun idx -> arr[idx])
/// <summary>
@@ -25,29 +24,30 @@ let private reduce (nodes: Grid.NodeIdx []) (arr: 'T []) =
/// <param name="nodes">Node indices from grid to write</param>
/// <param name="grid">Original grid</param>
/// <param name="file">Open NetCDFDataSet file handle</param>
let private writeFGrid (nodes: Grid.NodeIdx []) (grid: Fvcom.FvcomGrid) (file: NetCDF4.NetCDFDataSet) =
let private writeFGrid
(nodes: Grid.NodeIdx[])
(grid: Fvcom.FvcomGrid)
(file: NetCDF4.NetCDFDataSet)
=
let x, y =
nodes
|> Array.map (fun idx ->
grid.Nodes[idx]
|> (fun n -> float32 (fst n), float32 (snd n)))
grid.Nodes[idx] |> (fun n -> float32 (fst n), float32 (snd n))
)
|> Array.unzip
let lon, lat = Array.zip x y |> Array.map utm2ll.project |> Array.unzip
let h =
nodes
|> Array.map (fun idx ->
grid.Bathymetry.[idx])
let h = nodes |> Array.map (fun idx -> grid.Bathymetry.[idx])
printfn $"node dimension length: {nodes.Length}"
printfn $"x length: {x.Length}, y Length: {y.Length}"
file.AddVariable<float32>("x", x, [|"node"|]) |> ignore
file.AddVariable<float32>("y", y, [|"node"|]) |> ignore
file.AddVariable<float32>("lon", lon, [|"node"|]) |> ignore
file.AddVariable<float32>("lat", lat, [|"node"|]) |> ignore
file.AddVariable<float32>("h", h, [|"node"|]) |> ignore
file.AddVariable<float32>("x", x, [| "node" |]) |> ignore
file.AddVariable<float32>("y", y, [| "node" |]) |> ignore
file.AddVariable<float32>("lon", lon, [| "node" |]) |> ignore
file.AddVariable<float32>("lat", lat, [| "node" |]) |> ignore
file.AddVariable<float32>("h", h, [| "node" |]) |> ignore
file
/// <summary>
@@ -58,65 +58,75 @@ let private writeFGrid (nodes: Grid.NodeIdx []) (grid: Fvcom.FvcomGrid) (file: N
/// <param name="nodes"></param>
/// <param name="ds"></param>
/// <param name="file"></param>
let private writeVariables (nodes: Grid.NodeIdx []) (ds: DataSet) (file: NetCDF4.NetCDFDataSet) =
let private writeVariables
(nodes: Grid.NodeIdx[])
(ds: DataSet)
(file: NetCDF4.NetCDFDataSet)
=
let timeFrames = Fvcom.getNumFrames ds
file.CreateDimension("time", timeFrames)
let siglay = Fvcom.getNumSiglay ds
// Don't create dimension, use only z dimension when writing new file
// Read zeta values for each time frame,
// add readings to a 2d array
let rec readZetas (t: int) (acc: single [][]) =
let rec readZetas (t: int) (acc: single[][]) =
match t with
| x when x >= timeFrames -> acc
| _ ->
Fvcom.readZeta ds t
|> Array.map single
|> reduce nodes
|> fun a -> [|a|]
|> fun a -> [| a |]
|> Array.append acc
|> readZetas (t + 1)
let reducedZetas =
readZetas 0 [||]
// |> fun a ->
// printfn $"{a.Length}"
// a
|> fun a -> Array2D.init timeFrames nodes.Length (fun i j -> a[i][j])
// |> fun a ->
// printfn $"{a.Length}"
// a
file.AddVariable<single>("zeta", Array.empty, [|"time"; "node"|]) |> ignore
file.Variables[ "zeta" ].PutData([| 0; 0 |], reducedZetas)
// |> fun a ->
// printfn $"{a.Length}"
// a
file.AddVariable<single>("zeta", Array.empty, [| "time"; "node" |])
|> ignore
file.Variables["zeta"].PutData([| 0; 0 |], reducedZetas)
// Read salinity for each timeframe,
// use top and bottom layers only
let rec readSalinity (t: int) (acc: single [][][]) =
let rec readSalinity (t: int) (acc: single[][][]) =
// Helper to read at given layer
let readAtLayer (l: int) =
Fvcom.readSalinity ds t l
|> Array.map single
|> reduce nodes
|> fun a -> [|a|]
|> fun a -> [| a |]
match t with
| x when x >= timeFrames -> acc
| _ ->
let bottomLayer = readAtLayer 0
let topLayer = readAtLayer siglay
Array.append bottomLayer topLayer
|> fun a -> [|a|]
|> fun a -> [| a |]
|> Array.append acc
|> readSalinity (t + 1)
let reducedSalinity =
readSalinity 0 [||]
|> fun a -> Array3D.init timeFrames 2 nodes.Length (fun i j k -> a.[i].[j].[k])
file.AddVariable<single>("salinity", Array.empty, [|"time"; "z"; "node"|]) |> ignore
file.Variables[ "salinity" ].PutData([| 0; 0; 0 |], reducedSalinity)
|> fun a ->
Array3D.init timeFrames 2 nodes.Length (fun i j k -> a.[i].[j].[k])
file.AddVariable<single>("salinity", Array.empty, [| "time"; "z"; "node" |])
|> ignore
file.Variables["salinity"]
.PutData([| 0; 0; 0 |], reducedSalinity)
/// <summary>
/// Write a new grid based on an original grid using only data from the
@@ -126,14 +136,17 @@ let private writeVariables (nodes: Grid.NodeIdx []) (ds: DataSet) (file: NetCDF4
/// <param name="grid">Grid structure read from the original grid</param>
/// <param name="ds">The original dataset</param>
/// <param name="filename">Path of output file</param>
let writeReducedGrid (nodes: Grid.NodeIdx []) (grid: Fvcom.FvcomGrid) (ds: DataSet) (filename: string) =
let writeReducedGrid
(nodes: Grid.NodeIdx[])
(grid: Fvcom.FvcomGrid)
(ds: DataSet)
(filename: string)
=
let file = new NetCDF4.NetCDFDataSet(filename)
file.CreateDimension("node", nodes.Length)
file.CreateDimension("z", 2)
file
|> writeFGrid nodes grid
|> writeVariables nodes ds
file.Commit ()
file.Dispose ()
file |> writeFGrid nodes grid |> writeVariables nodes ds
file.Commit()
file.Dispose()

View File

@@ -11,41 +11,43 @@ open Oceanbox.FvcomKit
let separateCoastline (nIdx: Grid.NeighborIndex) (grid: Grid.Grid) =
let getEdges (a, b, c) =
[(a, b); (b, a); (a, c); (c, a); (b, c); (c, b)]
|> Set.ofList
[ (a, b); (b, a); (a, c); (c, a); (b, c); (c, b) ] |> Set.ofList
// Find whether an elem has less than two elems that it
// shares an edge with
let isEdgeElem (elemIdx: Grid.ElemIdx): bool =
let isEdgeElem (elemIdx: Grid.ElemIdx) : bool =
let ownEdges = grid.Elem[elemIdx] |> getEdges
Grid.getElemsSurroundingElem nIdx grid elemIdx
|> Array.filter (fun surroundingIdx ->
// Avoid including self
if surroundingIdx = elemIdx then
// Avoid including self
if surroundingIdx = elemIdx then
false
else
else
grid.Elem[surroundingIdx]
|> getEdges
|> Set.intersect ownEdges
|> Set.count > 0
)
|> Array.length <= 2
)
|> Array.length
<= 2
let cNodes =
([||], nIdx.ElemsAroundNode) ||> Map.fold (fun cNodes idx neighborElems ->
neighborElems
|> Array.tryFind (fun eIdx ->
eIdx |> isEdgeElem)
|> function
| Some _ -> Array.append cNodes [| idx |]
| None -> cNodes)
([||], nIdx.ElemsAroundNode)
||> Map.fold (fun cNodes idx neighborElems ->
neighborElems
|> Array.tryFind (fun eIdx -> eIdx |> isEdgeElem)
|> function
| Some _ -> Array.append cNodes [| idx |]
| None -> cNodes
)
|> Set.ofArray
let iNodes =
[| 0 .. (grid.Nodes.Length - 1) |]
|> Set.ofArray
|> fun allNodes -> Set.difference allNodes cNodes
(cNodes |> Array.ofSeq, iNodes |> Array.ofSeq)
/// <summary>
@@ -53,37 +55,37 @@ let separateCoastline (nIdx: Grid.NeighborIndex) (grid: Grid.Grid) =
/// </summary>
/// <param name="nodes">Edge node indices</param>
/// <param name="nIdx"></param>
let groupByPolygon (nodes: Grid.NodeIdx []) (nIdx: Grid.NeighborIndex) =
let groupByPolygon (nodes: Grid.NodeIdx[]) (nIdx: Grid.NeighborIndex) =
let nMap = nIdx.NodesAroundNode
let isEdgeNode (node: Grid.NodeIdx) =
nodes
|> Array.contains node
let isEdgeNode (node: Grid.NodeIdx) = nodes |> Array.contains node
// Find a single polygon given an edge node
let rec findPolygon (node: Grid.NodeIdx) (acc: Set<Grid.NodeIdx>): Set<Grid.NodeIdx> =
let rec findPolygon
(node: Grid.NodeIdx)
(acc: Set<Grid.NodeIdx>)
: Set<Grid.NodeIdx> =
Map.find node nMap
|> Array.tryFind (fun n -> (n |> isEdgeNode) && (not <| (Set.contains n acc)))
|> Array.tryFind (fun n ->
(n |> isEdgeNode) && (not <| (Set.contains n acc))
)
|> function
| Some n ->
// Search deep, then check for remaining nodes
findPolygon n (acc |> Set.add node)
|> findPolygon n
findPolygon n (acc |> Set.add node) |> findPolygon n
| None -> acc
// Find all polygons given array of edge nodes
let rec getGroups (remaining: Set<Grid.NodeIdx>) (acc: Grid.NodeIdx[][]) =
match remaining.Count with
| 0 -> acc
| _ ->
let polygon =
findPolygon (remaining |> Set.minElement) Set.empty
let newRemaining =
Set.difference remaining polygon
let polygon = findPolygon (remaining |> Set.minElement) Set.empty
let newRemaining = Set.difference remaining polygon
getGroups
(newRemaining)
newRemaining
(Array.append acc [| polygon |> Array.ofSeq |])
getGroups (nodes |> Set.ofArray) [||]
/// <summary>
@@ -94,6 +96,6 @@ let groupByPolygon (nodes: Grid.NodeIdx []) (nIdx: Grid.NeighborIndex) =
let getPolygonString (nodes: Grid.NodeIdx array) (grid: Fvcom.FvcomGrid) =
nodes
|> Array.map (fun nIdx ->
grid.Nodes[nIdx]
|> fun n -> $"{fst n} {snd n}")
grid.Nodes[nIdx] |> fun n -> $"{fst n} {snd n} 0.25"
)
|> Array.append [| $"{nodes.Length}" |]

View File

@@ -7,12 +7,14 @@ open Oceanbox.FvcomKit
/// Return a subset of the given node indices
/// </summary>
/// <param name="nodes"></param>
let reduceNodes (nodes: Grid.NodeIdx []) =
let reduceNodes (nodes: Grid.NodeIdx[]) =
nodes
|> Array.choose (fun x ->
// Randomly remove half of array
let rnd = Random ()
rnd.Next 2
let rnd = Random()
rnd.Next 2
|> function
| 0 -> Some x
| _ -> None)
| _ -> None
)