diff --git a/Manifest.toml b/Manifest.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2e9e6fd0400115d57e3da7ffec294313e63f6fcb
--- /dev/null
+++ b/Manifest.toml
@@ -0,0 +1,483 @@
+# This file is machine-generated - editing it directly is not advised
+
+julia_version = "1.7.0"
+manifest_format = "2.0"
+
+[[deps.Adapt]]
+deps = ["LinearAlgebra"]
+git-tree-sha1 = "af92965fb30777147966f58acb05da51c5616b5f"
+uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
+version = "3.3.3"
+
+[[deps.ArgTools]]
+uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
+
+[[deps.Artifacts]]
+uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+
+[[deps.Base64]]
+uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+
+[[deps.BufferedStreams]]
+git-tree-sha1 = "bb065b14d7f941b8617bc323063dbe79f55d16ea"
+uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
+version = "1.1.0"
+
+[[deps.CategoricalArrays]]
+deps = ["DataAPI", "Future", "Missings", "Printf", "Requires", "Statistics", "Unicode"]
+git-tree-sha1 = "5f5a975d996026a8dd877c35fe26a7b8179c02ba"
+uuid = "324d7699-5711-5eae-9e2f-1d82baa6b597"
+version = "0.10.6"
+
+[[deps.ChainRulesCore]]
+deps = ["Compat", "LinearAlgebra", "SparseArrays"]
+git-tree-sha1 = "2dd813e5f2f7eec2d1268c57cf2373d3ee91fcea"
+uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
+version = "1.15.1"
+
+[[deps.ChangesOfVariables]]
+deps = ["ChainRulesCore", "LinearAlgebra", "Test"]
+git-tree-sha1 = "1e315e3f4b0b7ce40feded39c73049692126cf53"
+uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0"
+version = "0.1.3"
+
+[[deps.CircularArrays]]
+deps = ["OffsetArrays"]
+git-tree-sha1 = "3587fdbecba8c44f7e7285a1957182711b95f580"
+uuid = "7a955b69-7140-5f4e-a0ed-f168c5e2e749"
+version = "1.3.1"
+
+[[deps.CodecZlib]]
+deps = ["TranscodingStreams", "Zlib_jll"]
+git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da"
+uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
+version = "0.7.0"
+
+[[deps.Compat]]
+deps = ["Dates", "LinearAlgebra", "UUIDs"]
+git-tree-sha1 = "924cdca592bc16f14d2f7006754a621735280b74"
+uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
+version = "4.1.0"
+
+[[deps.CompilerSupportLibraries_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
+
+[[deps.Conda]]
+deps = ["Downloads", "JSON", "VersionParsing"]
+git-tree-sha1 = "6e47d11ea2776bc5627421d59cdcc1296c058071"
+uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d"
+version = "1.7.0"
+
+[[deps.Crayons]]
+git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
+uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
+version = "4.1.1"
+
+[[deps.DataAPI]]
+git-tree-sha1 = "fb5f5316dd3fd4c5e7c30a24d50643b73e37cd40"
+uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
+version = "1.10.0"
+
+[[deps.DataStructures]]
+deps = ["Compat", "InteractiveUtils", "OrderedCollections"]
+git-tree-sha1 = "d1fff3a548102f48987a52a2e0d114fa97d730f0"
+uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
+version = "0.18.13"
+
+[[deps.DataValueInterfaces]]
+git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
+uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464"
+version = "1.0.0"
+
+[[deps.Dates]]
+deps = ["Printf"]
+uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
+
+[[deps.DelimitedFiles]]
+deps = ["Mmap"]
+uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"
+
+[[deps.Distances]]
+deps = ["LinearAlgebra", "SparseArrays", "Statistics", "StatsAPI"]
+git-tree-sha1 = "3258d0659f812acde79e8a74b11f17ac06d0ca04"
+uuid = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
+version = "0.10.7"
+
+[[deps.DocStringExtensions]]
+deps = ["LibGit2"]
+git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b"
+uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
+version = "0.8.6"
+
+[[deps.Downloads]]
+deps = ["ArgTools", "LibCURL", "NetworkOptions"]
+uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
+
+[[deps.Future]]
+deps = ["Random"]
+uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820"
+
+[[deps.HDF5]]
+deps = ["Compat", "HDF5_jll", "Libdl", "Mmap", "Random", "Requires"]
+git-tree-sha1 = "9ffc57b9bb643bf3fce34f3daf9ff506ed2d8b7a"
+uuid = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
+version = "0.16.10"
+
+[[deps.HDF5_jll]]
+deps = ["Artifacts", "JLLWrappers", "LibCURL_jll", "Libdl", "OpenSSL_jll", "Pkg", "Zlib_jll"]
+git-tree-sha1 = "bab67c0d1c4662d2c4be8c6007751b0b6111de5c"
+uuid = "0234f1f7-429e-5d53-9886-15a909be8d59"
+version = "1.12.1+0"
+
+[[deps.InteractiveUtils]]
+deps = ["Markdown"]
+uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
+
+[[deps.InverseFunctions]]
+deps = ["Test"]
+git-tree-sha1 = "b3364212fb5d870f724876ffcd34dd8ec6d98918"
+uuid = "3587e190-3f89-42d0-90ee-14403ec27112"
+version = "0.1.7"
+
+[[deps.IrrationalConstants]]
+git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151"
+uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
+version = "0.1.1"
+
+[[deps.IterTools]]
+git-tree-sha1 = "fa6287a4469f5e048d763df38279ee729fbd44e5"
+uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
+version = "1.4.0"
+
+[[deps.IteratorInterfaceExtensions]]
+git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856"
+uuid = "82899510-4779-5014-852e-03e436cf321d"
+version = "1.0.0"
+
+[[deps.JLLWrappers]]
+deps = ["Preferences"]
+git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
+uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
+version = "1.4.1"
+
+[[deps.JSON]]
+deps = ["Dates", "Mmap", "Parsers", "Unicode"]
+git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
+uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
+version = "0.21.3"
+
+[[deps.JSON3]]
+deps = ["Dates", "Mmap", "Parsers", "StructTypes", "UUIDs"]
+git-tree-sha1 = "fd6f0cae36f42525567108a42c1c674af2ac620d"
+uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
+version = "1.9.5"
+
+[[deps.LazyArtifacts]]
+deps = ["Artifacts", "Pkg"]
+uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
+
+[[deps.LibCURL]]
+deps = ["LibCURL_jll", "MozillaCACerts_jll"]
+uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
+
+[[deps.LibCURL_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
+uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
+
+[[deps.LibGit2]]
+deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
+uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
+
+[[deps.LibSSH2_jll]]
+deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
+uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
+
+[[deps.Libdl]]
+uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+
+[[deps.LinearAlgebra]]
+deps = ["Libdl", "libblastrampoline_jll"]
+uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+
+[[deps.LogExpFunctions]]
+deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"]
+git-tree-sha1 = "09e4b894ce6a976c354a69041a04748180d43637"
+uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
+version = "0.3.15"
+
+[[deps.Logging]]
+uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
+
+[[deps.MAT]]
+deps = ["BufferedStreams", "CodecZlib", "HDF5", "SparseArrays"]
+git-tree-sha1 = "971be550166fe3f604d28715302b58a3f7293160"
+uuid = "23992714-dd62-5051-b70f-ba57cb901cac"
+version = "0.10.3"
+
+[[deps.MacroTools]]
+deps = ["Markdown", "Random"]
+git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf"
+uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
+version = "0.5.9"
+
+[[deps.Markdown]]
+deps = ["Base64"]
+uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
+
+[[deps.MbedTLS_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
+
+[[deps.Memoization]]
+deps = ["MacroTools"]
+git-tree-sha1 = "55dc27dc3d663900d1d768822528960acadc012a"
+uuid = "6fafb56a-5788-4b4e-91ca-c0cea6611c73"
+version = "0.1.14"
+
+[[deps.Meshes]]
+deps = ["CategoricalArrays", "CircularArrays", "Distances", "IterTools", "IteratorInterfaceExtensions", "LinearAlgebra", "NearestNeighbors", "Random", "RecipesBase", "ReferenceFrameRotations", "SimpleTraits", "SparseArrays", "SpecialFunctions", "StaticArrays", "StatsBase", "TableTraits", "Tables"]
+git-tree-sha1 = "037fc9fcce2a83d0c933284e0bc01450e4aa5389"
+uuid = "eacbb407-ea5a-433e-ab97-5258b1ca43fa"
+version = "0.22.10"
+
+[[deps.Missings]]
+deps = ["DataAPI"]
+git-tree-sha1 = "bf210ce90b6c9eed32d25dbcae1ebc565df2687f"
+uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
+version = "1.0.2"
+
+[[deps.Mmap]]
+uuid = "a63ad114-7e13-5084-954f-fe012c677804"
+
+[[deps.MozillaCACerts_jll]]
+uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
+
+[[deps.NearestNeighbors]]
+deps = ["Distances", "StaticArrays"]
+git-tree-sha1 = "0e353ed734b1747fc20cd4cba0edd9ac027eff6a"
+uuid = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
+version = "0.4.11"
+
+[[deps.NetworkOptions]]
+uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
+
+[[deps.OffsetArrays]]
+deps = ["Adapt"]
+git-tree-sha1 = "1ea784113a6aa054c5ebd95945fa5e52c2f378e7"
+uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
+version = "1.12.7"
+
+[[deps.OpenBLAS_jll]]
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
+uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
+
+[[deps.OpenLibm_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "05823500-19ac-5b8b-9628-191a04bc5112"
+
+[[deps.OpenSSL_jll]]
+deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
+git-tree-sha1 = "e60321e3f2616584ff98f0a4f18d98ae6f89bbb3"
+uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
+version = "1.1.17+0"
+
+[[deps.OpenSpecFun_jll]]
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
+git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
+uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
+version = "0.5.5+0"
+
+[[deps.OrderedCollections]]
+git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
+uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
+version = "1.4.1"
+
+[[deps.Parsers]]
+deps = ["Dates"]
+git-tree-sha1 = "0044b23da09b5608b4ecacb4e5e6c6332f833a7e"
+uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
+version = "2.3.2"
+
+[[deps.Pkg]]
+deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
+uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+
+[[deps.PlanarLarvae]]
+deps = ["DelimitedFiles", "HDF5", "JSON3", "MAT", "Meshes", "OrderedCollections", "SHA", "StaticArrays", "Statistics", "StructTypes"]
+git-tree-sha1 = "6d3fa1c8ab879bf27dcfc22fbfbbc2dad46846df"
+repo-rev = "dev"
+repo-url = "https://gitlab.pasteur.fr/nyx/planarlarvae.jl"
+uuid = "c2615984-ef14-4d40-b148-916c85b43307"
+version = "0.5.0"
+
+[[deps.Preferences]]
+deps = ["TOML"]
+git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
+uuid = "21216c6a-2e73-6563-6e65-726566657250"
+version = "1.3.0"
+
+[[deps.Printf]]
+deps = ["Unicode"]
+uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+
+[[deps.PyCall]]
+deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"]
+git-tree-sha1 = "1fc929f47d7c151c839c5fc1375929766fb8edcc"
+uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
+version = "1.93.1"
+
+[[deps.REPL]]
+deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
+uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
+
+[[deps.Random]]
+deps = ["SHA", "Serialization"]
+uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+
+[[deps.RecipesBase]]
+git-tree-sha1 = "6bf3f380ff52ce0832ddd3a2a7b9538ed1bcca7d"
+uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
+version = "1.2.1"
+
+[[deps.ReferenceFrameRotations]]
+deps = ["Crayons", "LinearAlgebra", "Printf", "Random", "StaticArrays"]
+git-tree-sha1 = "ec9bde2e30bc221e05e20fcec9a36a9c315e04a6"
+uuid = "74f56ac7-18b3-5285-802d-d4bd4f104033"
+version = "3.0.0"
+
+[[deps.Requires]]
+deps = ["UUIDs"]
+git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
+uuid = "ae029012-a4dd-5104-9daa-d747884805df"
+version = "1.3.0"
+
+[[deps.SHA]]
+uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
+
+[[deps.Serialization]]
+uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
+
+[[deps.SimpleTraits]]
+deps = ["InteractiveUtils", "MacroTools"]
+git-tree-sha1 = "5d7e3f4e11935503d3ecaf7186eac40602e7d231"
+uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d"
+version = "0.9.4"
+
+[[deps.Sockets]]
+uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
+
+[[deps.SortingAlgorithms]]
+deps = ["DataStructures"]
+git-tree-sha1 = "b3363d7460f7d098ca0912c69b082f75625d7508"
+uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c"
+version = "1.0.1"
+
+[[deps.SparseArrays]]
+deps = ["LinearAlgebra", "Random"]
+uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
+
+[[deps.SpecialFunctions]]
+deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
+git-tree-sha1 = "d75bda01f8c31ebb72df80a46c88b25d1c79c56d"
+uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
+version = "2.1.7"
+
+[[deps.StaticArrays]]
+deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"]
+git-tree-sha1 = "e972716025466461a3dc1588d9168334b71aafff"
+uuid = "90137ffa-7385-5640-81b9-e52037218182"
+version = "1.5.1"
+
+[[deps.StaticArraysCore]]
+git-tree-sha1 = "66fe9eb253f910fe8cf161953880cfdaef01cdf0"
+uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
+version = "1.0.1"
+
+[[deps.Statistics]]
+deps = ["LinearAlgebra", "SparseArrays"]
+uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
+
+[[deps.StatsAPI]]
+deps = ["LinearAlgebra"]
+git-tree-sha1 = "2c11d7290036fe7aac9038ff312d3b3a2a5bf89e"
+uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0"
+version = "1.4.0"
+
+[[deps.StatsBase]]
+deps = ["DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"]
+git-tree-sha1 = "48598584bacbebf7d30e20880438ed1d24b7c7d6"
+uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
+version = "0.33.18"
+
+[[deps.StructTypes]]
+deps = ["Dates", "UUIDs"]
+git-tree-sha1 = "d24a825a95a6d98c385001212dc9020d609f2d4f"
+uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
+version = "1.8.1"
+
+[[deps.TOML]]
+deps = ["Dates"]
+uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
+
+[[deps.TableTraits]]
+deps = ["IteratorInterfaceExtensions"]
+git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39"
+uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c"
+version = "1.0.1"
+
+[[deps.Tables]]
+deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "OrderedCollections", "TableTraits", "Test"]
+git-tree-sha1 = "5ce79ce186cc678bbb5c5681ca3379d1ddae11a1"
+uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
+version = "1.7.0"
+
+[[deps.TaggingBackends]]
+deps = ["Dates", "HDF5", "LazyArtifacts", "MAT", "Memoization", "PlanarLarvae", "PyCall", "Random", "StaticArrays", "Statistics"]
+git-tree-sha1 = "576ffec2899eb3cbfa123446de21bef19ac225af"
+repo-rev = "dev"
+repo-url = "https://gitlab.pasteur.fr/nyx/TaggingBackends"
+uuid = "e551f703-3b82-4335-b341-d497b48d519b"
+version = "0.4.0"
+
+[[deps.Tar]]
+deps = ["ArgTools", "SHA"]
+uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
+
+[[deps.Test]]
+deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
+uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+
+[[deps.TranscodingStreams]]
+deps = ["Random", "Test"]
+git-tree-sha1 = "216b95ea110b5972db65aa90f88d8d89dcb8851c"
+uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
+version = "0.9.6"
+
+[[deps.UUIDs]]
+deps = ["Random", "SHA"]
+uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+
+[[deps.Unicode]]
+uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+
+[[deps.VersionParsing]]
+git-tree-sha1 = "58d6e80b4ee071f5efd07fda82cb9fbe17200868"
+uuid = "81def892-9a0e-5fdd-b105-ffc91e053289"
+version = "1.3.0"
+
+[[deps.Zlib_jll]]
+deps = ["Libdl"]
+uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
+
+[[deps.libblastrampoline_jll]]
+deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
+uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
+
+[[deps.nghttp2_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
+
+[[deps.p7zip_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
diff --git a/Project.toml b/Project.toml
new file mode 100644
index 0000000000000000000000000000000000000000..ff9c690c1f8af32b117aefdeb8520aebc5e8edd3
--- /dev/null
+++ b/Project.toml
@@ -0,0 +1,2 @@
+[deps]
+TaggingBackends = "e551f703-3b82-4335-b341-d497b48d519b"
diff --git a/models/autoencoder_config.json b/models/autoencoder_config.json
new file mode 100644
index 0000000000000000000000000000000000000000..47f16364b72518690f5f7709e31e47cca78a3ba9
--- /dev/null
+++ b/models/autoencoder_config.json
@@ -0,0 +1,105 @@
+{
+  "project_dir": "models",
+  "seed": 100,
+  "exp_name": "20220517",
+  "data_dir": "structured-temporal-convolution/20220425/larva_dataset_2022_04_25_20_20_100000.hdf5",
+  "raw_data_dir": "structured-temporal-convolution/larva_dataset/t5_t15_point_dynamics",
+  "log_dir": "models",
+  "exp_folder": "models",
+  "config": "models/autoencoder_config.json",
+  "num_workers": 4,
+  "n_features": 10,
+  "len_traj": 20,
+  "len_pred": 20,
+  "dim_latent": 10,
+  "activation": "relu",
+  "enc_filters": [
+    128,
+    64,
+    32,
+    32,
+    32,
+    16
+  ],
+  "dec_filters": [
+    128,
+    64,
+    32,
+    32,
+    32,
+    16
+  ],
+  "enc_kernel": [
+    [
+      5,
+      1
+    ],
+    [
+      1,
+      20
+    ],
+    [
+      5,
+      1
+    ],
+    [
+      1,
+      20
+    ],
+    [
+      5,
+      1
+    ],
+    [
+      1,
+      20
+    ]
+  ],
+  "dec_kernel": [
+    [
+      1,
+      20
+    ],
+    [
+      5,
+      1
+    ],
+    [
+      1,
+      20
+    ],
+    [
+      5,
+      1
+    ],
+    [
+      1,
+      20
+    ],
+    [
+      5,
+      1
+    ]
+  ],
+  "bias": false,
+  "enc_depth": 4,
+  "dec_depth": 4,
+  "init": "kaiming",
+  "n_clusters": 2,
+  "dim_reduc": "UMAP",
+  "optim_iter": 1000,
+  "pseudo_epoch": 100,
+  "batch_size": 128,
+  "lr": 0.005,
+  "loss": "MSE",
+  "cluster_penalty": null,
+  "cluster_penalty_coef": 0.0,
+  "length_penalty_coef": 0.0,
+  "grad_clip": 100.0,
+  "optimizer": "adam",
+  "target": [
+    "past",
+    "present",
+    "future"
+  ]
+}
diff --git a/models/best_validated_encoder.pt b/models/best_validated_encoder.pt
new file mode 100644
index 0000000000000000000000000000000000000000..ebc06f6e9b1c60ad2f858aabf903157a7b79a354
Binary files /dev/null and b/models/best_validated_encoder.pt differ
diff --git a/pyproject.toml b/pyproject.toml
index 356de2b5f9b604a07ddcd5fd2fdc9fedd87c2967..dc095dae7e2220542cc46dc7febdee0d26de3639 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,12 +10,11 @@ packages = [
 
 [tool.poetry.dependencies]
 python = "^3.8,<3.11"
-taggingbackends = {git = "https://gitlab.pasteur.fr/nyx/TaggingBackends", rev = "main"}
 structured-temporal-convolution = {git = "git@gitlab.pasteur.fr:les-larves/structured-temporal-convolution.git", branch="light-stable-for-tagging"}
 torch = "^1.11.0"
 numpy = "^1.19.3"
-
-[tool.poetry.dev-dependencies]
+protobuf = "3.9.2"
+taggingbackends = {git = "https://gitlab.pasteur.fr/nyx/TaggingBackends", rev = "dev"}
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/src/maggotuba/models/denselayer.py b/src/maggotuba/models/denselayer.py
index 7196bcd3cbe76fc077ddbf176b6822ee8fd3a9c2..815e0e6fb7d06ef7ebba5d8441d46878bebd10e0 100644
--- a/src/maggotuba/models/denselayer.py
+++ b/src/maggotuba/models/denselayer.py
@@ -4,12 +4,9 @@ import pathlib
 import numpy as np
 import torch
 import torch.nn as nn
-from behavior_model.models.neural_nets import Encoder
-from behavior_model.models.model import Trainer
+from behavior_model.models.neural_nets import Encoder, device
 import behavior_model.data.utils as data_utils
-from behavior_model.data.enums import Label
 
-device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
 
 class SupervisedMaggot(nn.Module):
     def __init__(self, n_latent_features, n_behaviors, enc_config, enc_path,
@@ -27,6 +24,7 @@ class SupervisedMaggot(nn.Module):
             nn.init.zeros_(self.clf.bias)
 
     def forward(self, x):
+        #x = torch.flip(x, (2,))
         return self.clf(self.encoder(x))
 
 """
@@ -36,6 +34,10 @@ for the decoder, and (re-)trains the entire model.
 Attribute `config` refers to MaggotUBA autoencoder.
 Attribute `clf_config` refers to the supervised model, both the retrained
 encoder and the trained classifier.
+
+Several data preprocessing steps are included for use in prediction mode.
+Training the model instead relies on the readily-preprocessed data of a
+*larva_dataset hdf5* file.
 """
 class DenseLayer:
     def __init__(self,
@@ -54,6 +56,11 @@ class DenseLayer:
         self.average_body_length = average_body_length
         self.device = device
 
+    """
+    dict: JSON-deserialized parameters for the pretrained autoencoder.
+          Warning: not all keys are properly adjusted after the original file
+          is repurposed for `SupervisedMaggot`.
+    """
     @property
     def config(self):
         if self._config is None:
@@ -128,8 +135,11 @@ class DenseLayer:
         if data.shape[0] == 1:
             return data
         else:
-            ind = np.r_[np.zeros(winlen // 2, dtype=int), np.arange(data.shape[0]), (data.shape[1]-1) *
-                    np.ones(winlen // 2 - 1, dtype=int)]
+            ind = np.r_[
+                    np.zeros(winlen // 2, dtype=int),
+                    np.arange(data.shape[0]),
+                    (data.shape[1]-1) * np.ones(winlen // 2 - 1, dtype=int),
+                    ]
             return data[ind]
 
     def body_length(self, data):
@@ -137,77 +147,115 @@ class DenseLayer:
         dy = np.diff(data[:,1::2], axis=1)
         return np.sum(np.sqrt(dx*dx + dy*dy), axis=1)
 
-    def preprocess(self, data):
+    def maggotuba_preprocess(self, data):
         # normalize length
         if self.average_body_length:
             data = data / self.average_body_length
         # revert the spines
         data = data[:,[8,9,6,7,4,5,2,3,0,1]]
         ws = []
-        for coords in self.window(data):
+        for w in self.window(data):
             # rotate
-            matrix = data_utils.compute_rotation_matrix(coords)
-            coords = np.stack([coords[:,::2], coords[:,1::2]], axis=-1)
-            coords = np.einsum('ji,tpi->tpj', matrix, coords)
-            coords = coords.reshape(coords.shape[0],-1)
-            w = coords
+            matrix = data_utils.compute_rotation_matrix(w)
+            w = np.stack([w[:,::2], w[:,1::2]], axis=-1)
+            w = np.einsum('ji,tpi->tpj', matrix, w)
+            w = w.reshape(w.shape[0],-1)
             # center coordinates
             wc = np.mean(w[:,4:6], axis=0, keepdims=True)
-            w -= np.tile(wc, 5).reshape(1, -1)
-            # select coordinates columns
-            # (nothing to do)
+            w = w - np.tile(wc, (1, 5))
             # reshape
             w = data_utils.reshape(w)
             ws.append(w)
         if ws:
             return self.pad(np.stack(ws))
 
+    def preprocess(self, data):
+        ws = []
+        for w in self.window(data):
+            # center coordinates
+            wc = np.mean(w[:,4:6], axis=0, keepdims=True)
+            w = w - np.tile(wc, (1, 5))
+            # rotate
+            v = np.mean(w[:,8:10] - w[:,0:2], axis=0)
+            v = v / np.sqrt(np.dot(v, v))
+            c, s = v / self.average_body_length # scale using the rotation matrix
+            rot = np.array([[ c, s],
+                            [-s, c]]) # clockwise rotation
+            w = np.einsum("ij,jkl", rot, np.reshape(w.T, (2, 5, -1), order='F'))
+            ws.append(w)
+        if ws:
+            return self.pad(np.stack(ws))[:,:,::-1,:] # swap head and tail
+
     def forward(self, x, train=False):
-        if not isinstance(x, torch.Tensor):
-            x = torch.from_numpy(x.astype(np.float32))
-        y = self.model(x.to(self.device))
         if train:
-            return y
+            return self.model(x)
         else:
+            if not isinstance(x, torch.Tensor):
+                x = torch.from_numpy(x.astype(np.float32))
+            y = self.model(x.to(self.device))
             return y.cpu().numpy()
 
-    def train(self, all_spines=None, tags=None):
-        if all_spines is None or tags is None:
-            trainer = Trainer(**self.config)
-        else:
-            data = self.preprocess(all_spines)
-            if data is None:
-                return
-            expected = tags # TODO
+    def train(self, dataset):
+        try:
+            dataset.batch_size
+        except AttributeError:
+            dataset.batch_size = self.config["batch_size"]
+        past_future_len = self.config["len_pred"]
+        present_len = self.config["len_traj"]
+        assert past_future_len == 20 and present_len == 20
+        dataset._mask = slice(past_future_len, past_future_len + present_len)
         #
         enc_path = "best_validated_encoder.pt"
         if self.prepend_log_dir:
-            enc_path = os.path.join(config["log_dir"], enc_path)
+            enc_path = os.path.join(self.config["log_dir"], enc_path)
+        if isinstance(dataset.labels[0], str):
+            self.labels = dataset.labels
+        else:
+            self.labels = [s.decode() for s in dataset.labels]
         self.model = model = SupervisedMaggot(
                 n_latent_features=self.config["dim_latent"],
                 n_behaviors=self.n_behaviors,
                 enc_config=self.config,
                 enc_path=enc_path,
                 )
+        model.train() # this only sets the model in training mode (enables gradients)
+        model.to(self.device)
         criterion = nn.CrossEntropyLoss()
+        # pre-train the classifier with static encoder weights
+        optimizer = torch.optim.Adam(model.clf.parameters())
+        print("pre-training the classifier...")
+        for step in range(self.config["optim_iter"] // 2):
+            optimizer.zero_grad()
+            data, expected = self.draw(dataset)
+            predicted = self.forward(data, train=True)
+            loss = criterion(predicted, expected)
+            #print("pre-train", torch.mean(loss).detach().numpy())
+            loss.backward()
+            optimizer.step()
+        # fine-tune both the encoder and the classifier
         optimizer = torch.optim.Adam(model.parameters())
-        model.train()
-        model.to(self.device)
-        for step in range(trainer.optim_iter):
+        print("fine-tuning the encoder and classifier...")
+        for step in range(self.config["optim_iter"] // 2):
             optimizer.zero_grad()
-            if all_spines is None:
-                batch = trainer.data.sample("train")
-                data = batch.present
-                expected = batch.label["present_label"].numpy()
-                expected = torch.tensor(expected, dtype=torch.long)
-            expected = expected.to(self.device)
+            data, expected = self.draw(dataset)
             predicted = self.forward(data, train=True)
             loss = criterion(predicted, expected)
+            #print("fine-tune", torch.mean(loss).detach().numpy())
             loss.backward()
             optimizer.step()
         #
         return self
 
+    def draw(self, dataset):
+        data, expected = dataset.getsample()
+        if isinstance(data, list):
+            data = torch.stack(data)
+        data = data.to(torch.float32).to(self.device)
+        if isinstance(expected, list):
+            expected = torch.stack(expected)
+        expected = expected.to(torch.long).to(self.device)
+        return data, expected
+
     @torch.no_grad()
     def predict(self, all_spines):
         data = self.preprocess(all_spines)
@@ -224,8 +272,11 @@ class DenseLayer:
         model.to(self.device)
         output = self.forward(data)
         label_ids = np.argmax(output, axis=1)
-        labelset = {float(symbol.value): symbol.name.lower() for symbol in Label}
-        labels = [labelset[label] for label in label_ids]
+        try:
+            self.labels
+        except AttributeError:
+            self.labels = self.clf_config["behavior_labels"]
+        labels = [self.labels[label] for label in label_ids]
         return labels
 
     def save(self, config_path="clf_config.json", config_only=False):
@@ -240,6 +291,7 @@ class DenseLayer:
                 enc_path=self.enc_path,
                 clf_path=self.clf_path,
                 n_behaviors=self.n_behaviors,
+                behavior_labels=self.labels,
                 # additional information (not reused):
                 clf_depth=0,
                 bias=True,
diff --git a/src/maggotuba/models/predict_model.py b/src/maggotuba/models/predict_model.py
index 4299dd2f381df7a3d2ccd758d6608f406bcf9027..afcae80017c4cfcdb3ab11ade54d4ef34b114920 100644
--- a/src/maggotuba/models/predict_model.py
+++ b/src/maggotuba/models/predict_model.py
@@ -3,8 +3,7 @@ from taggingbackends.data.chore import load_spine
 import taggingbackends.data.fimtrack as fimtrack
 from taggingbackends.data.labels import Labels
 from taggingbackends.features.skeleton import get_5point_spines
-#from randomforest import RandomForest as Clf
-from denselayer import DenseLayer as Clf
+from denselayer import DenseLayer
 import numpy as np
 import json
 
@@ -66,10 +65,10 @@ def predict_model(backend):
         config_file = [file for file in model_files if file.name.endswith("config.json")]
         if 1 < len(config_file):
             config_file = [file for file in config_file if file.name.endswith("clf_config.json")]
-        model = Clf(config_file[-1])
+        model = DenseLayer(config_file[-1])
         # assign labels
         if isinstance(data, dict):
-            ref_length = np.mean(np.concatenate([
+            ref_length = np.median(np.concatenate([
                 model.body_length(spines) for spines in data.values()
                 ]))
             model.average_body_length = ref_length
@@ -81,7 +80,7 @@ def predict_model(backend):
                 else:
                     labels[run, larva] = dict(zip(t[larva], predictions))
         else:
-            ref_length = model.body_length(data).mean()
+            ref_length = np.median(model.body_length(data))
             model.average_body_length = ref_length
             print(f"average body length: {ref_length}")
             for larva in np.unique(larvae):
@@ -92,11 +91,12 @@ def predict_model(backend):
                 else:
                     labels[run, larva] = dict(zip(t[mask], predictions))
         # save the predicted labels to file
-        labels.labelspec = {
-                "names": ["run", "bend", "stop", "hunch", "back", "roll"],
-                "colors": ["#000000", "#ff0000", "#00ff00", "#0000ff",
-                    "#00ffff", "#ffff00"]
-                }
+        # labels.labelspec = {
+        #         "names": ["run", "bend", "stop", "hunch", "back", "roll"],
+        #         "colors": ["#000000", "#ff0000", "#00ff00", "#0000ff",
+        #             "#00ffff", "#ffff00"]
+        #         }
+        labels.labelspec = model.clf_config["behavior_labels"]
         labels.dump(backend.processed_data_dir() / "predicted.labels")
 
 
diff --git a/src/maggotuba/models/train_model.py b/src/maggotuba/models/train_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a5d3a578a78d2912ec365b5a5758856c7cf4d8d
--- /dev/null
+++ b/src/maggotuba/models/train_model.py
@@ -0,0 +1,50 @@
+from taggingbackends.data.labels import Labels
+from taggingbackends.data.dataset import LarvaDataset
+from denselayer import DenseLayer, device
+import numpy as np
+import json
+import torch
+import os
+
+def train_model(backend):
+    # make_dataset generated or moved the larva_dataset file into data/interim/{instance}/
+    larva_dataset_file = backend.list_interim_files()
+    assert len(larva_dataset_file) == 1
+    dataset = LarvaDataset(larva_dataset_file[0], torch.Generator(device).manual_seed(42))
+    nlabels = len(dataset.labels)
+    assert 0 < nlabels
+    # copy the pretrained model into the model instance directory
+    pretrained_autoencoder_dir = backend.model_dir() / ".."
+    config_file = None
+    for file in pretrained_autoencoder_dir.iterdir():
+        if not file.is_file():
+            continue
+        dst = backend.model_dir() / file.name
+        if file.name.endswith("config.json"):
+            with open(str(file)) as f:
+                config = json.load(f)
+            dir = backend.model_dir().relative_to(backend.project_dir)
+            config["log_dir"] = str(dir)
+            # optional updates?
+            config["project_dir"] = config["exp_folder"] = str(dir)
+            config["exp_name"] = backend.model_instance
+            config["config"] = str(dir / os.path.basename(config["config"]))
+            with open(str(dst), "w") as f:
+                json.dump(config, f, indent=2)
+            assert config_file is None
+            config_file = dst
+        else:
+            with open(str(file), "rb") as i:
+                with open(str(dst), "wb") as o:
+                    o.write(i.read())
+    # load the pretrained model
+    model = DenseLayer(autoencoder_config=config_file, n_behaviors=nlabels)
+    # fine-tune and save the model
+    model.train(dataset)
+    model.save()
+
+
+from taggingbackends.main import main
+
+if __name__ == "__main__":
+    main(train_model)