@@ -2,15 +2,15 @@
00000010: 7265 7175 6972 6573 203d 205b 2273 6574 requires = ["set
00000020: 7570 746f 6f6c 7322 2c20 2277 6865 656c uptools", "wheel
00000030: 225d 0d0a 6275 696c 642d 6261 636b 656e "]..build-backen
00000040: 6420 3d20 2273 6574 7570 746f 6f6c 732e d = "setuptools.
00000050: 6275 696c 645f 6d65 7461 220d 0a0d 0a5b build_meta"....[
00000060: 7072 6f6a 6563 745d 0d0a 6e61 6d65 203d project]..name =
00000070: 2022 6661 726e 220d 0a76 6572 7369 6f6e "farn"..version
-00000080: 203d 2022 302e 332e 3622 0d0a 6465 7363 = "0.3.6"..desc
+00000080: 203d 2022 302e 332e 3722 0d0a 6465 7363 = "0.3.7"..desc
00000090: 7269 7074 696f 6e20 3d20 2250 7974 686f ription = "Pytho
000000a0: 6e20 7061 636b 6167 6520 746f 2067 656e n package to gen
000000b0: 6572 6174 6520 616e 206e 2d64 696d 656e erate an n-dimen
000000c0: 7369 6f6e 616c 2063 6173 6520 666f 6c64 sional case fold
000000d0: 6572 2073 7472 7563 7475 7265 2061 7070 er structure app
000000e0: 6c79 696e 6720 6c69 6e65 6172 2061 6e64 lying linear and
000000f0: 2073 7061 7469 616c 2073 616d 706c 696e spatial samplin
@@ -74,271 +74,269 @@
00000490: 6669 632f 456e 6769 6e65 6572 696e 6722 fic/Engineering"
000004a0: 2c0d 0a20 2020 2022 546f 7069 6320 3a3a ,.. "Topic ::
000004b0: 2053 6f66 7477 6172 6520 4465 7665 6c6f Software Develo
000004c0: 706d 656e 7420 3a3a 204c 6962 7261 7269 pment :: Librari
000004d0: 6573 203a 3a20 5079 7468 6f6e 204d 6f64 es :: Python Mod
000004e0: 756c 6573 222c 0d0a 5d0d 0a64 6570 656e ules",..]..depen
000004f0: 6465 6e63 6965 7320 3d20 5b0d 0a20 2020 dencies = [..
-00000500: 2022 6c78 6d6c 3e3d 352e 3122 2c0d 0a20 "lxml>=5.1",..
-00000510: 2020 2022 6e75 6d70 793e 3d31 2e32 3622 "numpy>=1.26"
-00000520: 2c0d 0a20 2020 2022 7363 6970 793e 3d31 ,.. "scipy>=1
-00000530: 2e31 3222 2c0d 0a20 2020 2022 7061 6e64 .12",.. "pand
-00000540: 6173 3e3d 322e 3222 2c0d 0a20 2020 2022 as>=2.2",.. "
-00000550: 6d61 7470 6c6f 746c 6962 3e3d 332e 3822 matplotlib>=3.8"
-00000560: 2c0d 0a20 2020 2022 5069 6c6c 6f77 3e3d ,.. "Pillow>=
-00000570: 3130 2e32 222c 0d0a 2020 2020 2270 7944 10.2",.. "pyD
-00000580: 4f45 333e 3d31 2e30 222c 0d0a 2020 2020 OE3>=1.0",..
-00000590: 2270 7375 7469 6c3e 3d35 2e39 222c 0d0a "psutil>=5.9",..
-000005a0: 2020 2020 2268 696c 6265 7274 6375 7276 "hilbertcurv
-000005b0: 653e 3d32 2e30 2e35 222c 0d0a 2020 2020 e>=2.0.5",..
-000005c0: 2264 6963 7449 4f3e 3d30 2e33 2e33 222c "dictIO>=0.3.3",
-000005d0: 0d0a 2020 2020 226f 7370 783e 3d30 2e32 .. "ospx>=0.2
-000005e0: 2e31 3322 2c0d 0a5d 0d0a 0d0a 5b70 726f .13",..]....[pro
-000005f0: 6a65 6374 2e75 726c 735d 0d0a 486f 6d65 ject.urls]..Home
-00000600: 7061 6765 203d 2022 6874 7470 733a 2f2f page = "https://
-00000610: 6769 7468 7562 2e63 6f6d 2f64 6e76 2d6f github.com/dnv-o
-00000620: 7065 6e73 6f75 7263 652f 6661 726e 220d pensource/farn".
-00000630: 0a44 6f63 756d 656e 7461 7469 6f6e 203d .Documentation =
-00000640: 2022 6874 7470 733a 2f2f 646e 762d 6f70 "https://dnv-op
-00000650: 656e 736f 7572 6365 2e67 6974 6875 622e ensource.github.
-00000660: 696f 2f66 6172 6e2f 5245 4144 4d45 2e68 io/farn/README.h
-00000670: 746d 6c22 0d0a 5265 706f 7369 746f 7279 tml"..Repository
-00000680: 203d 2022 6874 7470 733a 2f2f 6769 7468 = "https://gith
-00000690: 7562 2e63 6f6d 2f64 6e76 2d6f 7065 6e73 ub.com/dnv-opens
-000006a0: 6f75 7263 652f 6661 726e 2e67 6974 220d ource/farn.git".
-000006b0: 0a49 7373 7565 7320 3d20 2268 7474 7073 .Issues = "https
-000006c0: 3a2f 2f67 6974 6875 622e 636f 6d2f 646e ://github.com/dn
-000006d0: 762d 6f70 656e 736f 7572 6365 2f66 6172 v-opensource/far
-000006e0: 6e2f 6973 7375 6573 220d 0a43 6861 6e67 n/issues"..Chang
-000006f0: 656c 6f67 203d 2022 6874 7470 733a 2f2f elog = "https://
-00000700: 6769 7468 7562 2e63 6f6d 2f64 6e76 2d6f github.com/dnv-o
-00000710: 7065 6e73 6f75 7263 652f 6661 726e 2f62 pensource/farn/b
-00000720: 6c6f 622f 6d61 696e 2f43 4841 4e47 454c lob/main/CHANGEL
-00000730: 4f47 2e6d 6422 0d0a 0d0a 5b70 726f 6a65 OG.md"....[proje
-00000740: 6374 2e73 6372 6970 7473 5d0d 0a66 6172 ct.scripts]..far
-00000750: 6e20 3d20 2266 6172 6e2e 636c 692e 6661 n = "farn.cli.fa
-00000760: 726e 3a6d 6169 6e22 0d0a 6261 7463 6850 rn:main"..batchP
-00000770: 726f 6365 7373 203d 2022 6661 726e 2e72 rocess = "farn.r
-00000780: 756e 2e63 6c69 2e62 6174 6368 5072 6f63 un.cli.batchProc
-00000790: 6573 733a 6d61 696e 220d 0a0d 0a5b 746f ess:main"....[to
-000007a0: 6f6c 2e73 6574 7570 746f 6f6c 732e 7061 ol.setuptools.pa
-000007b0: 636b 6167 6573 2e66 696e 645d 0d0a 7768 ckages.find]..wh
-000007c0: 6572 6520 3d20 5b22 7372 6322 5d0d 0a65 ere = ["src"]..e
-000007d0: 7863 6c75 6465 203d 205b 2274 6573 742a xclude = ["test*
-000007e0: 225d 0d0a 0d0a 5b74 6f6f 6c2e 626c 6163 "]....[tool.blac
-000007f0: 6b5d 0d0a 6c69 6e65 2d6c 656e 6774 6820 k]..line-length
-00000800: 3d20 3132 300d 0a74 6172 6765 742d 7665 = 120..target-ve
-00000810: 7273 696f 6e20 3d20 5b22 7079 3339 222c rsion = ["py39",
-00000820: 2022 7079 3331 3022 2c20 2270 7933 3131 "py310", "py311
-00000830: 222c 2022 7079 3331 3222 5d0d 0a0d 0a5b ", "py312"]....[
-00000840: 746f 6f6c 2e72 7566 665d 0d0a 6578 636c tool.ruff]..excl
-00000850: 7564 6520 3d20 5b0d 0a20 2020 2022 2e67 ude = [.. ".g
-00000860: 6974 222c 0d0a 2020 2020 222e 7665 6e76 it",.. ".venv
-00000870: 222c 0d0a 2020 2020 222e 746f 7822 2c0d ",.. ".tox",.
-00000880: 0a20 2020 2022 6275 696c 6422 2c0d 0a20 . "build",..
-00000890: 2020 2022 6469 7374 222c 0d0a 2020 2020 "dist",..
-000008a0: 225f 5f70 7963 6163 6865 5f5f 222c 0d0a "__pycache__",..
-000008b0: 2020 2020 222e 2f64 6f63 732f 736f 7572 "./docs/sour
-000008c0: 6365 2f63 6f6e 662e 7079 222c 0d0a 5d0d ce/conf.py",..].
-000008d0: 0a73 7263 203d 205b 2273 7263 225d 0d0a .src = ["src"]..
-000008e0: 6c69 6e65 2d6c 656e 6774 6820 3d20 3132 line-length = 12
-000008f0: 300d 0a74 6172 6765 742d 7665 7273 696f 0..target-versio
-00000900: 6e20 3d20 2270 7933 3922 0d0a 0d0a 5b74 n = "py39"....[t
-00000910: 6f6f 6c2e 7275 6666 2e6c 696e 745d 0d0a ool.ruff.lint]..
-00000920: 6967 6e6f 7265 203d 205b 0d0a 2020 2020 ignore = [..
-00000930: 2245 3530 3122 2c20 2023 204c 696e 6520 "E501", # Line
-00000940: 6c65 6e67 7468 2074 6f6f 206c 6f6e 670d length too long.
-00000950: 0a20 2020 2022 4431 3030 222c 2020 2320 . "D100", #
-00000960: 4d69 7373 696e 6720 646f 6373 7472 696e Missing docstrin
-00000970: 6720 696e 2070 7562 6c69 6320 6d6f 6475 g in public modu
-00000980: 6c65 0d0a 2020 2020 2244 3130 3422 2c20 le.. "D104",
-00000990: 2023 204d 6973 7369 6e67 2064 6f63 7374 # Missing docst
-000009a0: 7269 6e67 2069 6e20 7075 626c 6963 2070 ring in public p
-000009b0: 6163 6b61 6765 0d0a 2020 2020 2244 3130 ackage.. "D10
-000009c0: 3522 2c20 2023 204d 6973 7369 6e67 2064 5", # Missing d
-000009d0: 6f63 7374 7269 6e67 2069 6e20 6d61 6769 ocstring in magi
-000009e0: 6320 6d65 7468 6f64 0d0a 2020 2020 2244 c method.. "D
-000009f0: 3130 3722 2c20 2023 204d 6973 7369 6e67 107", # Missing
-00000a00: 2064 6f63 7374 7269 6e67 2069 6e20 5f5f docstring in __
-00000a10: 696e 6974 5f5f 0d0a 2020 2020 2244 3230 init__.. "D20
-00000a20: 3222 2c20 2023 204e 6f20 626c 616e 6b20 2", # No blank
-00000a30: 6c69 6e65 7320 616c 6c6f 7765 6420 6166 lines allowed af
-00000a40: 7465 7220 6675 6e63 7469 6f6e 2064 6f63 ter function doc
-00000a50: 7374 7269 6e67 0d0a 2020 2020 2244 3230 string.. "D20
-00000a60: 3322 2c20 2023 2031 2062 6c61 6e6b 206c 3", # 1 blank l
-00000a70: 696e 6520 7265 7175 6972 6564 2062 6566 ine required bef
-00000a80: 6f72 6520 636c 6173 7320 646f 6373 7472 ore class docstr
-00000a90: 696e 670d 0a20 2020 2022 4432 3035 222c ing.. "D205",
-00000aa0: 2020 2320 3120 626c 616e 6b20 6c69 6e65 # 1 blank line
-00000ab0: 2072 6571 7569 7265 6420 6265 7477 6565 required betwee
-00000ac0: 6e20 7375 6d6d 6172 7920 6c69 6e65 2061 n summary line a
-00000ad0: 6e64 2064 6573 6372 6970 7469 6f6e 0d0a nd description..
-00000ae0: 2020 2020 2244 3231 3222 2c20 2023 204d "D212", # M
-00000af0: 756c 7469 2d6c 696e 6520 646f 6373 7472 ulti-line docstr
-00000b00: 696e 6720 7375 6d6d 6172 7920 7368 6f75 ing summary shou
-00000b10: 6c64 2073 7461 7274 2061 7420 7468 6520 ld start at the
-00000b20: 6669 7273 7420 6c69 6e65 0d0a 2020 2020 first line..
-00000b30: 2244 3231 3322 2c20 2023 204d 756c 7469 "D213", # Multi
-00000b40: 2d6c 696e 6520 646f 6373 7472 696e 6720 -line docstring
-00000b50: 7375 6d6d 6172 7920 7368 6f75 6c64 2073 summary should s
-00000b60: 7461 7274 2061 7420 7468 6520 7365 636f tart at the seco
-00000b70: 6e64 206c 696e 650d 0a20 2020 2023 2022 nd line.. # "
-00000b80: 4e38 3032 222c 2020 2320 4675 6e63 7469 N802", # Functi
-00000b90: 6f6e 206e 616d 6520 7368 6f75 6c64 2062 on name should b
-00000ba0: 6520 6c6f 7765 7263 6173 6520 2028 756e e lowercase (un
-00000bb0: 636f 6d6d 656e 7420 6966 2079 6f75 2077 comment if you w
-00000bc0: 616e 7420 746f 2061 6c6c 6f77 2055 7070 ant to allow Upp
-00000bd0: 6572 6361 7365 2066 756e 6374 696f 6e20 ercase function
-00000be0: 6e61 6d65 7329 0d0a 2020 2020 2320 224e names).. # "N
-00000bf0: 3830 3322 2c20 2023 2041 7267 756d 656e 803", # Argumen
-00000c00: 7420 6e61 6d65 2073 686f 756c 6420 6265 t name should be
-00000c10: 206c 6f77 6572 6361 7365 2020 2875 6e63 lowercase (unc
-00000c20: 6f6d 6d65 6e74 2069 6620 796f 7520 7761 omment if you wa
-00000c30: 6e74 2074 6f20 616c 6c6f 7720 5570 7065 nt to allow Uppe
-00000c40: 7263 6173 6520 6172 6775 6d65 6e74 206e rcase argument n
-00000c50: 616d 6573 290d 0a20 2020 2022 4e38 3036 ames).. "N806
-00000c60: 222c 2020 2320 5661 7269 6162 6c65 2069 ", # Variable i
-00000c70: 6e20 6675 6e63 7469 6f6e 2073 686f 756c n function shoul
-00000c80: 6420 6265 206c 6f77 6572 6361 7365 2020 d be lowercase
-00000c90: 2875 6e63 6f6d 6d65 6e74 2069 6620 796f (uncomment if yo
-00000ca0: 7520 7761 6e74 2074 6f20 616c 6c6f 7720 u want to allow
-00000cb0: 5570 7065 7263 6173 6520 7661 7269 6162 Uppercase variab
-00000cc0: 6c65 206e 616d 6573 2069 6e20 6675 6e63 le names in func
-00000cd0: 7469 6f6e 7329 0d0a 2020 2020 2320 224e tions).. # "N
-00000ce0: 3831 3522 2c20 2023 2056 6172 6961 626c 815", # Variabl
-00000cf0: 6520 696e 2063 6c61 7373 2073 636f 7065 e in class scope
-00000d00: 2073 686f 756c 6420 6e6f 7420 6265 206d should not be m
-00000d10: 6978 6564 4361 7365 2020 2875 6e63 6f6d ixedCase (uncom
-00000d20: 6d65 6e74 2069 6620 796f 7520 7761 6e74 ment if you want
-00000d30: 2074 6f20 616c 6c6f 7720 6d69 7865 6443 to allow mixedC
-00000d40: 6173 6520 7661 7269 6162 6c65 206e 616d ase variable nam
-00000d50: 6573 2069 6e20 636c 6173 7320 7363 6f70 es in class scop
-00000d60: 6529 0d0a 2020 2020 2320 224e 3831 3622 e).. # "N816"
-00000d70: 2c20 2023 2056 6172 6961 626c 6520 696e , # Variable in
-00000d80: 2067 6c6f 6261 6c20 7363 6f70 6520 7368 global scope sh
-00000d90: 6f75 6c64 206e 6f74 2062 6520 6d69 7865 ould not be mixe
-00000da0: 6443 6173 6520 2028 756e 636f 6d6d 656e dCase (uncommen
-00000db0: 7420 6966 2079 6f75 2077 616e 7420 746f t if you want to
-00000dc0: 2061 6c6c 6f77 206d 6978 6564 4361 7365 allow mixedCase
-00000dd0: 2076 6172 6961 626c 6520 6e61 6d65 7320 variable names
-00000de0: 696e 2067 6c6f 6261 6c20 7363 6f70 6529 in global scope)
-00000df0: 0d0a 2020 2020 224e 3939 3922 2c20 2023 .. "N999", #
-00000e00: 2049 6e76 616c 6964 206d 6f64 756c 6520 Invalid module
-00000e10: 6e61 6d65 0d0a 2020 2020 5d0d 0a73 656c name.. ]..sel
-00000e20: 6563 7420 3d20 5b0d 0a20 2020 2022 4522 ect = [.. "E"
-00000e30: 2c0d 0a20 2020 2022 4422 2c0d 0a20 2020 ,.. "D",..
-00000e40: 2022 4622 2c0d 0a20 2020 2022 4e22 2c0d "F",.. "N",.
-00000e50: 0a20 2020 2022 5722 2c0d 0a20 2020 2022 . "W",.. "
-00000e60: 4922 2c0d 0a20 2020 2022 4222 2c0d 0a5d I",.. "B",..]
-00000e70: 0d0a 0d0a 0d0a 5b74 6f6f 6c2e 7275 6666 ......[tool.ruff
-00000e80: 2e6c 696e 742e 7065 7038 2d6e 616d 696e .lint.pep8-namin
-00000e90: 675d 0d0a 6967 6e6f 7265 2d6e 616d 6573 g]..ignore-names
-00000ea0: 203d 205b 0d0a 2020 2020 2274 6573 745f = [.. "test_
-00000eb0: 2a22 2c0d 0a20 2020 2022 7365 7455 7022 *",.. "setUp"
-00000ec0: 2c0d 0a20 2020 2022 7465 6172 446f 776e ,.. "tearDown
-00000ed0: 222c 0d0a 5d0d 0a0d 0a5b 746f 6f6c 2e72 ",..]....[tool.r
-00000ee0: 7566 662e 6c69 6e74 2e70 7964 6f63 7374 uff.lint.pydocst
-00000ef0: 796c 655d 0d0a 636f 6e76 656e 7469 6f6e yle]..convention
-00000f00: 203d 2022 6e75 6d70 7922 0d0a 0d0a 5b74 = "numpy"....[t
-00000f10: 6f6f 6c2e 7275 6666 2e6c 696e 742e 7065 ool.ruff.lint.pe
-00000f20: 722d 6669 6c65 2d69 676e 6f72 6573 5d0d r-file-ignores].
-00000f30: 0a22 5f5f 696e 6974 5f5f 2e70 7922 203d ."__init__.py" =
-00000f40: 205b 2249 3030 3122 5d0d 0a22 2e2f 7465 ["I001"].."./te
-00000f50: 7374 732f 2a22 203d 205b 2244 225d 0d0a sts/*" = ["D"]..
-00000f60: 0d0a 5b74 6f6f 6c2e 7079 7269 6768 745d ..[tool.pyright]
-00000f70: 0d0a 6578 636c 7564 6520 3d20 5b0d 0a20 ..exclude = [..
-00000f80: 2020 2022 2e67 6974 222c 0d0a 2020 2020 ".git",..
-00000f90: 222e 7665 6e76 222c 0d0a 2020 2020 222e ".venv",.. ".
-00000fa0: 746f 7822 2c0d 0a20 2020 2022 6275 696c tox",.. "buil
-00000fb0: 6422 2c0d 0a20 2020 2022 6469 7374 222c d",.. "dist",
-00000fc0: 0d0a 2020 2020 222a 2a2f 5f5f 7079 6361 .. "**/__pyca
-00000fd0: 6368 655f 5f22 2c0d 0a20 2020 2022 2e2f che__",.. "./
-00000fe0: 646f 6373 2f73 6f75 7263 652f 636f 6e66 docs/source/conf
-00000ff0: 2e70 7922 2c0d 0a20 2020 2022 2e2f 7665 .py",.. "./ve
-00001000: 6e76 222c 0d0a 5d0d 0a65 7874 7261 5061 nv",..]..extraPa
-00001010: 7468 7320 3d20 5b22 2e2f 7372 6322 5d0d ths = ["./src"].
-00001020: 0a74 7970 6543 6865 636b 696e 674d 6f64 .typeCheckingMod
-00001030: 6520 3d20 2262 6173 6963 220d 0a75 7365 e = "basic"..use
-00001040: 4c69 6272 6172 7943 6f64 6546 6f72 5479 LibraryCodeForTy
-00001050: 7065 7320 3d20 7472 7565 0d0a 7265 706f pes = true..repo
-00001060: 7274 4d69 7373 696e 6750 6172 616d 6574 rtMissingParamet
-00001070: 6572 5479 7065 203d 2022 6572 726f 7222 erType = "error"
-00001080: 0d0a 7265 706f 7274 556e 6b6e 6f77 6e50 ..reportUnknownP
-00001090: 6172 616d 6574 6572 5479 7065 203d 2022 arameterType = "
-000010a0: 7761 726e 696e 6722 0d0a 7265 706f 7274 warning"..report
-000010b0: 556e 6b6e 6f77 6e4d 656d 6265 7254 7970 UnknownMemberTyp
-000010c0: 6520 3d20 2277 6172 6e69 6e67 220d 0a72 e = "warning"..r
-000010d0: 6570 6f72 744d 6973 7369 6e67 5479 7065 eportMissingType
-000010e0: 4172 6775 6d65 6e74 203d 2022 6572 726f Argument = "erro
-000010f0: 7222 0d0a 7265 706f 7274 5072 6f70 6572 r"..reportProper
-00001100: 7479 5479 7065 4d69 736d 6174 6368 203d tyTypeMismatch =
-00001110: 2022 6572 726f 7222 0d0a 7265 706f 7274 "error"..report
-00001120: 4675 6e63 7469 6f6e 4d65 6d62 6572 4163 FunctionMemberAc
-00001130: 6365 7373 203d 2022 7761 726e 696e 6722 cess = "warning"
-00001140: 0d0a 7265 706f 7274 5072 6976 6174 6555 ..reportPrivateU
-00001150: 7361 6765 203d 2022 7761 726e 696e 6722 sage = "warning"
-00001160: 0d0a 7265 706f 7274 5479 7065 436f 6d6d ..reportTypeComm
-00001170: 656e 7455 7361 6765 203d 2022 7761 726e entUsage = "warn
-00001180: 696e 6722 0d0a 7265 706f 7274 496e 636f ing"..reportInco
-00001190: 6d70 6174 6962 6c65 4d65 7468 6f64 4f76 mpatibleMethodOv
-000011a0: 6572 7269 6465 203d 2022 7761 726e 696e erride = "warnin
-000011b0: 6722 0d0a 7265 706f 7274 496e 636f 6d70 g"..reportIncomp
-000011c0: 6174 6962 6c65 5661 7269 6162 6c65 4f76 atibleVariableOv
-000011d0: 6572 7269 6465 203d 2022 6572 726f 7222 erride = "error"
-000011e0: 0d0a 7265 706f 7274 496e 636f 6e73 6973 ..reportInconsis
-000011f0: 7465 6e74 436f 6e73 7472 7563 746f 7220 tentConstructor
-00001200: 3d20 2265 7272 6f72 220d 0a72 6570 6f72 = "error"..repor
-00001210: 744f 7665 726c 6170 7069 6e67 4f76 6572 tOverlappingOver
-00001220: 6c6f 6164 203d 2022 7761 726e 696e 6722 load = "warning"
-00001230: 0d0a 7265 706f 7274 556e 696e 6974 6961 ..reportUninitia
-00001240: 6c69 7a65 6449 6e73 7461 6e63 6556 6172 lizedInstanceVar
-00001250: 6961 626c 6520 3d20 2277 6172 6e69 6e67 iable = "warning
-00001260: 220d 0a72 6570 6f72 7443 616c 6c49 6e44 "..reportCallInD
-00001270: 6566 6175 6c74 496e 6974 6961 6c69 7a65 efaultInitialize
-00001280: 7220 3d20 2277 6172 6e69 6e67 220d 0a72 r = "warning"..r
-00001290: 6570 6f72 7455 6e6e 6563 6573 7361 7279 eportUnnecessary
-000012a0: 4973 496e 7374 616e 6365 203d 2022 696e IsInstance = "in
-000012b0: 666f 726d 6174 696f 6e22 0d0a 7265 706f formation"..repo
-000012c0: 7274 556e 6e65 6365 7373 6172 7943 6173 rtUnnecessaryCas
-000012d0: 7420 3d20 2277 6172 6e69 6e67 220d 0a72 t = "warning"..r
-000012e0: 6570 6f72 7455 6e6e 6563 6573 7361 7279 eportUnnecessary
-000012f0: 436f 6d70 6172 6973 6f6e 203d 2022 7761 Comparison = "wa
-00001300: 726e 696e 6722 0d0a 7265 706f 7274 556e rning"..reportUn
-00001310: 6e65 6365 7373 6172 7943 6f6e 7461 696e necessaryContain
-00001320: 7320 3d20 2277 6172 6e69 6e67 220d 0a72 s = "warning"..r
-00001330: 6570 6f72 7455 6e75 7365 6443 616c 6c52 eportUnusedCallR
-00001340: 6573 756c 7420 3d20 2277 6172 6e69 6e67 esult = "warning
-00001350: 220d 0a72 6570 6f72 7455 6e75 7365 6445 "..reportUnusedE
-00001360: 7870 7265 7373 696f 6e20 3d20 2277 6172 xpression = "war
-00001370: 6e69 6e67 220d 0a72 6570 6f72 744d 6174 ning"..reportMat
-00001380: 6368 4e6f 7445 7868 6175 7374 6976 6520 chNotExhaustive
-00001390: 3d20 2277 6172 6e69 6e67 220d 0a72 6570 = "warning"..rep
-000013a0: 6f72 7453 6861 646f 7765 6449 6d70 6f72 ortShadowedImpor
-000013b0: 7473 203d 2022 7761 726e 696e 6722 0d0a ts = "warning"..
-000013c0: 7265 706f 7274 556e 7479 7065 6446 756e reportUntypedFun
-000013d0: 6374 696f 6e44 6563 6f72 6174 6f72 203d ctionDecorator =
-000013e0: 2022 7761 726e 696e 6722 0d0a 7265 706f "warning"..repo
-000013f0: 7274 556e 7479 7065 6442 6173 6543 6c61 rtUntypedBaseCla
-00001400: 7373 203d 2022 6572 726f 7222 0d0a 7265 ss = "error"..re
-00001410: 706f 7274 556e 7479 7065 644e 616d 6564 portUntypedNamed
-00001420: 5475 706c 6520 3d20 2277 6172 6e69 6e67 Tuple = "warning
-00001430: 220d 0a23 2041 6374 6976 6174 6520 7468 "..# Activate th
-00001440: 6520 666f 6c6c 6f77 696e 6720 7275 6c65 e following rule
-00001450: 7320 6f6e 6c79 206c 6f63 616c 6c79 2061 s only locally a
-00001460: 6e64 2074 656d 706f 7261 7279 2c20 692e nd temporary, i.
-00001470: 652e 2066 6f72 2061 2051 4120 7365 7373 e. for a QA sess
-00001480: 696f 6e2e 0d0a 2320 2846 6f72 2073 6572 ion...# (For ser
-00001490: 7665 7220 7369 6465 2043 4920 7468 6579 ver side CI they
-000014a0: 2061 7265 2063 6f6e 7369 6465 7265 6420 are considered
-000014b0: 746f 6f20 7374 7269 6374 2e29 0d0a 2320 too strict.)..#
-000014c0: 7265 706f 7274 436f 6e73 7461 6e74 5265 reportConstantRe
-000014d0: 6465 6669 6e69 7469 6f6e 203d 2022 7761 definition = "wa
-000014e0: 726e 696e 6722 0d0a 2320 7265 706f 7274 rning"..# report
-000014f0: 556e 6e65 6365 7373 6172 7954 7970 6549 UnnecessaryTypeI
-00001500: 676e 6f72 6543 6f6d 6d65 6e74 203d 2022 gnoreComment = "
-00001510: 696e 666f 726d 6174 696f 6e22 0d0a 2320 information"..#
-00001520: 7265 706f 7274 496d 706f 7274 4379 636c reportImportCycl
-00001530: 6573 203d 2022 7761 726e 696e 6722 0d0a es = "warning"..
-00001540: 2320 7265 706f 7274 496d 706c 6963 6974 # reportImplicit
-00001550: 5374 7269 6e67 436f 6e63 6174 656e 6174 StringConcatenat
-00001560: 696f 6e20 3d20 2277 6172 6e69 6e67 220d ion = "warning".
-00001570: 0a .
+00000500: 2022 6c78 6d6c 3e3d 352e 3222 2c0d 0a20 "lxml>=5.2",..
+00000510: 2020 2022 6e75 6d70 793e 3d31 2e32 362c "numpy>=1.26,
+00000520: 3c32 2e30 222c 0d0a 2020 2020 2273 6369 <2.0",.. "sci
+00000530: 7079 3e3d 312e 3133 222c 0d0a 2020 2020 py>=1.13",..
+00000540: 2270 616e 6461 733e 3d32 2e32 222c 0d0a "pandas>=2.2",..
+00000550: 2020 2020 226d 6174 706c 6f74 6c69 623e "matplotlib>
+00000560: 3d33 2e39 222c 0d0a 2020 2020 2250 696c =3.9",.. "Pil
+00000570: 6c6f 773e 3d31 302e 3322 2c0d 0a20 2020 low>=10.3",..
+00000580: 2022 7079 444f 4533 3e3d 312e 3022 2c0d "pyDOE3>=1.0",.
+00000590: 0a20 2020 2022 7073 7574 696c 3e3d 352e . "psutil>=5.
+000005a0: 3922 2c0d 0a20 2020 2022 6869 6c62 6572 9",.. "hilber
+000005b0: 7463 7572 7665 3e3d 322e 302e 3522 2c0d tcurve>=2.0.5",.
+000005c0: 0a20 2020 2022 6469 6374 494f 3e3d 302e . "dictIO>=0.
+000005d0: 332e 3422 2c0d 0a20 2020 2022 6f73 7078 3.4",.. "ospx
+000005e0: 3e3d 302e 322e 3134 222c 0d0a 5d0d 0a0d >=0.2.14",..]...
+000005f0: 0a5b 7072 6f6a 6563 742e 7572 6c73 5d0d .[project.urls].
+00000600: 0a48 6f6d 6570 6167 6520 3d20 2268 7474 .Homepage = "htt
+00000610: 7073 3a2f 2f67 6974 6875 622e 636f 6d2f ps://github.com/
+00000620: 646e 762d 6f70 656e 736f 7572 6365 2f66 dnv-opensource/f
+00000630: 6172 6e22 0d0a 446f 6375 6d65 6e74 6174 arn"..Documentat
+00000640: 696f 6e20 3d20 2268 7474 7073 3a2f 2f64 ion = "https://d
+00000650: 6e76 2d6f 7065 6e73 6f75 7263 652e 6769 nv-opensource.gi
+00000660: 7468 7562 2e69 6f2f 6661 726e 2f52 4541 thub.io/farn/REA
+00000670: 444d 452e 6874 6d6c 220d 0a52 6570 6f73 DME.html"..Repos
+00000680: 6974 6f72 7920 3d20 2268 7474 7073 3a2f itory = "https:/
+00000690: 2f67 6974 6875 622e 636f 6d2f 646e 762d /github.com/dnv-
+000006a0: 6f70 656e 736f 7572 6365 2f66 6172 6e2e opensource/farn.
+000006b0: 6769 7422 0d0a 4973 7375 6573 203d 2022 git"..Issues = "
+000006c0: 6874 7470 733a 2f2f 6769 7468 7562 2e63 https://github.c
+000006d0: 6f6d 2f64 6e76 2d6f 7065 6e73 6f75 7263 om/dnv-opensourc
+000006e0: 652f 6661 726e 2f69 7373 7565 7322 0d0a e/farn/issues"..
+000006f0: 4368 616e 6765 6c6f 6720 3d20 2268 7474 Changelog = "htt
+00000700: 7073 3a2f 2f67 6974 6875 622e 636f 6d2f ps://github.com/
+00000710: 646e 762d 6f70 656e 736f 7572 6365 2f66 dnv-opensource/f
+00000720: 6172 6e2f 626c 6f62 2f6d 6169 6e2f 4348 arn/blob/main/CH
+00000730: 414e 4745 4c4f 472e 6d64 220d 0a0d 0a5b ANGELOG.md"....[
+00000740: 7072 6f6a 6563 742e 7363 7269 7074 735d project.scripts]
+00000750: 0d0a 6661 726e 203d 2022 6661 726e 2e63 ..farn = "farn.c
+00000760: 6c69 2e66 6172 6e3a 6d61 696e 220d 0a62 li.farn:main"..b
+00000770: 6174 6368 5072 6f63 6573 7320 3d20 2266 atchProcess = "f
+00000780: 6172 6e2e 7275 6e2e 636c 692e 6261 7463 arn.run.cli.batc
+00000790: 6850 726f 6365 7373 3a6d 6169 6e22 0d0a hProcess:main"..
+000007a0: 0d0a 5b74 6f6f 6c2e 7365 7475 7074 6f6f ..[tool.setuptoo
+000007b0: 6c73 2e70 6163 6b61 6765 732e 6669 6e64 ls.packages.find
+000007c0: 5d0d 0a77 6865 7265 203d 205b 2273 7263 ]..where = ["src
+000007d0: 225d 0d0a 6578 636c 7564 6520 3d20 5b22 "]..exclude = ["
+000007e0: 7465 7374 2a22 5d0d 0a0d 0a5b 746f 6f6c test*"]....[tool
+000007f0: 2e72 7566 665d 0d0a 6578 636c 7564 6520 .ruff]..exclude
+00000800: 3d20 5b0d 0a20 2020 2022 2e67 6974 222c = [.. ".git",
+00000810: 0d0a 2020 2020 222e 7665 6e76 222c 0d0a .. ".venv",..
+00000820: 2020 2020 222e 746f 7822 2c0d 0a20 2020 ".tox",..
+00000830: 2022 6275 696c 6422 2c0d 0a20 2020 2022 "build",.. "
+00000840: 6469 7374 222c 0d0a 2020 2020 225f 5f70 dist",.. "__p
+00000850: 7963 6163 6865 5f5f 222c 0d0a 2020 2020 ycache__",..
+00000860: 222e 2f64 6f63 732f 736f 7572 6365 2f63 "./docs/source/c
+00000870: 6f6e 662e 7079 222c 0d0a 5d0d 0a73 7263 onf.py",..]..src
+00000880: 203d 205b 2273 7263 225d 0d0a 6c69 6e65 = ["src"]..line
+00000890: 2d6c 656e 6774 6820 3d20 3132 300d 0a74 -length = 120..t
+000008a0: 6172 6765 742d 7665 7273 696f 6e20 3d20 arget-version =
+000008b0: 2270 7933 3922 0d0a 0d0a 5b74 6f6f 6c2e "py39"....[tool.
+000008c0: 7275 6666 2e6c 696e 745d 0d0a 6967 6e6f ruff.lint]..igno
+000008d0: 7265 203d 205b 0d0a 2020 2020 2245 3530 re = [.. "E50
+000008e0: 3122 2c20 2023 204c 696e 6520 6c65 6e67 1", # Line leng
+000008f0: 7468 2074 6f6f 206c 6f6e 670d 0a20 2020 th too long..
+00000900: 2022 4431 3030 222c 2020 2320 4d69 7373 "D100", # Miss
+00000910: 696e 6720 646f 6373 7472 696e 6720 696e ing docstring in
+00000920: 2070 7562 6c69 6320 6d6f 6475 6c65 0d0a public module..
+00000930: 2020 2020 2244 3130 3422 2c20 2023 204d "D104", # M
+00000940: 6973 7369 6e67 2064 6f63 7374 7269 6e67 issing docstring
+00000950: 2069 6e20 7075 626c 6963 2070 6163 6b61 in public packa
+00000960: 6765 0d0a 2020 2020 2244 3130 3522 2c20 ge.. "D105",
+00000970: 2023 204d 6973 7369 6e67 2064 6f63 7374 # Missing docst
+00000980: 7269 6e67 2069 6e20 6d61 6769 6320 6d65 ring in magic me
+00000990: 7468 6f64 0d0a 2020 2020 2244 3130 3722 thod.. "D107"
+000009a0: 2c20 2023 204d 6973 7369 6e67 2064 6f63 , # Missing doc
+000009b0: 7374 7269 6e67 2069 6e20 5f5f 696e 6974 string in __init
+000009c0: 5f5f 0d0a 2020 2020 2244 3230 3222 2c20 __.. "D202",
+000009d0: 2023 204e 6f20 626c 616e 6b20 6c69 6e65 # No blank line
+000009e0: 7320 616c 6c6f 7765 6420 6166 7465 7220 s allowed after
+000009f0: 6675 6e63 7469 6f6e 2064 6f63 7374 7269 function docstri
+00000a00: 6e67 0d0a 2020 2020 2244 3230 3322 2c20 ng.. "D203",
+00000a10: 2023 2031 2062 6c61 6e6b 206c 696e 6520 # 1 blank line
+00000a20: 7265 7175 6972 6564 2062 6566 6f72 6520 required before
+00000a30: 636c 6173 7320 646f 6373 7472 696e 670d class docstring.
+00000a40: 0a20 2020 2022 4432 3035 222c 2020 2320 . "D205", #
+00000a50: 3120 626c 616e 6b20 6c69 6e65 2072 6571 1 blank line req
+00000a60: 7569 7265 6420 6265 7477 6565 6e20 7375 uired between su
+00000a70: 6d6d 6172 7920 6c69 6e65 2061 6e64 2064 mmary line and d
+00000a80: 6573 6372 6970 7469 6f6e 0d0a 2020 2020 escription..
+00000a90: 2244 3231 3222 2c20 2023 204d 756c 7469 "D212", # Multi
+00000aa0: 2d6c 696e 6520 646f 6373 7472 696e 6720 -line docstring
+00000ab0: 7375 6d6d 6172 7920 7368 6f75 6c64 2073 summary should s
+00000ac0: 7461 7274 2061 7420 7468 6520 6669 7273 tart at the firs
+00000ad0: 7420 6c69 6e65 0d0a 2020 2020 2244 3231 t line.. "D21
+00000ae0: 3322 2c20 2023 204d 756c 7469 2d6c 696e 3", # Multi-lin
+00000af0: 6520 646f 6373 7472 696e 6720 7375 6d6d e docstring summ
+00000b00: 6172 7920 7368 6f75 6c64 2073 7461 7274 ary should start
+00000b10: 2061 7420 7468 6520 7365 636f 6e64 206c at the second l
+00000b20: 696e 650d 0a20 2020 2023 2022 4e38 3032 ine.. # "N802
+00000b30: 222c 2020 2320 4675 6e63 7469 6f6e 206e ", # Function n
+00000b40: 616d 6520 7368 6f75 6c64 2062 6520 6c6f ame should be lo
+00000b50: 7765 7263 6173 6520 2028 756e 636f 6d6d wercase (uncomm
+00000b60: 656e 7420 6966 2079 6f75 2077 616e 7420 ent if you want
+00000b70: 746f 2061 6c6c 6f77 2055 7070 6572 6361 to allow Upperca
+00000b80: 7365 2066 756e 6374 696f 6e20 6e61 6d65 se function name
+00000b90: 7329 0d0a 2020 2020 2320 224e 3830 3322 s).. # "N803"
+00000ba0: 2c20 2023 2041 7267 756d 656e 7420 6e61 , # Argument na
+00000bb0: 6d65 2073 686f 756c 6420 6265 206c 6f77 me should be low
+00000bc0: 6572 6361 7365 2020 2875 6e63 6f6d 6d65 ercase (uncomme
+00000bd0: 6e74 2069 6620 796f 7520 7761 6e74 2074 nt if you want t
+00000be0: 6f20 616c 6c6f 7720 5570 7065 7263 6173 o allow Uppercas
+00000bf0: 6520 6172 6775 6d65 6e74 206e 616d 6573 e argument names
+00000c00: 290d 0a20 2020 2022 4e38 3036 222c 2020 ).. "N806",
+00000c10: 2320 5661 7269 6162 6c65 2069 6e20 6675 # Variable in fu
+00000c20: 6e63 7469 6f6e 2073 686f 756c 6420 6265 nction should be
+00000c30: 206c 6f77 6572 6361 7365 2020 2875 6e63 lowercase (unc
+00000c40: 6f6d 6d65 6e74 2069 6620 796f 7520 7761 omment if you wa
+00000c50: 6e74 2074 6f20 616c 6c6f 7720 5570 7065 nt to allow Uppe
+00000c60: 7263 6173 6520 7661 7269 6162 6c65 206e rcase variable n
+00000c70: 616d 6573 2069 6e20 6675 6e63 7469 6f6e ames in function
+00000c80: 7329 0d0a 2020 2020 2320 224e 3831 3522 s).. # "N815"
+00000c90: 2c20 2023 2056 6172 6961 626c 6520 696e , # Variable in
+00000ca0: 2063 6c61 7373 2073 636f 7065 2073 686f class scope sho
+00000cb0: 756c 6420 6e6f 7420 6265 206d 6978 6564 uld not be mixed
+00000cc0: 4361 7365 2020 2875 6e63 6f6d 6d65 6e74 Case (uncomment
+00000cd0: 2069 6620 796f 7520 7761 6e74 2074 6f20 if you want to
+00000ce0: 616c 6c6f 7720 6d69 7865 6443 6173 6520 allow mixedCase
+00000cf0: 7661 7269 6162 6c65 206e 616d 6573 2069 variable names i
+00000d00: 6e20 636c 6173 7320 7363 6f70 6529 0d0a n class scope)..
+00000d10: 2020 2020 2320 224e 3831 3622 2c20 2023 # "N816", #
+00000d20: 2056 6172 6961 626c 6520 696e 2067 6c6f Variable in glo
+00000d30: 6261 6c20 7363 6f70 6520 7368 6f75 6c64 bal scope should
+00000d40: 206e 6f74 2062 6520 6d69 7865 6443 6173 not be mixedCas
+00000d50: 6520 2028 756e 636f 6d6d 656e 7420 6966 e (uncomment if
+00000d60: 2079 6f75 2077 616e 7420 746f 2061 6c6c you want to all
+00000d70: 6f77 206d 6978 6564 4361 7365 2076 6172 ow mixedCase var
+00000d80: 6961 626c 6520 6e61 6d65 7320 696e 2067 iable names in g
+00000d90: 6c6f 6261 6c20 7363 6f70 6529 0d0a 2020 lobal scope)..
+00000da0: 2020 224e 3939 3922 2c20 2023 2049 6e76 "N999", # Inv
+00000db0: 616c 6964 206d 6f64 756c 6520 6e61 6d65 alid module name
+00000dc0: 0d0a 2020 2020 5d0d 0a73 656c 6563 7420 .. ]..select
+00000dd0: 3d20 5b0d 0a20 2020 2022 4522 2c0d 0a20 = [.. "E",..
+00000de0: 2020 2022 4422 2c0d 0a20 2020 2022 4622 "D",.. "F"
+00000df0: 2c0d 0a20 2020 2022 4e22 2c0d 0a20 2020 ,.. "N",..
+00000e00: 2022 5722 2c0d 0a20 2020 2022 4922 2c0d "W",.. "I",.
+00000e10: 0a20 2020 2022 4222 2c0d 0a5d 0d0a 0d0a . "B",..]....
+00000e20: 0d0a 5b74 6f6f 6c2e 7275 6666 2e6c 696e ..[tool.ruff.lin
+00000e30: 742e 7065 7038 2d6e 616d 696e 675d 0d0a t.pep8-naming]..
+00000e40: 6967 6e6f 7265 2d6e 616d 6573 203d 205b ignore-names = [
+00000e50: 0d0a 2020 2020 2274 6573 745f 2a22 2c0d .. "test_*",.
+00000e60: 0a20 2020 2022 7365 7455 7022 2c0d 0a20 . "setUp",..
+00000e70: 2020 2022 7465 6172 446f 776e 222c 0d0a "tearDown",..
+00000e80: 5d0d 0a0d 0a5b 746f 6f6c 2e72 7566 662e ]....[tool.ruff.
+00000e90: 6c69 6e74 2e70 7964 6f63 7374 796c 655d lint.pydocstyle]
+00000ea0: 0d0a 636f 6e76 656e 7469 6f6e 203d 2022 ..convention = "
+00000eb0: 6e75 6d70 7922 0d0a 0d0a 5b74 6f6f 6c2e numpy"....[tool.
+00000ec0: 7275 6666 2e6c 696e 742e 7065 722d 6669 ruff.lint.per-fi
+00000ed0: 6c65 2d69 676e 6f72 6573 5d0d 0a22 5f5f le-ignores].."__
+00000ee0: 696e 6974 5f5f 2e70 7922 203d 205b 2249 init__.py" = ["I
+00000ef0: 3030 3122 5d0d 0a22 2e2f 7465 7374 732f 001"].."./tests/
+00000f00: 2a22 203d 205b 2244 225d 0d0a 0d0a 5b74 *" = ["D"]....[t
+00000f10: 6f6f 6c2e 7275 6666 2e66 6f72 6d61 745d ool.ruff.format]
+00000f20: 0d0a 646f 6373 7472 696e 672d 636f 6465 ..docstring-code
+00000f30: 2d66 6f72 6d61 7420 3d20 7472 7565 0d0a -format = true..
+00000f40: 0d0a 5b74 6f6f 6c2e 7079 7269 6768 745d ..[tool.pyright]
+00000f50: 0d0a 6578 636c 7564 6520 3d20 5b0d 0a20 ..exclude = [..
+00000f60: 2020 2022 2e67 6974 222c 0d0a 2020 2020 ".git",..
+00000f70: 222e 7665 6e76 222c 0d0a 2020 2020 222e ".venv",.. ".
+00000f80: 746f 7822 2c0d 0a20 2020 2022 6275 696c tox",.. "buil
+00000f90: 6422 2c0d 0a20 2020 2022 6469 7374 222c d",.. "dist",
+00000fa0: 0d0a 2020 2020 222a 2a2f 5f5f 7079 6361 .. "**/__pyca
+00000fb0: 6368 655f 5f22 2c0d 0a20 2020 2022 2e2f che__",.. "./
+00000fc0: 646f 6373 2f73 6f75 7263 652f 636f 6e66 docs/source/conf
+00000fd0: 2e70 7922 2c0d 0a20 2020 2022 2e2f 7665 .py",.. "./ve
+00000fe0: 6e76 222c 0d0a 5d0d 0a65 7874 7261 5061 nv",..]..extraPa
+00000ff0: 7468 7320 3d20 5b22 2e2f 7372 6322 5d0d ths = ["./src"].
+00001000: 0a74 7970 6543 6865 636b 696e 674d 6f64 .typeCheckingMod
+00001010: 6520 3d20 2262 6173 6963 220d 0a75 7365 e = "basic"..use
+00001020: 4c69 6272 6172 7943 6f64 6546 6f72 5479 LibraryCodeForTy
+00001030: 7065 7320 3d20 7472 7565 0d0a 7265 706f pes = true..repo
+00001040: 7274 4d69 7373 696e 6750 6172 616d 6574 rtMissingParamet
+00001050: 6572 5479 7065 203d 2022 6572 726f 7222 erType = "error"
+00001060: 0d0a 7265 706f 7274 556e 6b6e 6f77 6e50 ..reportUnknownP
+00001070: 6172 616d 6574 6572 5479 7065 203d 2022 arameterType = "
+00001080: 7761 726e 696e 6722 0d0a 7265 706f 7274 warning"..report
+00001090: 556e 6b6e 6f77 6e4d 656d 6265 7254 7970 UnknownMemberTyp
+000010a0: 6520 3d20 2277 6172 6e69 6e67 220d 0a72 e = "warning"..r
+000010b0: 6570 6f72 744d 6973 7369 6e67 5479 7065 eportMissingType
+000010c0: 4172 6775 6d65 6e74 203d 2022 6572 726f Argument = "erro
+000010d0: 7222 0d0a 7265 706f 7274 5072 6f70 6572 r"..reportProper
+000010e0: 7479 5479 7065 4d69 736d 6174 6368 203d tyTypeMismatch =
+000010f0: 2022 6572 726f 7222 0d0a 7265 706f 7274 "error"..report
+00001100: 4675 6e63 7469 6f6e 4d65 6d62 6572 4163 FunctionMemberAc
+00001110: 6365 7373 203d 2022 7761 726e 696e 6722 cess = "warning"
+00001120: 0d0a 7265 706f 7274 5072 6976 6174 6555 ..reportPrivateU
+00001130: 7361 6765 203d 2022 7761 726e 696e 6722 sage = "warning"
+00001140: 0d0a 7265 706f 7274 5479 7065 436f 6d6d ..reportTypeComm
+00001150: 656e 7455 7361 6765 203d 2022 7761 726e entUsage = "warn
+00001160: 696e 6722 0d0a 7265 706f 7274 496e 636f ing"..reportInco
+00001170: 6d70 6174 6962 6c65 4d65 7468 6f64 4f76 mpatibleMethodOv
+00001180: 6572 7269 6465 203d 2022 7761 726e 696e erride = "warnin
+00001190: 6722 0d0a 7265 706f 7274 496e 636f 6d70 g"..reportIncomp
+000011a0: 6174 6962 6c65 5661 7269 6162 6c65 4f76 atibleVariableOv
+000011b0: 6572 7269 6465 203d 2022 6572 726f 7222 erride = "error"
+000011c0: 0d0a 7265 706f 7274 496e 636f 6e73 6973 ..reportInconsis
+000011d0: 7465 6e74 436f 6e73 7472 7563 746f 7220 tentConstructor
+000011e0: 3d20 2265 7272 6f72 220d 0a72 6570 6f72 = "error"..repor
+000011f0: 744f 7665 726c 6170 7069 6e67 4f76 6572 tOverlappingOver
+00001200: 6c6f 6164 203d 2022 7761 726e 696e 6722 load = "warning"
+00001210: 0d0a 7265 706f 7274 556e 696e 6974 6961 ..reportUninitia
+00001220: 6c69 7a65 6449 6e73 7461 6e63 6556 6172 lizedInstanceVar
+00001230: 6961 626c 6520 3d20 2277 6172 6e69 6e67 iable = "warning
+00001240: 220d 0a72 6570 6f72 7443 616c 6c49 6e44 "..reportCallInD
+00001250: 6566 6175 6c74 496e 6974 6961 6c69 7a65 efaultInitialize
+00001260: 7220 3d20 2277 6172 6e69 6e67 220d 0a72 r = "warning"..r
+00001270: 6570 6f72 7455 6e6e 6563 6573 7361 7279 eportUnnecessary
+00001280: 4973 496e 7374 616e 6365 203d 2022 696e IsInstance = "in
+00001290: 666f 726d 6174 696f 6e22 0d0a 7265 706f formation"..repo
+000012a0: 7274 556e 6e65 6365 7373 6172 7943 6173 rtUnnecessaryCas
+000012b0: 7420 3d20 2277 6172 6e69 6e67 220d 0a72 t = "warning"..r
+000012c0: 6570 6f72 7455 6e6e 6563 6573 7361 7279 eportUnnecessary
+000012d0: 436f 6d70 6172 6973 6f6e 203d 2022 7761 Comparison = "wa
+000012e0: 726e 696e 6722 0d0a 7265 706f 7274 556e rning"..reportUn
+000012f0: 6e65 6365 7373 6172 7943 6f6e 7461 696e necessaryContain
+00001300: 7320 3d20 2277 6172 6e69 6e67 220d 0a72 s = "warning"..r
+00001310: 6570 6f72 7455 6e75 7365 6443 616c 6c52 eportUnusedCallR
+00001320: 6573 756c 7420 3d20 2277 6172 6e69 6e67 esult = "warning
+00001330: 220d 0a72 6570 6f72 7455 6e75 7365 6445 "..reportUnusedE
+00001340: 7870 7265 7373 696f 6e20 3d20 2277 6172 xpression = "war
+00001350: 6e69 6e67 220d 0a72 6570 6f72 744d 6174 ning"..reportMat
+00001360: 6368 4e6f 7445 7868 6175 7374 6976 6520 chNotExhaustive
+00001370: 3d20 2277 6172 6e69 6e67 220d 0a72 6570 = "warning"..rep
+00001380: 6f72 7453 6861 646f 7765 6449 6d70 6f72 ortShadowedImpor
+00001390: 7473 203d 2022 7761 726e 696e 6722 0d0a ts = "warning"..
+000013a0: 7265 706f 7274 556e 7479 7065 6446 756e reportUntypedFun
+000013b0: 6374 696f 6e44 6563 6f72 6174 6f72 203d ctionDecorator =
+000013c0: 2022 7761 726e 696e 6722 0d0a 7265 706f "warning"..repo
+000013d0: 7274 556e 7479 7065 6442 6173 6543 6c61 rtUntypedBaseCla
+000013e0: 7373 203d 2022 6572 726f 7222 0d0a 7265 ss = "error"..re
+000013f0: 706f 7274 556e 7479 7065 644e 616d 6564 portUntypedNamed
+00001400: 5475 706c 6520 3d20 2277 6172 6e69 6e67 Tuple = "warning
+00001410: 220d 0a23 2041 6374 6976 6174 6520 7468 "..# Activate th
+00001420: 6520 666f 6c6c 6f77 696e 6720 7275 6c65 e following rule
+00001430: 7320 6f6e 6c79 206c 6f63 616c 6c79 2061 s only locally a
+00001440: 6e64 2074 656d 706f 7261 7279 2c20 692e nd temporary, i.
+00001450: 652e 2066 6f72 2061 2051 4120 7365 7373 e. for a QA sess
+00001460: 696f 6e2e 0d0a 2320 2846 6f72 2073 6572 ion...# (For ser
+00001470: 7665 7220 7369 6465 2043 4920 7468 6579 ver side CI they
+00001480: 2061 7265 2063 6f6e 7369 6465 7265 6420 are considered
+00001490: 746f 6f20 7374 7269 6374 2e29 0d0a 2320 too strict.)..#
+000014a0: 7265 706f 7274 436f 6e73 7461 6e74 5265 reportConstantRe
+000014b0: 6465 6669 6e69 7469 6f6e 203d 2022 7761 definition = "wa
+000014c0: 726e 696e 6722 0d0a 2320 7265 706f 7274 rning"..# report
+000014d0: 556e 6e65 6365 7373 6172 7954 7970 6549 UnnecessaryTypeI
+000014e0: 676e 6f72 6543 6f6d 6d65 6e74 203d 2022 gnoreComment = "
+000014f0: 696e 666f 726d 6174 696f 6e22 0d0a 2320 information"..#
+00001500: 7265 706f 7274 496d 706f 7274 4379 636c reportImportCycl
+00001510: 6573 203d 2022 7761 726e 696e 6722 0d0a es = "warning"..
+00001520: 2320 7265 706f 7274 496d 706c 6963 6974 # reportImplicit
+00001530: 5374 7269 6e67 436f 6e63 6174 656e 6174 StringConcatenat
+00001540: 696f 6e20 3d20 2277 6172 6e69 6e67 220d ion = "warning".
+00001550: 0a .
@@ -1,767 +1,779 @@
-import logging
-import os
-import platform
-import re
-from copy import deepcopy
-from pathlib import Path
-from typing import Any, Dict, List, MutableMapping, MutableSequence, MutableSet, Sequence, Union
-
-from dictIO import CppDict, DictReader, DictWriter, create_target_file_name
-from dictIO.utils.strings import remove_quotes
-
-from farn.core import Case, Cases, Parameter
-from farn.run.batchProcess import AsyncBatchProcessor
-from farn.run.subProcess import execute_in_sub_process
-from farn.utils.logging import plural
-from farn.utils.os import append_system_variable
-
-__ALL__ = [
- "run_farn",
- "create_samples",
- "create_cases",
- "create_case_folders",
- "create_param_dict_files",
- "create_case_list_files",
- "execute_command_set",
-]
-
-logger = logging.getLogger(__name__)
-
-
-def run_farn(
- farn_dict_file: Union[str, os.PathLike[str]],
- sample: bool = False,
- generate: bool = False,
- command: Union[str, None] = None,
- batch: bool = False,
- test: bool = False,
-) -> Cases:
- """Run farn.
-
- Runs the sampling for all layers as configured in farn dict,
- generates the corresponding case folder structure and
- executes user-defined shell command sets in all case folders.
-
- Parameters
- ----------
- farn_dict_file : Union[str, os.PathLike[str]]
- farnDict file. Contains the farn configuration.
- sample : bool, optional
- if True, runs the sampling defined for each layer and saves the sampled farnDict file with prefix sampled., by default False
- generate : bool, optional
- if True, generates the folder structure that spawns all layers and cases defined in farnDict, by default False
- command : Union[str, None], optional
- executes the given command set in all case folders. The command set must be defined in the commands section of the applicable layer in farnDict., by default None
- batch : bool, optional
- if True, executes the given command set in batch mode, i.e. asynchronously, by default False
- test : bool, optional
- if True, runs only first case and returns, by default False
-
- Returns
- -------
- Cases
- List containing all valid leaf cases.
-
- Raises
- ------
- FileNotFoundError
- if farn_dict_file does not exist
- """
-
- # Make sure farn_dict_file argument is of type Path. If not, cast it to Path type.
- farn_dict_file = farn_dict_file if isinstance(farn_dict_file, Path) else Path(farn_dict_file)
-
- # Check whether farn dict file exists
- if not farn_dict_file.exists():
- logger.error(f"run_farn: File {farn_dict_file} not found.")
- raise FileNotFoundError(farn_dict_file)
-
- # Set up farn environment
- farn_dirs: Dict[str, Path] = _set_up_farn_environment(farn_dict_file)
-
- # Read farn dict
- farn_dict = DictReader.read(farn_dict_file, comments=False)
-
- # Run sampling and create the samples for all layers in farn dict
- if sample:
- create_samples(farn_dict) # run sampling
- farn_dict.source_file = create_target_file_name( # change filename to 'sampled.*'
- farn_dict.source_file, prefix="sampled." # type: ignore
- )
- logger.info(f"Save sampled farn dict {farn_dict.name}...") # 1
- DictWriter.write(farn_dict, mode="w") # save sampled.* farn dict file
- logger.info(f"Saved sampled farn dict in {farn_dict.source_file}.") # 1
-
- # Document CLI arguments of current farn call in the farn dict (for traceability)
- farn_opts = {
- "farnDict": farn_dict.name,
- "sample": sample,
- "generate": generate,
- "execute": command,
- "test": test,
- }
- farn_dict.update({"_farnOpts": farn_opts})
-
- # Create all valid cases from the samples defined in farn dict.
- cases = create_cases(
- farn_dict=farn_dict,
- case_dir=farn_dirs["CASEDIR"],
- valid_only=True,
- )
-
- # Generate case folder structure
- # and create a case-specific paramDict file in each case folder
- if generate:
- _ = create_case_folders(cases)
- _ = create_param_dict_files(cases)
- _ = create_case_list_files(
- cases=cases,
- target_dir=farn_dirs["ROOTDIR"],
- )
-
- # Execute a given command set in all case folders
- if command:
- _ = execute_command_set(
- cases=cases,
- command_set=command,
- batch=batch,
- test=test,
- )
-
- valid_leaf_cases: Cases = cases.filter(levels=-1, valid_only=True)
-
- logger.info("Successfully finished farn.\n")
-
- return valid_leaf_cases
-
-
-def create_samples(farn_dict: CppDict):
- """Run sampling and create the samples inside all layers of the passed in farn dict.
-
- Creates the _samples element in each layer and populates it with the discrete samples generated for the parameters defined and varied in the respective layer.
- In case the _samples element already exists in a layer, it will be overwritten.
-
- Parameters
- ----------
- farn_dict : CppDict
- farn dict the samples shall be created in
- """
- from farn.sampling.sampling import DiscreteSampling
-
- if "_layers" not in farn_dict:
- logger.error(f"no '_layers' element in farn dict {farn_dict.name}. Sampling not possible.")
- return
-
- def create_samples_in_layer(
- level: int,
- layer_name: str,
- layer: MutableMapping[str, Any],
- ):
- """Run sampling and generate the samples in the passed in layer."""
- if "_sampling" not in layer:
- logger.error("no '_sampling' element in layer")
- return
- if "_type" not in layer["_sampling"]:
- logger.error("no '_type' element in sampling")
- return
-
- # instantiate and parameterize the sampling object
- sampling = DiscreteSampling()
- sampling.set_sampling_type(sampling_type=layer["_sampling"]["_type"])
- sampling.set_sampling_parameters(
- sampling_parameters=layer["_sampling"],
- layer_name=layer_name,
- )
-
- # in case a _samples element already exists (e.g. from a former run) -> delete it
- if "_samples" in layer:
- del layer["_samples"]
-
- # generate the samples and write them into the _samples element of the layer
- samples: Dict[str, List[Any]] = sampling.generate_samples()
- layer["_samples"] = samples
-
- # if the layer does not have a _comment element yet: create a default comment
- if "_comment" not in layer:
- default_comment = f"level {level:2d}, layer {layer_name}"
- layer["_comment"] = default_comment
-
- return
-
- logger.info(f"Run sampling of {farn_dict.name}...")
-
- for index, (key, value) in enumerate(farn_dict["_layers"].items()):
- create_samples_in_layer(
- level=index,
- layer_name=key,
- layer=value,
- )
-
- logger.info(f"Successfully ran sampling of {farn_dict.name}.")
-
- return
-
-
-def create_cases(
- farn_dict: MutableMapping[Any, Any],
- case_dir: Path,
- valid_only: bool = False,
-) -> Cases:
- """Create cases based on the layers, filter expressions and samples defined in the passed farn dict.
-
- Creates case objects for all cases derived by recursive permutation of layers and the case specific samples defined per layer.
- create_cases() creates one distinct case object for each case, holding all case attributes (parameters) set to their case specific values.
-
- Optionally, only _valid_ cases can be returned, i.e. cases which fulfill the filter criteria configured for the respective layer.
- Invalid cases then get excluded.
-
- Note:
- The corresponding case folder structure is not yet created by create_cases().
- Creating the case folder structure is the responsibility of create_case_folder_structure().
- However, the case_dir argument is passed in to allow create_cases() to already document in each case object
- its _intended_ case folder path. This information is then read and used in create_case_folder_structure()
- to actually create the case folders.
-
- Parameters
- ----------
- farn_dict : MutableMapping
- farn dict. The farn dict must be sampled, e.g. samples must have been generated for all layers defined in the farn dict.
- case_dir : Path
- directory the case folder structure is (intended) to be generated in.
- valid_only: bool
- whether or not only valid cases shall be returned, i.e. cases which fulfill the filter criteria configured for the respective layer., by default False
-
- Returns
- -------
- Cases
- list of case objects representing all created cases.
- """
- log_msg: str = "List all valid cases.." if valid_only else "List all cases.."
- logger.info(log_msg)
-
- # Check default distributions
- default_distribution: Dict[str, Any] = {}
-
- if "_always" in farn_dict:
- default_distribution = farn_dict["_always"]
-
- # Check arguments.
- if "_layers" not in farn_dict:
- logger.error("create_cases: No '_layers' element contained in farn dict.")
- return Cases()
-
- # Initialize cases list
- cases: Cases = Cases()
- number_of_invalid_cases: int = 0
-
- # Create a local layers list that carries also the layers' name
- # to ease sequential and indexed access to individual layers in create_next_level_cases()
- layers: List[Dict[str, Any]] = []
- for layer_name, layer in farn_dict["_layers"].items():
- layer_copy: Dict[str, Any] = deepcopy(layer)
- layer_copy["_name"] = layer_name
- layers.append(layer_copy)
-
- def create_next_level_cases(
- level: int = 0,
- base_case: Union[Case, None] = None,
- ):
- nonlocal cases
- nonlocal number_of_invalid_cases
- nonlocal layers
-
- base_case = base_case or Case(path=Path.cwd())
- base_case.parameters = base_case.parameters or []
-
- current_layer: Dict[str, Any] = layers[level]
- # validity checks for current layer
- if "_samples" not in current_layer:
- logger.warning(
- f"No _samples element found in layer {current_layer['_name']}.\n"
- f"Creation of cases for level {level:2d} aborted. "
- )
- return
- if "_case_name" not in current_layer["_samples"]:
- logger.warning(
- f"The _samples element in layer {current_layer['_name']} is empty or does not have a _case_name element.\n"
- f"Creation of cases for level {level:2d} aborted. "
- )
- return
-
- current_layer_name: str = str(current_layer["_name"])
- current_layer_is_leaf: bool = level == len(layers) - 1
-
- no_of_samples_in_current_layer: int = len(current_layer["_samples"]["_case_name"])
- samples_in_current_layer: MutableMapping[str, MutableSequence[float]] = {
- param_name: param_values
- for param_name, param_values in current_layer["_samples"].items()
- if param_name != "_case_name"
- }
-
- parameter_names_used_in_preceeding_layers: MutableSet[str] = {
- parameter.name for parameter in base_case.parameters if parameter.name
- }
-
- parameter_names_in_current_layer: MutableSequence[str] = []
- for parameter_name in list(samples_in_current_layer.keys()):
- if parameter_name in parameter_names_used_in_preceeding_layers:
- logger.warning(
- f"The parameter {parameter_name} defined in layer {current_layer_name} had already been defined in a preceeding layer.\n"
- f"The preceeding definition prevails. The samples for parameter {parameter_name} defined in layer {current_layer_name} are skipped. "
- )
- else:
- parameter_names_in_current_layer.append(parameter_name)
-
- user_variables_in_current_layer: MutableSequence[Parameter] = []
- for key, item in default_distribution.items():
- if not key.startswith("_"):
- default_variable = Parameter(name=key, value=item)
- user_variables_in_current_layer.append(default_variable)
-
- for key, item in current_layer.items():
- if not key.startswith("_"):
- user_variable = Parameter(name=key, value=item)
- if user_variable.name in parameter_names_used_in_preceeding_layers:
- logger.warning(
- f"The user variable {user_variable.name} defined in layer {current_layer_name} matches a parameter that\n"
- f"had already been defined in a preceeding layer.\n"
- f"The preceeding definition prevails. The user variable {user_variable.name} defined in layer {current_layer_name} is skipped. "
- )
- elif user_variable.name in parameter_names_in_current_layer:
- logger.warning(
- f"The user variable {user_variable.name} defined in layer {current_layer_name} matches a parameter name defined in the same layer.\n"
- f"The preceeding definition prevails. The user variable {user_variable.name} defined in layer {current_layer_name} is skipped. "
- )
- else:
- user_variables_in_current_layer.append(user_variable)
-
- condition_in_current_layer: Union[MutableMapping[str, str], None] = (
- current_layer["_condition"] if "_condition" in current_layer else None
- )
- commands_in_current_layer: Union[MutableMapping[str, List[str]], None] = (
- current_layer["_commands"] if "_commands" in current_layer else None
- )
-
- for index, case_name in enumerate(current_layer["_samples"]["_case_name"]):
- case_name = remove_quotes(case_name)
-
- case_parameters: MutableSequence[Parameter] = [
- parameter for parameter in base_case.parameters if parameter.name
- ]
- case_parameters.extend(
- Parameter(parameter_name, samples_in_current_layer[parameter_name][index])
- for parameter_name in parameter_names_in_current_layer
- )
- case_parameters.extend(user_variables_in_current_layer)
-
- case = Case(
- case=case_name,
- layer=current_layer_name,
- level=level,
- no_of_samples=no_of_samples_in_current_layer,
- index=index,
- path=base_case.path / case_name,
- is_leaf=current_layer_is_leaf,
- condition=condition_in_current_layer,
- parameters=case_parameters,
- command_sets=commands_in_current_layer,
- )
-
- if not valid_only or case.is_valid:
- cases.append(case)
- if not case.is_leaf: # Recursion for next level cases
- create_next_level_cases(
- level=level + 1,
- base_case=case,
- )
- else:
- number_of_invalid_cases += 1
-
- return
-
- # Commence recursive collection of cases among all layers
- base_case = Case(path=case_dir)
- create_next_level_cases(level=0, base_case=base_case)
-
- leaf_cases = [case for case in cases if case.is_leaf]
-
- log_msg = ""
- if valid_only:
- log_msg = (
- f"Successfully listed {len(leaf_cases)} valid case{plural(len(leaf_cases))}. "
- f'{number_of_invalid_cases} invalid case{plural(number_of_invalid_cases)} {plural(number_of_invalid_cases, "were")} excluded.'
- )
- else:
- log_msg = f"Successfully listed {len(leaf_cases)} case{plural(len(leaf_cases))}. "
- logger.info(log_msg)
-
- return cases
-
-
-def create_case_folders(cases: MutableSequence[Case]) -> int:
- """Create the case folder structure for the passed in cases.
-
- Parameters
- ----------
- cases : MutableSequence[Case]
- cases the case folders shall be created for.
-
- Returns
- -------
- int
- number of case folders created.
- """
-
- logger.info("Create case folder structure...")
- number_of_case_folders_created: int = 0
-
- for case in cases:
- logger.debug(f"creating case folder {case.path}") # 1
- case.path.mkdir(parents=True, exist_ok=True)
- number_of_case_folders_created += 1
-
- logger.info(f"Successfully created {number_of_case_folders_created} case folders.")
-
- return number_of_case_folders_created
-
-
-def create_param_dict_files(cases: MutableSequence[Case]) -> int:
- """Create the case specific paramDict files in the case folders of the passed in cases.
-
- paramDict files contain the case specific parameters, meaning, via the paramDict files the case specific values
- for all parameters get distributed to and persisted in the case folders.
-
- Parameters
- ----------
- cases : MutableSequence[Case]
- cases the paramDict file shall be created for
-
- Returns
- -------
- int
- number of paramDict files created
- """
-
- logger.info("Create case-specific paramDict files in all case folders...")
- number_of_param_dicts_created: int = 0
-
- for case in cases:
- logger.debug(f"creating paramDict in {case.path}") # 1
- target_file = case.path / "paramDict"
- param_dict = CppDict(target_file)
-
- for parameter in case.parameters or []:
- if parameter.name and not re.match("^_", parameter.name):
- param_dict[parameter.name] = parameter.value
-
- param_dict["_case"] = case.to_dict()
-
- DictWriter.write(param_dict, target_file, mode="w")
-
- if case.is_leaf:
- number_of_param_dicts_created += 1
-
- leaf_cases = [case for case in cases if case.is_leaf]
-
- logger.info(
- f"Successfully created {number_of_param_dicts_created} "
- f"paramDict file{plural(number_of_param_dicts_created)} "
- f"in {len(leaf_cases)} case folder{plural(len(leaf_cases))}."
- )
-
- return number_of_param_dicts_created
-
-
-def create_case_list_files(
- cases: MutableSequence[Case],
- target_dir: Union[Path, None] = None,
- levels: Union[int, Sequence[int], None] = None,
-) -> list[Path]:
- """Create case list files for the specified nest levels.
-
- Case list files are simple text files containing a list of paths to all case folders that share a common nest level within the case folder structure.
- I.e. a case list file created for level 0 contains the paths to all case folders on level 0.
- A case list file for level 1 contains the paths to all case folders on level 1, and so on.
-
- These lists can be used i.e. in a batchProcess to execute shell commands
- in all case folders of a specific nest level inside the case folder structure.
-
- Parameters
- ----------
- cases : MutableSequence[Case]
- cases the case list files shall be created for
- target_dir : Path, optional
- directory in which the case list files shall be created. If None, current working directory will be used., by default None
- levels : Union[int, Sequence[int], None], optional
- list of integers indicating the nest levels for which case list files shall be created.
- If missing, by default a case list file for the deepest nest level (the leaf level) will becreated., by default None
-
- Returns
- -------
- list[Path]
- The case list files that have been created (returned as a list of Path objects)
- """
-
- _remove_old_case_list_files()
- target_dir = target_dir or Path.cwd()
- case_list_file_all_levels = target_dir / "caseList"
- logger.info(f"Create case list file '{case_list_file_all_levels}', containing all case folders.")
-
- case_list_files_created: MutableSequence[Path] = []
- max_level: int = 0
- with case_list_file_all_levels.open(mode="w") as f:
- for case in cases:
- _ = f.write(f"{case.path.absolute()}\n")
- max_level = max(max_level, case.level)
- case_list_files_created.append(case_list_file_all_levels)
-
- levels = levels or max_level
- levels = [levels] if isinstance(levels, int) else levels
-
- for level in levels:
- case_list_file_for_level = target_dir / f"caseList_level_{level:02d}"
- logger.info(
- f"Create case list file '{case_list_file_for_level}', containing the case folders of level {level}."
- )
- with case_list_file_for_level.open(mode="w") as f:
- for case in (case for case in cases if case.level == level):
- _ = f.write(f"{case.path.absolute()}\n")
- case_list_files_created.append(case_list_file_for_level)
-
- case_list_files_created_log = "".join("\t" + path.name + "\n" for path in case_list_files_created)
- case_list_files_created_log = case_list_files_created_log.removesuffix("\n")
- logger.info(f"Successfully created following case list files:\n {case_list_files_created_log}")
-
- return case_list_files_created
-
-
-def execute_command_set(
- cases: MutableSequence[Case],
- command_set: str,
- batch: bool = True,
- test: bool = False,
-) -> int:
- """Execute the given command set in the case folders of the passed in cases.
-
- Parameters
- ----------
- cases : MutableSequence[Case]
- cases for which the specified command set shall be executed.
- command_set : str
- name of the command set to be executed, as defined in farnDict
- batch : bool, optional
- if True, executes the given command set in batch mode, i.e. asynchronously, by default False
- test : bool, optional
- if True, executes command set in only first case folder where command set is defined, by default False
-
- Returns
- -------
- int
- number of case folders in which the command set has been executed
- """
-
- logger.info(f"Execute command set '{command_set}' in all layers where '{command_set}' is defined...")
-
- cases_registered: List[Case] = []
- number_of_cases_registered: int = 0
- reached_first_leaf: bool = False
- if test:
- logger.warning(
- f"farn.py called with option --test: Only first case folder where command set '{command_set}' is defined will be executed."
- )
-
- for case in cases:
- if not case.path.exists():
- logger.warning(
- f"Path {case.path} does not exist. "
- f"This most commonly happens if a filter expression was changed in between generating the folder structure (option --generate) \n"
- f"and executing a command set (option --execute). "
- f"If so, first generate the missing cases by calling farn with option --generate once again \n"
- f"and then retry to execute the command set with option --execute."
- )
- continue
- if case.command_sets:
- if command_set in case.command_sets:
- cases_registered.append(case)
- number_of_cases_registered += 1
- if case.is_leaf:
- reached_first_leaf = True
- else:
- logger.debug(f"Command set '{command_set}' not defined in case {case.case}")
- if test and reached_first_leaf: # if test and at least one execution
- break
-
- number_of_cases_processed: int = 0
-
- if batch:
- cases_per_shell_command: Dict[str, List[Case]] = {}
- for case in cases_registered:
- if case.command_sets and command_set in case.command_sets:
- shell_commands: List[str] = case.command_sets[command_set]
- for shell_command in shell_commands:
- if shell_command in cases_per_shell_command:
- cases_per_shell_command[shell_command].append(case)
- else:
- cases_per_shell_command |= {shell_command: [case]}
- for index, (shell_command, cases) in enumerate(cases_per_shell_command.items()):
- case_list_file = Path.cwd() / f"caseList_for_command_{index}"
- with case_list_file.open(mode="w") as f:
- for case in cases:
- _ = f.write(f"{case.path.absolute()}\n")
- batch_processor = AsyncBatchProcessor(case_list_file, shell_command)
- batch_processor.run()
- else:
- for case in cases_registered:
- if case.command_sets and command_set in case.command_sets:
- shell_commands = case.command_sets[command_set]
- # logger.debug(f"Execute command set '{command_set}' in {case.path}") # commented out as a similar message gets logged in also subProcess
- # Temporarily change cwd to case folder, to execute the shell commands from there
- current_dir = Path.cwd()
- os.chdir(case.path)
- # Execute shell commands
- _execute_shell_commands(shell_commands)
- # Change back cwd to current folder
- os.chdir(current_dir)
- number_of_cases_processed += 1
-
- # @TODO: This is only a temporary dummy.
- # To be replaced by a smarter algorithm.
- # CLAROS, 2022-08-16
- number_of_cases_processed = number_of_cases_registered
-
- if number_of_cases_processed > 0:
- if test:
- logger.info(
- f"Test finished. Executed command set '{command_set}' in following case folder:\n"
- f"\t {cases_registered[-1].path}"
- )
- else:
- logger.info(
- f"Successfully executed command set '{command_set}' "
- f"in {number_of_cases_registered} case folder{plural(number_of_cases_registered)}."
- )
-
- return number_of_cases_registered
-
-
-def _set_up_farn_environment(farn_dict_file: Path) -> Dict[str, Path]:
- """Read the '_environment' section from farn dict and sets up the farn environment accordingly.
-
- Reads the '_environment' section from farnDict and sets up the farn environment directories as configured therein.
- If the '_environment' section or certain entries therein are missing in farn dict, default values will be used.
-
- Parameters
- ----------
- farn_dict_file : Path
- farnDict file
-
- Returns
- -------
- Dict[str, str]
- dict containing the environment directories set up for farn (matching the _environment section in farnDict)
- """
-
- logger.info("Set up farn environment...")
-
- # Set up farn environment.
- # 1: Define default values for environment
- # sourcery skip: merge-dict-assign
- environment: Dict[str, str] = {}
- environment["CASEDIR"] = "cases"
- environment["DUMPDIR"] = "dump"
- environment["LOGDIR"] = "logs"
- environment["RESULTDIR"] = "results"
- environment["TEMPLATEDIR"] = "template"
- # 2: Overwrite default values with values defined in farn dict, if so
- if environment_from_farn_dict := DictReader.read(farn_dict_file, scope=["_environment"]):
- environment |= environment_from_farn_dict
- else:
- logger.warning(
- f"Key '_environment' is missing in farn dict {farn_dict_file}. Using default values for farn environment."
- )
-
- # Read farn directories from environment
- farn_dirs: Dict[str, Path]
- farn_dirs = {k: Path.joinpath(Path.cwd(), v) for k, v in environment.items()}
- farn_dirs["ROOTDIR"] = Path.cwd()
- # Configure logging handler to write the farn log (use an additional handler, exclusively for farn)
- _configure_additional_logging_handler_exclusively_for_farn(farn_dirs["LOGDIR"])
-
- # Set up system environment variables for each farn directory
- # This is necessary to enable shell commands defined in farnDict to point to them with i.e. %TEMPLATEDIR%
- for key, item in farn_dirs.items():
- append_system_variable(key, str(item))
-
- logger.info("Successfully set up farn environment.")
-
- return farn_dirs
-
-
-def _configure_additional_logging_handler_exclusively_for_farn(log_dir: Path):
- """Create an additional logging handler exclusively for the farn log.
-
- Parameters
- ----------
- log_dir : Path
- folder in which the log file will be created
- """
- # Create log file
- log_dir.mkdir(parents=True, exist_ok=True)
- log_file = log_dir / "farn.log"
- # Create logging file handler
- file_handler = logging.FileHandler(str(log_file.absolute()), "a")
- file_handler.name = str(log_file.absolute())
- file_handler.setLevel(logging.INFO)
- file_formatter = logging.Formatter("%(asctime)s %(levelname)-8s %(message)s", "%Y-%m-%d %H:%M:%S")
- file_handler.setFormatter(file_formatter)
- # Register file handler at root logger
- root_logger = logging.getLogger()
- file_handler_already_exists: bool = any(handler.name == file_handler.name for handler in root_logger.handlers)
- if not file_handler_already_exists:
- root_logger.addHandler(file_handler)
- return
-
-
-def _remove_old_case_list_files(): # sourcery skip: avoid-builtin-shadow
- """Remove old case list files, if existing."""
- logger.info("Remove old case list files...")
-
- lists = [list for list in Path.cwd().rglob("*") if re.search("(path|queue)List", str(list))]
-
- for list in lists:
- list = Path(list)
- list.unlink()
-
- logger.info("Successfully removed old case list files.")
-
- return
-
-
-def _sys_call(shell_commands: MutableSequence[str]):
- """Fallback function until _execute_command is usable under linux."""
-
- for shell_command in shell_commands:
- _ = os.system(shell_command)
-
- return
-
-
-def _execute_shell_commands(shell_commands: MutableSequence[str]):
- """Execute a sequence of shell commands using subprocess.
-
- Parameters
- ----------
- shell_commands : MutableSequence
- list with shell commands to be executed
- """
-
- # @TODO: until the problem with vanishing '.'s on Linux systems is solved (e.g. in command "ln -s target ."),
- # reroute the function call to _sys_call instead, as a workaround.
- if platform.system() == "Linux":
- _sys_call(shell_commands)
- return
-
- for shell_command in shell_commands:
- _ = execute_in_sub_process(shell_command)
-
- return
+import logging
+import os
+import platform
+import re
+from copy import deepcopy
+from pathlib import Path
+from typing import (
+ Any,
+ Dict,
+ List,
+ MutableMapping,
+ MutableSequence,
+ MutableSet,
+ Sequence,
+ Union,
+)
+
+from dictIO import CppDict, DictReader, DictWriter, create_target_file_name
+from dictIO.utils.strings import remove_quotes
+
+from farn.core import Case, Cases, Parameter
+from farn.run.batchProcess import AsyncBatchProcessor
+from farn.run.subProcess import execute_in_sub_process
+from farn.utils.logging import plural
+from farn.utils.os import append_system_variable
+
+__ALL__ = [
+ "run_farn",
+ "create_samples",
+ "create_cases",
+ "create_case_folders",
+ "create_param_dict_files",
+ "create_case_list_files",
+ "execute_command_set",
+]
+
+logger = logging.getLogger(__name__)
+
+
+def run_farn(
+ farn_dict_file: Union[str, os.PathLike[str]],
+ sample: bool = False,
+ generate: bool = False,
+ command: Union[str, None] = None,
+ batch: bool = False,
+ test: bool = False,
+) -> Cases:
+ """Run farn.
+
+ Runs the sampling for all layers as configured in farn dict,
+ generates the corresponding case folder structure and
+ executes user-defined shell command sets in all case folders.
+
+ Parameters
+ ----------
+ farn_dict_file : Union[str, os.PathLike[str]]
+ farnDict file. Contains the farn configuration.
+ sample : bool, optional
+ if True, runs the sampling defined for each layer and saves the sampled farnDict file with prefix sampled., by default False
+ generate : bool, optional
+ if True, generates the folder structure that spawns all layers and cases defined in farnDict, by default False
+ command : Union[str, None], optional
+ executes the given command set in all case folders. The command set must be defined in the commands section of the applicable layer in farnDict., by default None
+ batch : bool, optional
+ if True, executes the given command set in batch mode, i.e. asynchronously, by default False
+ test : bool, optional
+ if True, runs only first case and returns, by default False
+
+ Returns
+ -------
+ Cases
+ List containing all valid leaf cases.
+
+ Raises
+ ------
+ FileNotFoundError
+ if farn_dict_file does not exist
+ """
+ # sourcery skip: extract-method
+
+ # Make sure farn_dict_file argument is of type Path. If not, cast it to Path type.
+ farn_dict_file = farn_dict_file if isinstance(farn_dict_file, Path) else Path(farn_dict_file)
+
+ # Check whether farn dict file exists
+ if not farn_dict_file.exists():
+ logger.error(f"run_farn: File {farn_dict_file} not found.")
+ raise FileNotFoundError(farn_dict_file)
+
+ # Set up farn environment
+ farn_dirs: Dict[str, Path] = _set_up_farn_environment(farn_dict_file)
+
+ # Read farn dict
+ farn_dict = DictReader.read(farn_dict_file, comments=False)
+
+ # Run sampling and create the samples for all layers in farn dict
+ if sample:
+ create_samples(farn_dict) # run sampling
+ assert farn_dict.source_file is not None
+ farn_dict.source_file = create_target_file_name( # change filename to 'sampled.*'
+ farn_dict.source_file,
+ prefix="sampled.", # type: ignore
+ )
+ logger.info(f"Save sampled farn dict {farn_dict.name}...") # 1
+ DictWriter.write(farn_dict, mode="w") # save sampled.* farn dict file
+ logger.info(f"Saved sampled farn dict in {farn_dict.source_file}.") # 1
+
+ # Document CLI arguments of current farn call in the farn dict (for traceability)
+ farn_opts = {
+ "farnDict": farn_dict.name,
+ "sample": sample,
+ "generate": generate,
+ "execute": command,
+ "test": test,
+ }
+ farn_dict.update({"_farnOpts": farn_opts})
+
+ # Create all valid cases from the samples defined in farn dict.
+ cases = create_cases(
+ farn_dict=farn_dict,
+ case_dir=farn_dirs["CASEDIR"],
+ valid_only=True,
+ )
+
+ # Generate case folder structure
+ # and create a case-specific paramDict file in each case folder
+ if generate:
+ _ = create_case_folders(cases)
+ _ = create_param_dict_files(cases)
+ _ = create_case_list_files(
+ cases=cases,
+ target_dir=farn_dirs["ROOTDIR"],
+ )
+
+ # Execute a given command set in all case folders
+ if command:
+ _ = execute_command_set(
+ cases=cases,
+ command_set=command,
+ batch=batch,
+ test=test,
+ )
+
+ valid_leaf_cases: Cases = cases.filter(levels=-1, valid_only=True)
+
+ logger.info("Successfully finished farn.\n")
+
+ return valid_leaf_cases
+
+
+def create_samples(farn_dict: CppDict):
+ """Run sampling and create the samples inside all layers of the passed in farn dict.
+
+ Creates the _samples element in each layer and populates it with the discrete samples generated for the parameters defined and varied in the respective layer.
+ In case the _samples element already exists in a layer, it will be overwritten.
+
+ Parameters
+ ----------
+ farn_dict : CppDict
+ farn dict the samples shall be created in
+ """
+ from farn.sampling.sampling import DiscreteSampling
+
+ if "_layers" not in farn_dict:
+ logger.error(f"no '_layers' element in farn dict {farn_dict.name}. Sampling not possible.")
+ return
+
+ def create_samples_in_layer(
+ level: int,
+ layer_name: str,
+ layer: MutableMapping[str, Any],
+ ):
+ """Run sampling and generate the samples in the passed in layer."""
+ if "_sampling" not in layer:
+ logger.error("no '_sampling' element in layer")
+ return
+ if "_type" not in layer["_sampling"]:
+ logger.error("no '_type' element in sampling")
+ return
+
+ # instantiate and parameterize the sampling object
+ sampling = DiscreteSampling()
+ sampling.set_sampling_type(sampling_type=layer["_sampling"]["_type"])
+ sampling.set_sampling_parameters(
+ sampling_parameters=layer["_sampling"],
+ layer_name=layer_name,
+ )
+
+ # in case a _samples element already exists (e.g. from a former run) -> delete it
+ if "_samples" in layer:
+ del layer["_samples"]
+
+ # generate the samples and write them into the _samples element of the layer
+ samples: Dict[str, List[Any]] = sampling.generate_samples()
+ layer["_samples"] = samples
+
+ # if the layer does not have a _comment element yet: create a default comment
+ if "_comment" not in layer:
+ default_comment = f"level {level:2d}, layer {layer_name}"
+ layer["_comment"] = default_comment
+
+ return
+
+ logger.info(f"Run sampling of {farn_dict.name}...")
+
+ for index, (key, value) in enumerate(farn_dict["_layers"].items()):
+ create_samples_in_layer(
+ level=index,
+ layer_name=key,
+ layer=value,
+ )
+
+ logger.info(f"Successfully ran sampling of {farn_dict.name}.")
+
+ return
+
+
+def create_cases(
+ farn_dict: MutableMapping[Any, Any],
+ case_dir: Path,
+ valid_only: bool = False,
+) -> Cases:
+ """Create cases based on the layers, filter expressions and samples defined in the passed farn dict.
+
+ Creates case objects for all cases derived by recursive permutation of layers and the case specific samples defined per layer.
+ create_cases() creates one distinct case object for each case, holding all case attributes (parameters) set to their case specific values.
+
+ Optionally, only _valid_ cases can be returned, i.e. cases which fulfill the filter criteria configured for the respective layer.
+ Invalid cases then get excluded.
+
+ Note:
+ The corresponding case folder structure is not yet created by create_cases().
+ Creating the case folder structure is the responsibility of create_case_folder_structure().
+ However, the case_dir argument is passed in to allow create_cases() to already document in each case object
+ its _intended_ case folder path. This information is then read and used in create_case_folder_structure()
+ to actually create the case folders.
+
+ Parameters
+ ----------
+ farn_dict : MutableMapping
+ farn dict. The farn dict must be sampled, e.g. samples must have been generated for all layers defined in the farn dict.
+ case_dir : Path
+ directory the case folder structure is (intended) to be generated in.
+ valid_only: bool
+ whether or not only valid cases shall be returned, i.e. cases which fulfill the filter criteria configured for the respective layer., by default False
+
+ Returns
+ -------
+ Cases
+ list of case objects representing all created cases.
+ """
+ log_msg: str = "List all valid cases.." if valid_only else "List all cases.."
+ logger.info(log_msg)
+
+ # Check default distributions
+ default_distribution: Dict[str, Any] = {}
+
+ if "_always" in farn_dict:
+ default_distribution = farn_dict["_always"]
+
+ # Check arguments.
+ if "_layers" not in farn_dict:
+ logger.error("create_cases: No '_layers' element contained in farn dict.")
+ return Cases()
+
+ # Initialize cases list
+ cases: Cases = Cases()
+ number_of_invalid_cases: int = 0
+
+ # Create a local layers list that carries also the layers' name
+ # to ease sequential and indexed access to individual layers in create_next_level_cases()
+ layers: List[Dict[str, Any]] = []
+ for layer_name, layer in farn_dict["_layers"].items():
+ layer_copy: Dict[str, Any] = deepcopy(layer)
+ layer_copy["_name"] = layer_name
+ layers.append(layer_copy)
+
+ def create_next_level_cases(
+ level: int = 0,
+ base_case: Union[Case, None] = None,
+ ):
+ nonlocal cases
+ nonlocal number_of_invalid_cases
+ nonlocal layers
+
+ base_case = base_case or Case(path=Path.cwd())
+ base_case.parameters = base_case.parameters or []
+
+ current_layer: Dict[str, Any] = layers[level]
+ # validity checks for current layer
+ if "_samples" not in current_layer:
+ logger.warning(
+ f"No _samples element found in layer {current_layer['_name']}.\n"
+ f"Creation of cases for level {level:2d} aborted. "
+ )
+ return
+ if "_case_name" not in current_layer["_samples"]:
+ logger.warning(
+ f"The _samples element in layer {current_layer['_name']} is empty or does not have a _case_name element.\n"
+ f"Creation of cases for level {level:2d} aborted. "
+ )
+ return
+
+ current_layer_name: str = str(current_layer["_name"])
+ current_layer_is_leaf: bool = level == len(layers) - 1
+
+ no_of_samples_in_current_layer: int = len(current_layer["_samples"]["_case_name"])
+ samples_in_current_layer: MutableMapping[str, MutableSequence[float]] = {
+ param_name: param_values
+ for param_name, param_values in current_layer["_samples"].items()
+ if param_name != "_case_name"
+ }
+
+ parameter_names_used_in_preceeding_layers: MutableSet[str] = {
+ parameter.name for parameter in base_case.parameters if parameter.name
+ }
+
+ parameter_names_in_current_layer: MutableSequence[str] = []
+ for parameter_name in list(samples_in_current_layer.keys()):
+ if parameter_name in parameter_names_used_in_preceeding_layers:
+ logger.warning(
+ f"The parameter {parameter_name} defined in layer {current_layer_name} had already been defined in a preceeding layer.\n"
+ f"The preceeding definition prevails. The samples for parameter {parameter_name} defined in layer {current_layer_name} are skipped. "
+ )
+ else:
+ parameter_names_in_current_layer.append(parameter_name)
+
+ user_variables_in_current_layer: MutableSequence[Parameter] = []
+ for key, item in default_distribution.items():
+ if not key.startswith("_"):
+ default_variable = Parameter(name=key, value=item)
+ user_variables_in_current_layer.append(default_variable)
+
+ for key, item in current_layer.items():
+ if not key.startswith("_"):
+ user_variable = Parameter(name=key, value=item)
+ if user_variable.name in parameter_names_used_in_preceeding_layers:
+ logger.warning(
+ f"The user variable {user_variable.name} defined in layer {current_layer_name} matches a parameter that\n"
+ f"had already been defined in a preceeding layer.\n"
+ f"The preceeding definition prevails. The user variable {user_variable.name} defined in layer {current_layer_name} is skipped. "
+ )
+ elif user_variable.name in parameter_names_in_current_layer:
+ logger.warning(
+ f"The user variable {user_variable.name} defined in layer {current_layer_name} matches a parameter name defined in the same layer.\n"
+ f"The preceeding definition prevails. The user variable {user_variable.name} defined in layer {current_layer_name} is skipped. "
+ )
+ else:
+ user_variables_in_current_layer.append(user_variable)
+
+ condition_in_current_layer: Union[MutableMapping[str, str], None] = (
+ current_layer["_condition"] if "_condition" in current_layer else None
+ )
+ commands_in_current_layer: Union[MutableMapping[str, List[str]], None] = (
+ current_layer["_commands"] if "_commands" in current_layer else None
+ )
+
+ for index, case_name in enumerate(current_layer["_samples"]["_case_name"]):
+ case_name = remove_quotes(case_name)
+
+ case_parameters: MutableSequence[Parameter] = [
+ parameter for parameter in base_case.parameters if parameter.name
+ ]
+ case_parameters.extend(
+ Parameter(parameter_name, samples_in_current_layer[parameter_name][index])
+ for parameter_name in parameter_names_in_current_layer
+ )
+ case_parameters.extend(user_variables_in_current_layer)
+
+ case = Case(
+ case=case_name,
+ layer=current_layer_name,
+ level=level,
+ no_of_samples=no_of_samples_in_current_layer,
+ index=index,
+ path=base_case.path / case_name,
+ is_leaf=current_layer_is_leaf,
+ condition=condition_in_current_layer,
+ parameters=case_parameters,
+ command_sets=commands_in_current_layer,
+ )
+
+ if not valid_only or case.is_valid:
+ cases.append(case)
+ if not case.is_leaf: # Recursion for next level cases
+ create_next_level_cases(
+ level=level + 1,
+ base_case=case,
+ )
+ else:
+ number_of_invalid_cases += 1
+
+ return
+
+ # Commence recursive collection of cases among all layers
+ base_case = Case(path=case_dir)
+ create_next_level_cases(level=0, base_case=base_case)
+
+ leaf_cases = [case for case in cases if case.is_leaf]
+
+ log_msg = ""
+ if valid_only:
+ log_msg = (
+ f"Successfully listed {len(leaf_cases)} valid case{plural(len(leaf_cases))}. "
+ f'{number_of_invalid_cases} invalid case{plural(number_of_invalid_cases)} {plural(number_of_invalid_cases, "were")} excluded.'
+ )
+ else:
+ log_msg = f"Successfully listed {len(leaf_cases)} case{plural(len(leaf_cases))}. "
+ logger.info(log_msg)
+
+ return cases
+
+
+def create_case_folders(cases: MutableSequence[Case]) -> int:
+ """Create the case folder structure for the passed in cases.
+
+ Parameters
+ ----------
+ cases : MutableSequence[Case]
+ cases the case folders shall be created for.
+
+ Returns
+ -------
+ int
+ number of case folders created.
+ """
+
+ logger.info("Create case folder structure...")
+ number_of_case_folders_created: int = 0
+
+ for case in cases:
+ logger.debug(f"creating case folder {case.path}") # 1
+ case.path.mkdir(parents=True, exist_ok=True)
+ number_of_case_folders_created += 1
+
+ logger.info(f"Successfully created {number_of_case_folders_created} case folders.")
+
+ return number_of_case_folders_created
+
+
+def create_param_dict_files(cases: MutableSequence[Case]) -> int:
+ """Create the case specific paramDict files in the case folders of the passed in cases.
+
+ paramDict files contain the case specific parameters, meaning, via the paramDict files the case specific values
+ for all parameters get distributed to and persisted in the case folders.
+
+ Parameters
+ ----------
+ cases : MutableSequence[Case]
+ cases the paramDict file shall be created for
+
+ Returns
+ -------
+ int
+ number of paramDict files created
+ """
+
+ logger.info("Create case-specific paramDict files in all case folders...")
+ number_of_param_dicts_created: int = 0
+
+ for case in cases:
+ logger.debug(f"creating paramDict in {case.path}") # 1
+ target_file = case.path / "paramDict"
+ param_dict = CppDict(target_file)
+
+ for parameter in case.parameters or []:
+ if parameter.name and not re.match("^_", parameter.name):
+ param_dict[parameter.name] = parameter.value
+
+ param_dict["_case"] = case.to_dict()
+
+ DictWriter.write(param_dict, target_file, mode="w")
+
+ if case.is_leaf:
+ number_of_param_dicts_created += 1
+
+ leaf_cases = [case for case in cases if case.is_leaf]
+
+ logger.info(
+ f"Successfully created {number_of_param_dicts_created} "
+ f"paramDict file{plural(number_of_param_dicts_created)} "
+ f"in {len(leaf_cases)} case folder{plural(len(leaf_cases))}."
+ )
+
+ return number_of_param_dicts_created
+
+
+def create_case_list_files(
+ cases: MutableSequence[Case],
+ target_dir: Union[Path, None] = None,
+ levels: Union[int, Sequence[int], None] = None,
+) -> list[Path]:
+ """Create case list files for the specified nest levels.
+
+ Case list files are simple text files containing a list of paths to all case folders that share a common nest level within the case folder structure.
+ I.e. a case list file created for level 0 contains the paths to all case folders on level 0.
+ A case list file for level 1 contains the paths to all case folders on level 1, and so on.
+
+ These lists can be used i.e. in a batchProcess to execute shell commands
+ in all case folders of a specific nest level inside the case folder structure.
+
+ Parameters
+ ----------
+ cases : MutableSequence[Case]
+ cases the case list files shall be created for
+ target_dir : Path, optional
+ directory in which the case list files shall be created. If None, current working directory will be used., by default None
+ levels : Union[int, Sequence[int], None], optional
+ list of integers indicating the nest levels for which case list files shall be created.
+ If missing, by default a case list file for the deepest nest level (the leaf level) will becreated., by default None
+
+ Returns
+ -------
+ list[Path]
+ The case list files that have been created (returned as a list of Path objects)
+ """
+
+ _remove_old_case_list_files()
+ target_dir = target_dir or Path.cwd()
+ case_list_file_all_levels = target_dir / "caseList"
+ logger.info(f"Create case list file '{case_list_file_all_levels}', containing all case folders.")
+
+ case_list_files_created: MutableSequence[Path] = []
+ max_level: int = 0
+ with case_list_file_all_levels.open(mode="w") as f:
+ for case in cases:
+ _ = f.write(f"{case.path.absolute()}\n")
+ max_level = max(max_level, case.level)
+ case_list_files_created.append(case_list_file_all_levels)
+
+ levels = levels or max_level
+ levels = [levels] if isinstance(levels, int) else levels
+
+ for level in levels:
+ case_list_file_for_level = target_dir / f"caseList_level_{level:02d}"
+ logger.info(
+ f"Create case list file '{case_list_file_for_level}', containing the case folders of level {level}."
+ )
+ with case_list_file_for_level.open(mode="w") as f:
+ for case in (case for case in cases if case.level == level):
+ _ = f.write(f"{case.path.absolute()}\n")
+ case_list_files_created.append(case_list_file_for_level)
+
+ case_list_files_created_log = "".join("\t" + path.name + "\n" for path in case_list_files_created)
+ case_list_files_created_log = case_list_files_created_log.removesuffix("\n")
+ logger.info(f"Successfully created following case list files:\n {case_list_files_created_log}")
+
+ return case_list_files_created
+
+
+def execute_command_set(
+ cases: MutableSequence[Case],
+ command_set: str,
+ batch: bool = True,
+ test: bool = False,
+) -> int:
+ """Execute the given command set in the case folders of the passed in cases.
+
+ Parameters
+ ----------
+ cases : MutableSequence[Case]
+ cases for which the specified command set shall be executed.
+ command_set : str
+ name of the command set to be executed, as defined in farnDict
+ batch : bool, optional
+ if True, executes the given command set in batch mode, i.e. asynchronously, by default False
+ test : bool, optional
+ if True, executes command set in only first case folder where command set is defined, by default False
+
+ Returns
+ -------
+ int
+ number of case folders in which the command set has been executed
+ """
+
+ logger.info(f"Execute command set '{command_set}' in all layers where '{command_set}' is defined...")
+
+ cases_registered: List[Case] = []
+ number_of_cases_registered: int = 0
+ reached_first_leaf: bool = False
+ if test:
+ logger.warning(
+ f"farn.py called with option --test: Only first case folder where command set '{command_set}' is defined will be executed."
+ )
+
+ for case in cases:
+ if not case.path.exists():
+ logger.warning(
+ f"Path {case.path} does not exist. "
+ f"This most commonly happens if a filter expression was changed in between generating the folder structure (option --generate) \n"
+ f"and executing a command set (option --execute). "
+ f"If so, first generate the missing cases by calling farn with option --generate once again \n"
+ f"and then retry to execute the command set with option --execute."
+ )
+ continue
+ if case.command_sets:
+ if command_set in case.command_sets:
+ cases_registered.append(case)
+ number_of_cases_registered += 1
+ if case.is_leaf:
+ reached_first_leaf = True
+ else:
+ logger.debug(f"Command set '{command_set}' not defined in case {case.case}")
+ if test and reached_first_leaf: # if test and at least one execution
+ break
+
+ number_of_cases_processed: int = 0
+
+ if batch:
+ cases_per_shell_command: Dict[str, List[Case]] = {}
+ for case in cases_registered:
+ if case.command_sets and command_set in case.command_sets:
+ shell_commands: List[str] = case.command_sets[command_set]
+ for shell_command in shell_commands:
+ if shell_command in cases_per_shell_command:
+ cases_per_shell_command[shell_command].append(case)
+ else:
+ cases_per_shell_command |= {shell_command: [case]}
+ for index, (shell_command, cases) in enumerate(cases_per_shell_command.items()):
+ case_list_file = Path.cwd() / f"caseList_for_command_{index}"
+ with case_list_file.open(mode="w") as f:
+ for case in cases:
+ _ = f.write(f"{case.path.absolute()}\n")
+ batch_processor = AsyncBatchProcessor(case_list_file, shell_command)
+ batch_processor.run()
+ else:
+ for case in cases_registered:
+ if case.command_sets and command_set in case.command_sets:
+ shell_commands = case.command_sets[command_set]
+ # logger.debug(f"Execute command set '{command_set}' in {case.path}") # commented out as a similar message gets logged in also subProcess
+ # Temporarily change cwd to case folder, to execute the shell commands from there
+ current_dir = Path.cwd()
+ os.chdir(case.path)
+ # Execute shell commands
+ _execute_shell_commands(shell_commands)
+ # Change back cwd to current folder
+ os.chdir(current_dir)
+ number_of_cases_processed += 1
+
+ # @TODO: This is only a temporary dummy.
+ # To be replaced by a smarter algorithm.
+ # CLAROS, 2022-08-16
+ number_of_cases_processed = number_of_cases_registered
+
+ if number_of_cases_processed > 0:
+ if test:
+ logger.info(
+ f"Test finished. Executed command set '{command_set}' in following case folder:\n"
+ f"\t {cases_registered[-1].path}"
+ )
+ else:
+ logger.info(
+ f"Successfully executed command set '{command_set}' "
+ f"in {number_of_cases_registered} case folder{plural(number_of_cases_registered)}."
+ )
+
+ return number_of_cases_registered
+
+
+def _set_up_farn_environment(farn_dict_file: Path) -> Dict[str, Path]:
+ """Read the '_environment' section from farn dict and sets up the farn environment accordingly.
+
+ Reads the '_environment' section from farnDict and sets up the farn environment directories as configured therein.
+ If the '_environment' section or certain entries therein are missing in farn dict, default values will be used.
+
+ Parameters
+ ----------
+ farn_dict_file : Path
+ farnDict file
+
+ Returns
+ -------
+ Dict[str, str]
+ dict containing the environment directories set up for farn (matching the _environment section in farnDict)
+ """
+
+ logger.info("Set up farn environment...")
+
+ # Set up farn environment.
+ # 1: Define default values for environment
+ # sourcery skip: merge-dict-assign
+ environment: Dict[str, str] = {}
+ environment["CASEDIR"] = "cases"
+ environment["DUMPDIR"] = "dump"
+ environment["LOGDIR"] = "logs"
+ environment["RESULTDIR"] = "results"
+ environment["TEMPLATEDIR"] = "template"
+ # 2: Overwrite default values with values defined in farn dict, if so
+ if environment_from_farn_dict := DictReader.read(farn_dict_file, scope=["_environment"]):
+ environment |= environment_from_farn_dict
+ else:
+ logger.warning(
+ f"Key '_environment' is missing in farn dict {farn_dict_file}. Using default values for farn environment."
+ )
+
+ # Read farn directories from environment
+ farn_dirs: Dict[str, Path]
+ farn_dirs = {k: Path.joinpath(Path.cwd(), v) for k, v in environment.items()}
+ farn_dirs["ROOTDIR"] = Path.cwd()
+ # Configure logging handler to write the farn log (use an additional handler, exclusively for farn)
+ _configure_additional_logging_handler_exclusively_for_farn(farn_dirs["LOGDIR"])
+
+ # Set up system environment variables for each farn directory
+ # This is necessary to enable shell commands defined in farnDict to point to them with i.e. %TEMPLATEDIR%
+ for key, item in farn_dirs.items():
+ append_system_variable(key, str(item))
+
+ logger.info("Successfully set up farn environment.")
+
+ return farn_dirs
+
+
+def _configure_additional_logging_handler_exclusively_for_farn(log_dir: Path):
+ """Create an additional logging handler exclusively for the farn log.
+
+ Parameters
+ ----------
+ log_dir : Path
+ folder in which the log file will be created
+ """
+ # Create log file
+ log_dir.mkdir(parents=True, exist_ok=True)
+ log_file = log_dir / "farn.log"
+ # Create logging file handler
+ file_handler = logging.FileHandler(str(log_file.absolute()), "a")
+ file_handler.name = str(log_file.absolute())
+ file_handler.setLevel(logging.INFO)
+ file_formatter = logging.Formatter("%(asctime)s %(levelname)-8s %(message)s", "%Y-%m-%d %H:%M:%S")
+ file_handler.setFormatter(file_formatter)
+ # Register file handler at root logger
+ root_logger = logging.getLogger()
+ file_handler_already_exists: bool = any(handler.name == file_handler.name for handler in root_logger.handlers)
+ if not file_handler_already_exists:
+ root_logger.addHandler(file_handler)
+ return
+
+
+def _remove_old_case_list_files(): # sourcery skip: avoid-builtin-shadow
+ """Remove old case list files, if existing."""
+ logger.info("Remove old case list files...")
+
+ lists = [list for list in Path.cwd().rglob("*") if re.search("(path|queue)List", str(list))]
+
+ for list in lists:
+ list = Path(list)
+ list.unlink()
+
+ logger.info("Successfully removed old case list files.")
+
+ return
+
+
+def _sys_call(shell_commands: MutableSequence[str]):
+ """Fallback function until _execute_command is usable under linux."""
+
+ for shell_command in shell_commands:
+ _ = os.system(shell_command)
+
+ return
+
+
+def _execute_shell_commands(shell_commands: MutableSequence[str]):
+ """Execute a sequence of shell commands using subprocess.
+
+ Parameters
+ ----------
+ shell_commands : MutableSequence
+ list with shell commands to be executed
+ """
+
+ # @TODO: until the problem with vanishing '.'s on Linux systems is solved (e.g. in command "ln -s target ."),
+ # reroute the function call to _sys_call instead, as a workaround.
+ if platform.system() == "Linux":
+ _sys_call(shell_commands)
+ return
+
+ for shell_command in shell_commands:
+ _ = execute_in_sub_process(shell_command)
+
+ return