diff --git a/.azure/azure-benchmark-pipeline.yaml b/.azure/azure-benchmark-pipeline.yaml index 1f7b8ebe..78c40bfd 100644 --- a/.azure/azure-benchmark-pipeline.yaml +++ b/.azure/azure-benchmark-pipeline.yaml @@ -11,74 +11,80 @@ schedules: branches: include: - 'refs/heads/dev' - -pool: - vmImage: ubuntu-latest - variables: VERSION: '' MAJOR_VERSION: '' -steps: -- checkout: self - persistCredentials: true +jobs: +- job: PrimAITE_Benchmark + timeoutInMinutes: 360 # 6-hour maximum + pool: + name: 'Imaginary Yak Pool' + workspace: + clean: all -- script: | - VERSION=$(cat src/primaite/VERSION | tr -d '\n') - if [[ "$(Build.SourceBranch)" == "refs/heads/dev" ]]; then - DATE=$(date +%Y%m%d) - echo "${VERSION}+dev.${DATE}" > src/primaite/VERSION - fi - displayName: 'Update VERSION file for Dev Benchmark' + steps: + - checkout: self + persistCredentials: true -- script: | - VERSION=$(cat src/primaite/VERSION | tr -d '\n') - MAJOR_VERSION=$(echo $VERSION | cut -d. -f1) - echo "##vso[task.setvariable variable=VERSION]$VERSION" - echo "##vso[task.setvariable variable=MAJOR_VERSION]$MAJOR_VERSION" - displayName: 'Set Version Variables' + - script: | + python3.10 -m venv venv + displayName: 'Create venv' -- task: UsePythonVersion@0 - inputs: - versionSpec: '3.11' - addToPath: true + - script: | + VERSION=$(cat src/primaite/VERSION | tr -d '\n') + if [[ "$(Build.SourceBranch)" == "refs/heads/dev" ]]; then + DATE=$(date +%Y%m%d) + echo "${VERSION}+dev.${DATE}" > src/primaite/VERSION + fi + displayName: 'Update VERSION file for Dev Benchmark' -- script: | - python -m pip install --upgrade pip - pip install -e .[dev,rl] - primaite setup - displayName: 'Install Dependencies' + - script: | + VERSION=$(cat src/primaite/VERSION | tr -d '\n') + MAJOR_VERSION=$(echo $VERSION | cut -d. -f1) + echo "##vso[task.setvariable variable=VERSION]$VERSION" + echo "##vso[task.setvariable variable=MAJOR_VERSION]$MAJOR_VERSION" + displayName: 'Set Version Variables' -- script: | - cd benchmark - python3 primaite_benchmark.py - cd .. - displayName: 'Run Benchmarking Script' + - script: | + source venv/bin/activate + pip install --upgrade pip + pip install -e .[dev,rl] + primaite setup + displayName: 'Install Dependencies' -- script: | - git config --global user.email "oss@dstl.gov.uk" - git config --global user.name "Defence Science and Technology Laboratory UK" - workingDirectory: $(System.DefaultWorkingDirectory) - displayName: 'Configure Git' - condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release')) + - script: | + set -e + source venv/bin/activate + cd benchmark + python primaite_benchmark.py + cd .. + displayName: 'Run Benchmarking Script' -- script: | - git add benchmark/results/v$(MAJOR_VERSION)/v$(VERSION)/* - git commit -m "Automated benchmark output commit for version $(VERSION)" - git push origin HEAD:refs/heads/$(Build.SourceBranchName) - displayName: 'Commit and Push Benchmark Results' - workingDirectory: $(System.DefaultWorkingDirectory) - env: - GIT_CREDENTIALS: $(System.AccessToken) - condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/heads/release')) + - script: | + git config --global user.email "oss@dstl.gov.uk" + git config --global user.name "Defence Science and Technology Laboratory UK" + workingDirectory: $(System.DefaultWorkingDirectory) + displayName: 'Configure Git' + condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release')) -- script: | - tar czf primaite_v$(VERSION)_benchmark.tar.gz benchmark/results/v$(MAJOR_VERSION)/v$(VERSION) - displayName: 'Prepare Artifacts for Publishing' + - script: | + git add benchmark/results/v$(MAJOR_VERSION)/v$(VERSION)/* + git commit -m "Automated benchmark output commit for version $(VERSION)" + git push origin HEAD:refs/heads/$(Build.SourceBranchName) + displayName: 'Commit and Push Benchmark Results' + workingDirectory: $(System.DefaultWorkingDirectory) + env: + GIT_CREDENTIALS: $(System.AccessToken) + condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/heads/release')) -- task: PublishPipelineArtifact@1 - inputs: - targetPath: primaite_v$(VERSION)_benchmark.tar.gz - artifactName: 'benchmark-output' - publishLocation: 'pipeline' - displayName: 'Publish Benchmark Output as Artifact' + - script: | + tar czf primaite_v$(VERSION)_benchmark.tar.gz benchmark/results/v$(MAJOR_VERSION)/v$(VERSION) + displayName: 'Prepare Artifacts for Publishing' + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: primaite_v$(VERSION)_benchmark.tar.gz + artifactName: 'benchmark-output' + publishLocation: 'pipeline' + displayName: 'Publish Benchmark Output as Artifact' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2db4f3..24ff83ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,31 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added + +- **show_bandwidth_load Function**: Displays current bandwidth load for each frequency in the airspace. +- **Bandwidth Tracking**: Tracks data transmission across each frequency. +- **New Tests**: Added to validate the respect of bandwidth capacities and the correct parsing of airspace configurations from YAML files. +- **New Logging**: Added a new agent behaviour log which are more human friendly than agent history. These Logs are found in session log directory and can be enabled in the I/O settings in a yaml configuration file. + +### Changed + +- **NetworkInterface Speed Type**: The `speed` attribute of `NetworkInterface` has been changed from `int` to `float`. +- **Transmission Feasibility Check**: Updated `_can_transmit` function in `Link` to account for current load and total bandwidth capacity, ensuring transmissions do not exceed limits. +- **Frame Size Details**: Frame `size` attribute now includes both core size and payload size in bytes. +- **Transmission Blocking**: Enhanced `AirSpace` logic to block transmissions that would exceed the available capacity. + +### Fixed + +- **Transmission Permission Logic**: Corrected the logic in `can_transmit_frame` to accurately prevent overloads by checking if the transmission of a frame stays within allowable bandwidth limits after considering current load. + + +[//]: # (This file needs tidying up between 2.0.0 and this line as it hasn't been segmented into 3.0.0 and 3.1.0 and isn't compliant with https://keepachangelog.com/en/1.1.0/) + ## 3.0.0b9 - Removed deprecated `PrimaiteSession` class. - Added ability to set log levels via configuration. @@ -26,8 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactored all air-space usage to that a new instance of AirSpace is created for each instance of Network. This 1:1 relationship between network and airspace will allow parallelization. - Added notebook to demonstrate use of SubprocVecEnv from SB3 to vectorise environments to speed up training. - - ## [Unreleased] - Made requests fail to reach their target if the node is off - Added responses to requests diff --git a/benchmark/results/v3/v3.0.0/v3.0.0_benchmark_metadata.json b/benchmark/results/v3/v3.0.0/v3.0.0_benchmark_metadata.json index b6780eac..ed3ea4eb 100644 --- a/benchmark/results/v3/v3.0.0/v3.0.0_benchmark_metadata.json +++ b/benchmark/results/v3/v3.0.0/v3.0.0_benchmark_metadata.json @@ -26,7 +26,7 @@ "av_s_per_session": 3205.6340542, "av_s_per_step": 0.10017606419375, "av_s_per_100_steps_10_nodes": 10.017606419375, - "combined_av_reward_per_episode": { + "combined_total_reward_per_episode": { "1": -53.42999999999999, "2": -25.18000000000001, "3": -42.00000000000002, diff --git a/benchmark/results/v3/v3.1.0/PrimAITE v3.1.0 Learning Benchmark.pdf b/benchmark/results/v3/v3.1.0/PrimAITE v3.1.0 Learning Benchmark.pdf new file mode 100644 index 00000000..880fd8de Binary files /dev/null and b/benchmark/results/v3/v3.1.0/PrimAITE v3.1.0 Learning Benchmark.pdf differ diff --git a/benchmark/results/v3/v3.1.0/PrimAITE v3.1.0 Learning Benchmark.png b/benchmark/results/v3/v3.1.0/PrimAITE v3.1.0 Learning Benchmark.png new file mode 100644 index 00000000..2771745d Binary files /dev/null and b/benchmark/results/v3/v3.1.0/PrimAITE v3.1.0 Learning Benchmark.png differ diff --git a/benchmark/results/v3/v3.1.0/v3.1.0_benchmark_metadata.json b/benchmark/results/v3/v3.1.0/v3.1.0_benchmark_metadata.json new file mode 100644 index 00000000..fcd2d610 --- /dev/null +++ b/benchmark/results/v3/v3.1.0/v3.1.0_benchmark_metadata.json @@ -0,0 +1,7444 @@ +{ + "start_timestamp": "2024-07-13T13:15:57.915794", + "end_datetime": "2024-07-13T17:23:50.622036", + "primaite_version": "3.1.0", + "system_info": { + "System": { + "OS": "Windows", + "OS Version": "10.0.22631", + "Machine": "AMD64", + "Processor": "Intel64 Family 6 Model 142 Stepping 12, GenuineIntel" + }, + "CPU": { + "Physical Cores": 4, + "Total Cores": 8, + "Max Frequency": "2304.00Mhz" + }, + "Memory": { + "Total": "15.68GB", + "Swap Total": "15.68GB" + }, + "GPU": [] + }, + "total_sessions": 5, + "total_episodes": 5005, + "total_time_steps": 640000, + "av_s_per_session": 2974.2557622, + "av_s_per_step": 0.09294549256875, + "av_s_per_100_steps_10_nodes": 9.294549256875001, + "combined_total_reward_per_episode": { + "1": -50.19000000000003, + "2": -37.08, + "3": -39.42000000000002, + "4": -32.90000000000003, + "5": -29.1, + "6": -21.420000000000005, + "7": -24.229999999999997, + "8": -32.010000000000026, + "9": -41.95000000000006, + "10": -33.46000000000002, + "11": -22.999999999999986, + "12": -27.81000000000002, + "13": -37.719999999999985, + "14": -31.21999999999998, + "15": -35.62000000000003, + "16": -20.88999999999998, + "17": -31.059999999999995, + "18": -39.09000000000004, + "19": -47.99999999999998, + "20": -50.209999999999994, + "21": -31.219999999999978, + "22": -27.05, + "23": -42.66000000000001, + "24": -28.89999999999997, + "25": -30.089999999999964, + "26": -58.25, + "27": -23.779999999999987, + "28": -23.189999999999994, + "29": -35.39000000000003, + "30": -26.810000000000013, + "31": -50.27999999999999, + "32": -14.059999999999999, + "33": -27.82000000000003, + "34": -34.619999999999976, + "35": -19.749999999999986, + "36": -18.659999999999986, + "37": -39.81000000000003, + "38": -21.5, + "39": -52.24000000000002, + "40": -35.65000000000005, + "41": -33.48999999999999, + "42": -36.70999999999999, + "43": -23.019999999999992, + "44": -44.96, + "45": -17.969999999999974, + "46": -29.44999999999998, + "47": -17.76999999999997, + "48": -22.339999999999986, + "49": -36.18999999999999, + "50": -28.01, + "51": -44.8, + "52": -35.59, + "53": -36.69000000000001, + "54": -53.0, + "55": -23.449999999999978, + "56": -32.06999999999999, + "57": -17.409999999999982, + "58": -29.610000000000003, + "59": -13.339999999999984, + "60": -36.94000000000002, + "61": -15.64999999999998, + "62": -21.539999999999992, + "63": -41.82999999999999, + "64": -8.18999999999999, + "65": -33.300000000000004, + "66": -34.66000000000004, + "67": -22.839999999999986, + "68": -23.839999999999996, + "69": -18.14999999999997, + "70": -17.339999999999982, + "71": -56.26000000000001, + "72": -37.179999999999986, + "73": -32.870000000000026, + "74": -15.09999999999997, + "75": -37.679999999999986, + "76": -13.569999999999983, + "77": -11.789999999999981, + "78": -46.869999999999976, + "79": -29.45999999999998, + "80": -39.99, + "81": -22.599999999999977, + "82": -35.64000000000001, + "83": -19.20999999999996, + "84": -10.239999999999995, + "85": -10.409999999999982, + "86": -25.14999999999997, + "87": -13.70000000000002, + "88": -46.37999999999999, + "89": -25.379999999999992, + "90": -43.129999999999974, + "91": -26.359999999999975, + "92": -28.9, + "93": -52.14, + "94": -14.389999999999981, + "95": -17.739999999999995, + "96": -15.079999999999984, + "97": -18.75999999999997, + "98": -10.959999999999985, + "99": -23.189999999999984, + "100": -26.79, + "101": -13.209999999999997, + "102": -15.759999999999994, + "103": -28.50999999999998, + "104": -28.499999999999993, + "105": -50.37000000000003, + "106": -23.359999999999985, + "107": -17.990000000000016, + "108": -13.849999999999998, + "109": -22.820000000000014, + "110": -19.509999999999973, + "111": -21.719999999999978, + "112": -11.379999999999981, + "113": -27.03999999999998, + "114": -16.50000000000003, + "115": 0.5000000000000108, + "116": -38.779999999999994, + "117": -21.609999999999967, + "118": -33.17, + "119": -42.91999999999999, + "120": -6.389999999999989, + "121": -35.57, + "122": -10.479999999999993, + "123": -28.919999999999977, + "124": -29.24999999999998, + "125": -45.599999999999966, + "126": -33.97999999999998, + "127": -6.819999999999976, + "128": -25.44999999999999, + "129": -10.839999999999979, + "130": -7.3699999999999735, + "131": -13.14999999999998, + "132": -5.600000000000014, + "133": -38.18999999999996, + "134": -22.839999999999975, + "135": -28.01999999999996, + "136": -18.46, + "137": -17.769999999999975, + "138": -17.860000000000007, + "139": -24.69, + "140": -19.99999999999998, + "141": -29.120000000000005, + "142": -23.93999999999998, + "143": -13.999999999999996, + "144": -13.179999999999973, + "145": -33.58, + "146": -3.7899999999999983, + "147": -11.520000000000035, + "148": -38.609999999999985, + "149": -7.090000000000018, + "150": 14.609999999999932, + "151": -37.13999999999997, + "152": 3.390000000000023, + "153": -7.829999999999981, + "154": -29.940000000000026, + "155": -36.17999999999999, + "156": -10.690000000000005, + "157": -49.84000000000001, + "158": -11.899999999999986, + "159": -27.940000000000005, + "160": -51.04999999999998, + "161": -49.46999999999998, + "162": -0.6199999999999779, + "163": 2.9800000000000164, + "164": -19.80999999999998, + "165": -34.34999999999999, + "166": -14.520000000000039, + "167": -56.739999999999974, + "168": -37.59, + "169": -11.200000000000017, + "170": -39.030000000000044, + "171": 0.34000000000003683, + "172": -3.690000000000004, + "173": -38.34000000000001, + "174": 0.27999999999997627, + "175": -9.659999999999988, + "176": -21.130000000000052, + "177": -17.179999999999975, + "178": -29.15999999999999, + "179": -19.709999999999972, + "180": -27.849999999999987, + "181": -17.36000000000006, + "182": -30.92000000000001, + "183": -31.849999999999984, + "184": 18.66999999999998, + "185": -14.780000000000005, + "186": -28.70999999999999, + "187": 1.4300000000000146, + "188": -16.180000000000042, + "189": -6.440000000000012, + "190": -7.770000000000017, + "191": 8.599999999999909, + "192": -35.13999999999999, + "193": -8.440000000000008, + "194": -33.03999999999999, + "195": -20.320000000000014, + "196": -22.949999999999992, + "197": -10.810000000000024, + "198": -9.95999999999997, + "199": -14.459999999999997, + "200": 6.80000000000004, + "201": -10.139999999999999, + "202": -29.719999999999978, + "203": 4.839999999999988, + "204": -45.51999999999996, + "205": 9.359999999999967, + "206": -15.060000000000011, + "207": -11.120000000000001, + "208": -10.230000000000018, + "209": -4.810000000000011, + "210": -24.110000000000003, + "211": -10.809999999999965, + "212": -22.969999999999995, + "213": -32.93000000000001, + "214": -44.849999999999966, + "215": -7.409999999999971, + "216": -55.249999999999986, + "217": -11.000000000000039, + "218": -13.789999999999987, + "219": 6.780000000000013, + "220": -36.989999999999995, + "221": 0.05999999999995964, + "222": -23.66999999999999, + "223": 4.489999999999997, + "224": -32.989999999999995, + "225": -30.080000000000005, + "226": -25.13999999999997, + "227": 21.37999999999991, + "228": -25.430000000000042, + "229": -11.67999999999999, + "230": -0.8, + "231": -16.230000000000054, + "232": 19.34999999999995, + "233": -16.190000000000037, + "234": -39.27999999999999, + "235": -32.65999999999999, + "236": -24.819999999999993, + "237": -13.209999999999974, + "238": -12.51, + "239": 14.51, + "240": -11.750000000000018, + "241": -11.279999999999976, + "242": -3.460000000000045, + "243": -1.5800000000000438, + "244": -9.049999999999997, + "245": -10.840000000000021, + "246": -12.060000000000025, + "247": -10.719999999999988, + "248": -4.250000000000009, + "249": -15.509999999999977, + "250": -20.600000000000044, + "251": -16.36000000000002, + "252": 26.319999999999972, + "253": 30.57000000000004, + "254": -66.94000000000001, + "255": -33.81999999999997, + "256": 3.1900000000000075, + "257": -9.36000000000001, + "258": 15.4799999999999, + "259": 25.349999999999987, + "260": -19.860000000000014, + "261": 23.169999999999977, + "262": 10.279999999999967, + "263": 41.609999999999886, + "264": -16.279999999999994, + "265": 19.81999999999998, + "266": 4.179999999999954, + "267": 12.709999999999997, + "268": -5.1600000000000135, + "269": -7.580000000000041, + "270": 0.38999999999994517, + "271": 12.130000000000017, + "272": 17.339999999999982, + "273": -2.52000000000002, + "274": -39.070000000000036, + "275": 6.959999999999923, + "276": -8.35000000000006, + "277": 19.52999999999997, + "278": 48.72, + "279": 27.119999999999926, + "280": 19.370000000000076, + "281": 31.73999999999997, + "282": 26.65999999999993, + "283": -6.270000000000027, + "284": -29.76999999999997, + "285": 17.299999999999923, + "286": -7.230000000000004, + "287": 22.819999999999983, + "288": 8.859999999999992, + "289": 12.009999999999954, + "290": 9.480000000000015, + "291": 28.48999999999996, + "292": 22.72999999999994, + "293": 9.759999999999996, + "294": 16.889999999999993, + "295": 47.449999999999974, + "296": 29.229999999999972, + "297": 33.959999999999965, + "298": 34.949999999999896, + "299": 39.8799999999999, + "300": 22.319999999999915, + "301": 9.599999999999955, + "302": -1.3099999999999739, + "303": 23.930000000000003, + "304": 28.26999999999997, + "305": 21.609999999999946, + "306": 54.81999999999996, + "307": 17.859999999999978, + "308": -0.8900000000000119, + "309": 27.900000000000006, + "310": 15.899999999999991, + "311": 59.06999999999997, + "312": 12.529999999999916, + "313": 33.330000000000005, + "314": 46.78999999999993, + "315": 42.32000000000001, + "316": 39.19999999999994, + "317": 19.399999999999984, + "318": 10.940000000000007, + "319": 4.799999999999926, + "320": 53.97999999999992, + "321": 15.399999999999983, + "322": 33.55999999999995, + "323": 35.370000000000005, + "324": 56.52999999999997, + "325": -19.31000000000006, + "326": 59.559999999999945, + "327": 64.26000000000002, + "328": 61.64, + "329": 31.54000000000003, + "330": 25.21999999999994, + "331": 4.900000000000077, + "332": 43.93, + "333": 58.64000000000003, + "334": 45.10999999999995, + "335": 7.880000000000015, + "336": 21.56000000000001, + "337": 28.059999999999967, + "338": 62.60999999999992, + "339": 35.860000000000014, + "340": 47.01999999999995, + "341": 38.5, + "342": 29.940000000000026, + "343": 38.869999999999955, + "344": 54.18000000000002, + "345": 52.470000000000006, + "346": 77.61999999999992, + "347": 91.96000000000005, + "348": 45.899999999999885, + "349": 61.289999999999964, + "350": 66.13999999999994, + "351": 41.21999999999996, + "352": 60.00999999999999, + "353": 76.99999999999996, + "354": 38.95999999999991, + "355": 54.65999999999991, + "356": 53.92999999999999, + "357": 21.990000000000048, + "358": 44.9, + "359": 2.220000000000013, + "360": 16.209999999999972, + "361": 49.140000000000036, + "362": 77.32999999999998, + "363": 36.98000000000001, + "364": 32.62999999999999, + "365": 18.11999999999994, + "366": 60.84999999999995, + "367": 32.38999999999997, + "368": 40.66999999999997, + "369": 56.98999999999995, + "370": 43.18000000000003, + "371": 67.46999999999996, + "372": 38.04000000000004, + "373": 36.599999999999945, + "374": 56.759999999999955, + "375": 86.07999999999996, + "376": 67.81999999999996, + "377": 43.750000000000036, + "378": 66.75999999999993, + "379": 81.25000000000009, + "380": 35.030000000000015, + "381": 84.4399999999999, + "382": 46.96999999999992, + "383": 52.759999999999934, + "384": 42.72000000000001, + "385": 72.69999999999999, + "386": 20.10000000000006, + "387": 61.92999999999988, + "388": 40.89999999999995, + "389": 50.309999999999924, + "390": 83.08999999999997, + "391": 45.04999999999994, + "392": 92.0100000000001, + "393": 72.51999999999995, + "394": 82.71000000000001, + "395": 67.56, + "396": 43.689999999999955, + "397": 64.49999999999994, + "398": 56.60000000000006, + "399": 87.21, + "400": 69.86999999999998, + "401": 73.04000000000009, + "402": 77.73999999999997, + "403": 70.53999999999996, + "404": 38.960000000000065, + "405": 53.639999999999944, + "406": 36.68999999999998, + "407": 70.75000000000003, + "408": 32.63999999999994, + "409": 82.27999999999992, + "410": 83.77000000000002, + "411": 80.82999999999984, + "412": 50.33, + "413": 56.04000000000004, + "414": 29.680000000000085, + "415": 43.84, + "416": 66.66000000000004, + "417": 68.80999999999997, + "418": 67.46999999999997, + "419": 40.41999999999993, + "420": 69.02000000000001, + "421": 79.35999999999993, + "422": 64.07, + "423": 41.07000000000001, + "424": 84.76999999999997, + "425": 80.94999999999993, + "426": 70.64000000000007, + "427": 53.80999999999997, + "428": 72.94999999999996, + "429": 94.79999999999998, + "430": 106.06999999999987, + "431": 98.68999999999994, + "432": 101.55000000000007, + "433": 96.01000000000008, + "434": 83.60999999999999, + "435": 68.25999999999996, + "436": 70.89, + "437": 97.38999999999993, + "438": 70.83000000000001, + "439": 90.90999999999994, + "440": 106.93999999999998, + "441": 94.95999999999998, + "442": 101.21000000000004, + "443": 76.48999999999998, + "444": 103.21000000000011, + "445": 79.66999999999989, + "446": 63.64000000000003, + "447": 83.44999999999993, + "448": 49.78999999999999, + "449": 90.65999999999997, + "450": 61.13000000000005, + "451": 100.85000000000002, + "452": 60.54, + "453": 85.21999999999998, + "454": 87.20000000000007, + "455": 93.86, + "456": 60.91000000000001, + "457": 89.71000000000008, + "458": 84.68000000000004, + "459": 99.51999999999997, + "460": 77.16, + "461": 92.71999999999989, + "462": 96.67999999999998, + "463": 93.40000000000006, + "464": 79.86000000000004, + "465": 103.88999999999987, + "466": 54.62000000000008, + "467": 54.690000000000055, + "468": 92.58999999999992, + "469": 59.19999999999995, + "470": 63.47999999999996, + "471": 78.86999999999989, + "472": 60.120000000000026, + "473": 85.85999999999999, + "474": 98.70000000000007, + "475": 67.90999999999994, + "476": 66.67000000000007, + "477": 102.28000000000003, + "478": 104.27000000000002, + "479": 104.14000000000003, + "480": 97.9599999999999, + "481": 92.68999999999998, + "482": 92.87999999999995, + "483": 89.50999999999995, + "484": 85.83999999999995, + "485": 83.54999999999998, + "486": 102.29000000000003, + "487": 99.46999999999991, + "488": 100.30999999999997, + "489": 84.05999999999993, + "490": 76.88999999999999, + "491": 106.40000000000006, + "492": 93.4799999999999, + "493": 103.57999999999997, + "494": 88.19999999999992, + "495": 92.16999999999987, + "496": 88.89999999999995, + "497": 104.15999999999994, + "498": 98.28, + "499": 107.52999999999997, + "500": 78.07000000000001, + "501": 103.88000000000002, + "502": 99.37000000000003, + "503": 94.39999999999998, + "504": 108.35999999999999, + "505": 100.99, + "506": 78.42, + "507": 93.80999999999997, + "508": 108.90000000000002, + "509": 89.64999999999998, + "510": 108.55999999999999, + "511": 81.81999999999996, + "512": 90.56999999999995, + "513": 94.83000000000004, + "514": 99.62000000000002, + "515": 110.9200000000001, + "516": 104.24000000000005, + "517": 110.09000000000007, + "518": 83.50999999999999, + "519": 99.98000000000002, + "520": 108.48999999999985, + "521": 105.34999999999994, + "522": 96.30999999999997, + "523": 106.87999999999997, + "524": 104.03999999999996, + "525": 98.23999999999998, + "526": 106.78000000000002, + "527": 75.90999999999995, + "528": 104.52999999999993, + "529": 101.9799999999999, + "530": 99.07999999999991, + "531": 102.55999999999995, + "532": 107.69999999999993, + "533": 92.12999999999997, + "534": 110.20000000000002, + "535": 93.24999999999994, + "536": 100.79999999999991, + "537": 102.19000000000005, + "538": 91.01000000000003, + "539": 104.14000000000003, + "540": 104.32000000000001, + "541": 75.87999999999997, + "542": 72.13999999999994, + "543": 102.33000000000001, + "544": 102.02999999999993, + "545": 93.53999999999989, + "546": 97.08999999999997, + "547": 95.48, + "548": 95.15999999999988, + "549": 97.62999999999991, + "550": 94.71999999999998, + "551": 96.79, + "552": 109.08999999999999, + "553": 99.15999999999995, + "554": 105.43999999999997, + "555": 99.43000000000002, + "556": 99.44999999999996, + "557": 102.26999999999994, + "558": 108.96999999999998, + "559": 102.85999999999999, + "560": 101.80999999999997, + "561": 99.13000000000004, + "562": 96.28999999999981, + "563": 97.94000000000004, + "564": 110.34, + "565": 106.42999999999998, + "566": 106.78000000000002, + "567": 111.13000000000014, + "568": 104.49999999999997, + "569": 108.21999999999996, + "570": 78.12999999999991, + "571": 101.58, + "572": 100.57000000000001, + "573": 74.57999999999993, + "574": 50.72000000000006, + "575": 109.27999999999997, + "576": 86.68000000000006, + "577": 109.28999999999996, + "578": 86.58000000000004, + "579": 98.42000000000004, + "580": 83.32999999999996, + "581": 76.26999999999997, + "582": 108.31999999999998, + "583": 99.02000000000008, + "584": 99.43999999999997, + "585": 93.69999999999999, + "586": 92.58999999999996, + "587": 99.98000000000005, + "588": 98.03999999999992, + "589": 82.60000000000002, + "590": 105.47999999999999, + "591": 80.39999999999989, + "592": 93.51999999999992, + "593": 69.4299999999999, + "594": 76.12999999999988, + "595": 80.21000000000005, + "596": 73.21000000000001, + "597": 88.74999999999993, + "598": 80.70999999999997, + "599": 90.69999999999997, + "600": 81.02999999999993, + "601": 60.57999999999991, + "602": 97.92999999999994, + "603": 73.92999999999998, + "604": 83.46000000000008, + "605": 82.54999999999995, + "606": 85.26999999999998, + "607": 69.3299999999999, + "608": 60.17999999999999, + "609": 87.09999999999994, + "610": 106.67000000000007, + "611": 93.97999999999988, + "612": 73.37999999999998, + "613": 64.58999999999993, + "614": 80.5599999999999, + "615": 99.38000000000002, + "616": 88.98999999999997, + "617": 75.57999999999991, + "618": 76.00000000000006, + "619": 58.75999999999999, + "620": 63.21999999999993, + "621": 62.07999999999995, + "622": 70.83999999999997, + "623": 60.44000000000003, + "624": 86.45000000000002, + "625": 69.58999999999997, + "626": 82.05999999999995, + "627": 77.79, + "628": 65.22999999999998, + "629": 71.94999999999995, + "630": 65.83000000000001, + "631": 70.28999999999996, + "632": 62.65, + "633": 72.90999999999991, + "634": 83.25999999999996, + "635": 64.75999999999996, + "636": 52.02000000000005, + "637": 73.73000000000006, + "638": 75.53999999999994, + "639": 86.05999999999997, + "640": 63.08, + "641": 84.82000000000001, + "642": 64.18999999999993, + "643": 60.64999999999999, + "644": 71.23999999999992, + "645": 58.85000000000002, + "646": 78.13999999999994, + "647": 88.07999999999994, + "648": 85.05999999999999, + "649": 80.97999999999999, + "650": 59.919999999999945, + "651": 66.49999999999987, + "652": 74.45, + "653": 66.74000000000012, + "654": 93.63999999999997, + "655": 89.36999999999995, + "656": 88.47, + "657": 93.96000000000001, + "658": 80.02999999999999, + "659": 71.99000000000002, + "660": 70.56000000000003, + "661": 102.16999999999999, + "662": 82.48999999999995, + "663": 81.85999999999999, + "664": 67.88999999999999, + "665": 78.07999999999998, + "666": 51.479999999999926, + "667": 74.87000000000008, + "668": 82.04, + "669": 56.53999999999992, + "670": 96.64000000000003, + "671": 90.94999999999995, + "672": 90.45, + "673": 66.54000000000005, + "674": 84.67, + "675": 107.12999999999992, + "676": 98.6700000000001, + "677": 92.9500000000001, + "678": 76.47000000000003, + "679": 80.52999999999997, + "680": 70.38000000000004, + "681": 84.00000000000011, + "682": 104.79000000000008, + "683": 99.92999999999998, + "684": 94.68999999999998, + "685": 99.95000000000002, + "686": 92.61000000000006, + "687": 82.13999999999996, + "688": 101.60999999999994, + "689": 99.07000000000006, + "690": 84.57999999999996, + "691": 95.8600000000001, + "692": 88.60000000000002, + "693": 83.73000000000008, + "694": 73.83, + "695": 108.27000000000001, + "696": 100.87000000000008, + "697": 105.27000000000012, + "698": 96.77000000000001, + "699": 89.7300000000001, + "700": 93.39000000000001, + "701": 94.54, + "702": 88.72000000000003, + "703": 84.92000000000009, + "704": 106.47999999999999, + "705": 95.74999999999997, + "706": 83.50999999999996, + "707": 90.57000000000004, + "708": 100.48999999999994, + "709": 103.41000000000001, + "710": 91.1600000000001, + "711": 104.74000000000005, + "712": 91.21000000000001, + "713": 79.26, + "714": 105.12000000000003, + "715": 86.2300000000001, + "716": 98.3600000000001, + "717": 92.7900000000001, + "718": 95.51000000000009, + "719": 109.90000000000013, + "720": 104.70000000000009, + "721": 87.9900000000001, + "722": 89.44000000000005, + "723": 101.9599999999999, + "724": 102.55999999999999, + "725": 83.79000000000005, + "726": 79.24999999999997, + "727": 107.15000000000005, + "728": 80.05000000000008, + "729": 107.49000000000017, + "730": 96.63, + "731": 105.88000000000008, + "732": 107.70000000000009, + "733": 89.3500000000001, + "734": 83.90000000000013, + "735": 82.75, + "736": 99.63000000000002, + "737": 104.5400000000001, + "738": 105.10999999999999, + "739": 95.79000000000009, + "740": 106.92000000000007, + "741": 97.0600000000001, + "742": 101.53000000000007, + "743": 103.48000000000002, + "744": 85.27000000000007, + "745": 75.59999999999998, + "746": 105.17000000000012, + "747": 100.48000000000002, + "748": 83.61000000000008, + "749": 107.25000000000004, + "750": 56.840000000000074, + "751": 99.72999999999995, + "752": 72.87000000000005, + "753": 87.5700000000001, + "754": 108.24000000000001, + "755": 107.35999999999999, + "756": 99.72000000000004, + "757": 100.24, + "758": 112.08000000000011, + "759": 94.98, + "760": 107.14000000000007, + "761": 110.20000000000009, + "762": 104.11000000000004, + "763": 109.82000000000014, + "764": 87.53000000000013, + "765": 85.35000000000001, + "766": 106.84000000000006, + "767": 110.1900000000001, + "768": 68.91, + "769": 103.63999999999999, + "770": 86.44000000000013, + "771": 94.47999999999999, + "772": 93.81000000000004, + "773": 114.15000000000018, + "774": 106.29000000000005, + "775": 110.21000000000008, + "776": 104.8500000000001, + "777": 106.29000000000008, + "778": 94.86000000000006, + "779": 97.29000000000005, + "780": 100.36000000000006, + "781": 96.86000000000007, + "782": 115.45000000000009, + "783": 105.95000000000002, + "784": 91.35000000000002, + "785": 96.51000000000003, + "786": 108.47000000000006, + "787": 112.44000000000005, + "788": 113.40000000000009, + "789": 106.47999999999999, + "790": 110.37000000000009, + "791": 71.37000000000006, + "792": 99.78000000000009, + "793": 56.160000000000046, + "794": 96.18999999999998, + "795": 95.39000000000003, + "796": 99.30000000000004, + "797": 112.14000000000006, + "798": 104.70000000000002, + "799": 91.49, + "800": 106.93999999999997, + "801": 84.84999999999998, + "802": 76.02000000000002, + "803": 106.90999999999993, + "804": 101.50000000000009, + "805": 95.89000000000003, + "806": 93.28, + "807": 82.80000000000003, + "808": 94.57000000000002, + "809": 106.33999999999999, + "810": 87.66000000000011, + "811": 97.2, + "812": 98.35, + "813": 111.22000000000011, + "814": 79.20000000000009, + "815": 96.70000000000009, + "816": 109.2300000000001, + "817": 107.96000000000001, + "818": 106.08000000000011, + "819": 78.23, + "820": 109.59000000000003, + "821": 112.76000000000013, + "822": 105.44000000000005, + "823": 106.04000000000005, + "824": 107.45000000000012, + "825": 85.59999999999992, + "826": 86.8499999999999, + "827": 102.86000000000008, + "828": 96.86000000000003, + "829": 92.24999999999997, + "830": 96.45, + "831": 104.06000000000006, + "832": 98.92000000000003, + "833": 98.90000000000009, + "834": 106.53000000000013, + "835": 85.58999999999997, + "836": 106.08000000000004, + "837": 104.14000000000013, + "838": 88.97000000000011, + "839": 102.71000000000004, + "840": 108.82000000000002, + "841": 108.32000000000012, + "842": 104.55000000000004, + "843": 100.3700000000001, + "844": 108.65000000000013, + "845": 109.07000000000008, + "846": 105.22000000000011, + "847": 112.66000000000017, + "848": 106.72000000000007, + "849": 108.38999999999999, + "850": 101.06000000000003, + "851": 97.02000000000007, + "852": 93.6700000000001, + "853": 109.68000000000013, + "854": 106.59000000000007, + "855": 100.99000000000007, + "856": 109.50000000000007, + "857": 92.62999999999997, + "858": 107.44000000000005, + "859": 108.06999999999998, + "860": 100.76000000000013, + "861": 95.26000000000003, + "862": 101.02, + "863": 88.91000000000003, + "864": 112.1000000000001, + "865": 65.08000000000001, + "866": 112.20000000000009, + "867": 101.91000000000001, + "868": 111.21000000000008, + "869": 106.36000000000006, + "870": 110.57000000000005, + "871": 110.53000000000016, + "872": 63.220000000000084, + "873": 108.88000000000015, + "874": 107.58000000000007, + "875": 100.64000000000004, + "876": 109.90000000000006, + "877": 112.52000000000018, + "878": 108.60000000000007, + "879": 110.27000000000002, + "880": 89.95000000000003, + "881": 81.24000000000001, + "882": 105.15000000000006, + "883": 107.81000000000013, + "884": 65.77, + "885": 108.19000000000008, + "886": 113.3400000000001, + "887": 102.62000000000005, + "888": 112.37000000000012, + "889": 102.68000000000004, + "890": 105.24000000000012, + "891": 106.27000000000002, + "892": 97.48000000000005, + "893": 105.95000000000005, + "894": 99.50000000000001, + "895": 108.3500000000001, + "896": 109.97000000000011, + "897": 98.68000000000004, + "898": 108.95000000000009, + "899": 107.53000000000004, + "900": 110.14000000000013, + "901": 104.47, + "902": 111.57000000000009, + "903": 99.88000000000002, + "904": 109.98000000000006, + "905": 111.31000000000009, + "906": 109.68000000000013, + "907": 98.51000000000003, + "908": 79.69000000000003, + "909": 98.15, + "910": 92.56000000000002, + "911": 96.29000000000005, + "912": 102.84000000000003, + "913": 95.44, + "914": 112.39000000000013, + "915": 101.91000000000001, + "916": 88.87000000000003, + "917": 110.44000000000013, + "918": 102.96000000000008, + "919": 107.64000000000003, + "920": 102.58000000000011, + "921": 102.22000000000006, + "922": 111.8600000000001, + "923": 104.65000000000006, + "924": 111.16999999999999, + "925": 99.11000000000013, + "926": 107.14999999999998, + "927": 104.17000000000003, + "928": 110.23000000000006, + "929": 99.83000000000001, + "930": 105.15000000000009, + "931": 111.58000000000001, + "932": 108.78000000000011, + "933": 101.24999999999997, + "934": 109.0600000000001, + "935": 69.79000000000005, + "936": 101.37000000000003, + "937": 108.06000000000009, + "938": 96.2400000000001, + "939": 107.15000000000009, + "940": 107.59000000000003, + "941": 100.72, + "942": 91.73000000000005, + "943": 104.58000000000004, + "944": 109.4900000000001, + "945": 104.87000000000003, + "946": 98.48000000000012, + "947": 107.53000000000011, + "948": 108.7400000000001, + "949": 96.33000000000007, + "950": 109.82000000000009, + "951": 99.71000000000004, + "952": 100.64000000000011, + "953": 107.84000000000012, + "954": 104.81000000000009, + "955": 109.61000000000013, + "956": 107.86000000000008, + "957": 102.35000000000005, + "958": 107.70000000000013, + "959": 100.60000000000001, + "960": 110.30000000000003, + "961": 110.25000000000004, + "962": 87.39000000000007, + "963": 109.01000000000003, + "964": 113.37000000000012, + "965": 101.08000000000001, + "966": 108.4800000000001, + "967": 103.98000000000002, + "968": 102.89000000000001, + "969": 101.12000000000009, + "970": 108.16000000000012, + "971": 104.08000000000001, + "972": 111.71000000000006, + "973": 72.61000000000004, + "974": 104.61999999999998, + "975": 105.24000000000005, + "976": 100.28000000000003, + "977": 82.39999999999998, + "978": 111.73000000000013, + "979": 111.9900000000001, + "980": 108.42000000000007, + "981": 99.59000000000007, + "982": 104.66000000000012, + "983": 107.31000000000006, + "984": 107.61000000000008, + "985": 110.38000000000007, + "986": 111.51000000000008, + "987": 107.72000000000006, + "988": 110.40000000000006, + "989": 108.0900000000001, + "990": 110.33000000000011, + "991": 100.7100000000001, + "992": 105.92000000000007, + "993": 98.41000000000001, + "994": 100.67999999999998, + "995": 104.59000000000006, + "996": 106.10000000000007, + "997": 107.17000000000007, + "998": 110.26000000000006, + "999": 99.21, + "1000": 107.41999999999999 + }, + "session_av_reward_per_episode": { + "1": { + "1": -11.54999999999999, + "2": -21.849999999999955, + "3": -34.29999999999997, + "4": -19.199999999999964, + "5": -40.300000000000075, + "6": -36.800000000000026, + "7": -28.300000000000008, + "8": -64.4000000000001, + "9": -52.80000000000019, + "10": -60.25000000000009, + "11": -22.249999999999954, + "12": -19.299999999999965, + "13": -22.99999999999995, + "14": -89.85000000000001, + "15": -10.500000000000004, + "16": -36.90000000000003, + "17": -35.85000000000003, + "18": -19.199999999999978, + "19": -21.499999999999957, + "20": -40.350000000000115, + "21": -75.8499999999999, + "22": -90.3, + "23": -96.15, + "24": -16.999999999999986, + "25": -16.699999999999967, + "26": -23.55000000000001, + "27": -14.199999999999983, + "28": -44.10000000000005, + "29": -99.85000000000005, + "30": -15.849999999999975, + "31": -103.45, + "32": -13.04999999999998, + "33": -44.850000000000136, + "34": -20.799999999999976, + "35": -18.749999999999968, + "36": -21.54999999999995, + "37": -35.95000000000001, + "38": -16.94999999999997, + "39": -71.55000000000004, + "40": -19.34999999999997, + "41": -33.19999999999998, + "42": -80.95, + "43": -15.64999999999997, + "44": -22.85, + "45": -16.79999999999998, + "46": -78.55000000000004, + "47": -13.399999999999979, + "48": -10.749999999999986, + "49": -20.24999999999996, + "50": -94.25000000000009, + "51": -56.900000000000034, + "52": -21.249999999999957, + "53": -19.799999999999965, + "54": -19.299999999999965, + "55": -16.999999999999964, + "56": -11.599999999999985, + "57": -35.15000000000002, + "58": -59.8000000000001, + "59": -8.650000000000002, + "60": -49.95000000000017, + "61": -17.54999999999997, + "62": -41.90000000000005, + "63": -2.099999999999989, + "64": -6.850000000000007, + "65": -45.599999999999994, + "66": -15.449999999999973, + "67": -22.449999999999992, + "68": -22.449999999999953, + "69": -22.299999999999955, + "70": -20.649999999999956, + "71": -9.59999999999999, + "72": -20.44999999999996, + "73": -49.10000000000011, + "74": -20.74999999999997, + "75": -18.499999999999964, + "76": -8.099999999999987, + "77": -19.199999999999967, + "78": -13.349999999999968, + "79": -30.900000000000013, + "80": -12.999999999999986, + "81": -13.949999999999987, + "82": -11.449999999999985, + "83": -19.899999999999963, + "84": -8.949999999999987, + "85": -14.39999999999998, + "86": -27.59999999999997, + "87": -62.599999999999994, + "88": -93.95, + "89": -96.7, + "90": -76.1, + "91": -8.199999999999982, + "92": -23.19999999999995, + "93": -20.14999999999996, + "94": -20.69999999999996, + "95": -21.09999999999996, + "96": -20.79999999999996, + "97": -19.249999999999968, + "98": -16.74999999999997, + "99": -9.749999999999995, + "100": -22.499999999999954, + "101": 2.40000000000002, + "102": 2.3499999999999615, + "103": -10.749999999999991, + "104": -29.750000000000007, + "105": -59.75000000000014, + "106": -86.25, + "107": -1.2499999999999865, + "108": -18.099999999999966, + "109": -48.0500000000001, + "110": -19.999999999999964, + "111": -16.449999999999974, + "112": -18.199999999999967, + "113": -18.70000000000001, + "114": 13.049999999999937, + "115": -15.599999999999973, + "116": -7.950000000000006, + "117": -20.64999999999996, + "118": -60.80000000000007, + "119": -15.799999999999969, + "120": 8.950000000000006, + "121": 7.800000000000014, + "122": 1.0500000000000098, + "123": -12.89999999999997, + "124": -79.15, + "125": -76.85, + "126": -5.199999999999989, + "127": -23.949999999999964, + "128": -14.099999999999984, + "129": 16.60000000000001, + "130": 0.8500000000000276, + "131": -11.499999999999993, + "132": -7.8000000000000025, + "133": -19.949999999999964, + "134": -77.99999999999997, + "135": -16.449999999999967, + "136": 1.4500000000000257, + "137": -16.34999999999997, + "138": -23.29999999999995, + "139": -14.649999999999977, + "140": 2.800000000000015, + "141": -10.399999999999993, + "142": -27.599999999999945, + "143": -30.399999999999967, + "144": -19.849999999999948, + "145": -4.4499999999999815, + "146": -15.549999999999976, + "147": -54.20000000000001, + "148": 6.650000000000046, + "149": 16.89999999999998, + "150": 8.550000000000026, + "151": -7.999999999999995, + "152": 6.650000000000028, + "153": 1.7000000000000353, + "154": -0.6499999999999777, + "155": -17.399999999999977, + "156": -49.950000000000045, + "157": -10.499999999999995, + "158": 12.700000000000035, + "159": -94.54999999999998, + "160": -27.99999999999995, + "161": -103.69999999999999, + "162": -1.1999999999999869, + "163": 9.850000000000001, + "164": -11.399999999999993, + "165": -13.499999999999988, + "166": -81.5, + "167": -86.14999999999999, + "168": -25.800000000000008, + "169": -21.899999999999956, + "170": -96.19999999999996, + "171": 28.600000000000076, + "172": -6.199999999999996, + "173": 20.800000000000008, + "174": -13.799999999999985, + "175": -20.249999999999964, + "176": -28.49999999999998, + "177": -15.099999999999968, + "178": -16.849999999999977, + "179": -21.199999999999957, + "180": -1.799999999999969, + "181": 20.400000000000063, + "182": 0.4500000000000153, + "183": -5.599999999999983, + "184": 13.70000000000007, + "185": 41.59999999999995, + "186": -4.349999999999988, + "187": 0.8500000000000101, + "188": -4.399999999999994, + "189": 10.900000000000043, + "190": -9.5, + "191": 31.899999999999746, + "192": 19.60000000000008, + "193": -17.04999999999999, + "194": 14.350000000000017, + "195": -8.50000000000001, + "196": 12.350000000000026, + "197": 18.800000000000036, + "198": 30.35000000000007, + "199": -1.3999999999999637, + "200": -19.799999999999997, + "201": 6.950000000000027, + "202": -25.04999999999999, + "203": 5.000000000000051, + "204": -77.09999999999998, + "205": -34.400000000000034, + "206": 4.050000000000061, + "207": 37.899999999999956, + "208": -67.00000000000003, + "209": 19.650000000000073, + "210": 6.800000000000031, + "211": 14.40000000000008, + "212": -19.34999999999997, + "213": 11.450000000000003, + "214": -26.499999999999957, + "215": 6.300000000000046, + "216": -38.35000000000004, + "217": 18.000000000000078, + "218": -7.499999999999994, + "219": 18.80000000000003, + "220": 22.450000000000024, + "221": 19.200000000000053, + "222": 9.050000000000061, + "223": 20.650000000000066, + "224": -5.549999999999993, + "225": 31.29999999999996, + "226": -18.849999999999962, + "227": 32.249999999999986, + "228": -34.10000000000001, + "229": 9.100000000000005, + "230": 2.1500000000000146, + "231": -42.65000000000009, + "232": 49.04999999999976, + "233": 21.099999999999977, + "234": -1.7999999999999867, + "235": -5.699999999999985, + "236": -54.15000000000003, + "237": 10.550000000000072, + "238": -0.6999999999999889, + "239": 26.700000000000077, + "240": 10.999999999999998, + "241": -9.600000000000005, + "242": 19.099999999999998, + "243": 1.6000000000000463, + "244": 1.100000000000019, + "245": -19.699999999999978, + "246": 19.199999999999985, + "247": 5.50000000000003, + "248": 0.8499999999999992, + "249": -69.84999999999998, + "250": -33.35000000000008, + "251": -54.1000000000001, + "252": 51.949999999999974, + "253": 31.200000000000053, + "254": -79.05, + "255": 6.050000000000049, + "256": 17.800000000000075, + "257": 8.950000000000063, + "258": 45.849999999999866, + "259": 21.05000000000004, + "260": 9.999999999999957, + "261": 27.049999999999994, + "262": 21.650000000000038, + "263": 0.45000000000000995, + "264": 14.650000000000045, + "265": 5.9500000000000055, + "266": 4.199999999999993, + "267": 38.39999999999983, + "268": 15.700000000000017, + "269": -22.399999999999977, + "270": -52.050000000000104, + "271": 49.89999999999983, + "272": 11.400000000000023, + "273": 57.29999999999981, + "274": 38.6999999999999, + "275": 41.44999999999997, + "276": 20.550000000000065, + "277": 55.04999999999984, + "278": 2.550000000000031, + "279": 50.349999999999746, + "280": 33.05000000000005, + "281": 80.45, + "282": 7.1000000000000005, + "283": 26.650000000000052, + "284": 6.850000000000049, + "285": 32.34999999999997, + "286": -35.65000000000002, + "287": 24.850000000000005, + "288": 49.59999999999978, + "289": 82.34999999999977, + "290": 18.70000000000005, + "291": -4.2000000000000055, + "292": -41.50000000000013, + "293": 21.15000000000001, + "294": -12.499999999999998, + "295": 60.09999999999976, + "296": -20.25, + "297": 52.64999999999983, + "298": 51.999999999999744, + "299": -2.6999999999999873, + "300": 61.79999999999981, + "301": 72.2499999999998, + "302": 44.999999999999964, + "303": 74.69999999999999, + "304": 61.29999999999977, + "305": 44.299999999999855, + "306": 32.499999999999964, + "307": 51.25000000000005, + "308": -18.400000000000016, + "309": 105.6000000000001, + "310": 80.80000000000008, + "311": 77.64999999999979, + "312": 38.799999999999805, + "313": 82.70000000000002, + "314": 62.79999999999974, + "315": 39.84999999999993, + "316": -82.29999999999998, + "317": -16.049999999999972, + "318": 67.59999999999994, + "319": 70.14999999999984, + "320": 80.79999999999978, + "321": 57.94999999999992, + "322": 72.69999999999995, + "323": 100.30000000000003, + "324": 36.449999999999996, + "325": 69.29999999999986, + "326": 93.6500000000001, + "327": 91.00000000000016, + "328": 87.64999999999995, + "329": 99.69999999999996, + "330": 22.549999999999926, + "331": -46.89999999999998, + "332": 98.35000000000011, + "333": 61.2999999999999, + "334": 89.90000000000003, + "335": 92.95000000000007, + "336": 54.849999999999845, + "337": 48.999999999999964, + "338": 60.499999999999815, + "339": 110.90000000000008, + "340": 84.69999999999999, + "341": 3.900000000000027, + "342": 107.10000000000015, + "343": 74.59999999999987, + "344": 99.30000000000007, + "345": 102.55000000000018, + "346": 70.69999999999987, + "347": 110.65000000000016, + "348": 44.89999999999975, + "349": 34.04999999999978, + "350": 100.04999999999997, + "351": 58.599999999999824, + "352": 90.75000000000003, + "353": 106.64999999999978, + "354": 37.94999999999972, + "355": 71.14999999999975, + "356": 87.74999999999994, + "357": 101.80000000000018, + "358": 98.29999999999978, + "359": 117.90000000000006, + "360": 69.54999999999986, + "361": 35.89999999999984, + "362": 101.29999999999998, + "363": 105.54999999999995, + "364": 96.30000000000014, + "365": 50.1499999999998, + "366": 40.0, + "367": 103.34999999999977, + "368": 90.99999999999974, + "369": 87.99999999999999, + "370": 115.60000000000025, + "371": 79.09999999999992, + "372": 112.10000000000018, + "373": 49.09999999999975, + "374": 84.60000000000007, + "375": 108.80000000000004, + "376": 69.64999999999979, + "377": 115.10000000000024, + "378": 64.7499999999999, + "379": 83.0, + "380": -11.149999999999991, + "381": 67.99999999999986, + "382": 82.79999999999981, + "383": 102.39999999999978, + "384": 47.99999999999983, + "385": 83.64999999999984, + "386": 96.10000000000004, + "387": -80.55000000000005, + "388": 72.0999999999999, + "389": 85.60000000000007, + "390": 112.75000000000007, + "391": 56.04999999999982, + "392": 99.10000000000015, + "393": 101.24999999999994, + "394": 112.6000000000002, + "395": 98.7, + "396": 100.79999999999987, + "397": 84.0000000000001, + "398": 111.74999999999994, + "399": 72.24999999999999, + "400": 108.00000000000013, + "401": 111.20000000000012, + "402": 107.39999999999979, + "403": 108.20000000000019, + "404": 109.35000000000008, + "405": 88.65000000000009, + "406": 106.14999999999975, + "407": 112.25000000000016, + "408": 76.14999999999986, + "409": 48.64999999999982, + "410": 105.70000000000019, + "411": 82.14999999999982, + "412": 90.79999999999998, + "413": 114.30000000000004, + "414": -0.39999999999998037, + "415": 84.64999999999999, + "416": 94.45000000000017, + "417": 112.80000000000008, + "418": 90.59999999999998, + "419": 92.04999999999974, + "420": 103.80000000000017, + "421": 102.64999999999989, + "422": -3.250000000000009, + "423": 108.60000000000016, + "424": 109.89999999999989, + "425": 111.80000000000003, + "426": 112.50000000000011, + "427": 22.049999999999976, + "428": 111.50000000000003, + "429": 109.65000000000026, + "430": 99.74999999999979, + "431": 110.65000000000009, + "432": 109.99999999999989, + "433": 94.70000000000009, + "434": 112.30000000000017, + "435": 110.50000000000006, + "436": 111.75000000000014, + "437": 101.05000000000003, + "438": 107.3500000000002, + "439": 107.3499999999999, + "440": 112.85000000000015, + "441": 106.15000000000006, + "442": 80.29999999999995, + "443": 100.3000000000001, + "444": 115.5500000000003, + "445": 110.29999999999977, + "446": 100.7, + "447": 90.50000000000011, + "448": 93.8500000000001, + "449": 82.09999999999995, + "450": 111.0500000000002, + "451": 103.95000000000013, + "452": 106.59999999999998, + "453": 104.30000000000007, + "454": 110.8000000000002, + "455": 112.8500000000001, + "456": 102.64999999999979, + "457": 104.70000000000014, + "458": 92.34999999999988, + "459": 99.80000000000003, + "460": 113.85000000000002, + "461": 106.0499999999999, + "462": 111.8500000000001, + "463": 106.40000000000012, + "464": 110.30000000000004, + "465": 105.29999999999976, + "466": 104.90000000000013, + "467": 41.599999999999945, + "468": 87.1499999999999, + "469": 108.44999999999987, + "470": 91.75000000000011, + "471": 113.10000000000008, + "472": 105.25000000000013, + "473": 106.25000000000009, + "474": 107.75000000000013, + "475": 57.09999999999995, + "476": 111.45000000000024, + "477": 103.70000000000009, + "478": 114.3000000000001, + "479": 102.3000000000001, + "480": 100.49999999999974, + "481": 47.09999999999997, + "482": 112.00000000000016, + "483": 118.6500000000002, + "484": 83.45, + "485": 115.45000000000024, + "486": 107.25000000000013, + "487": 107.85000000000011, + "488": 109.14999999999989, + "489": 98.89999999999988, + "490": 29.699999999999832, + "491": 116.25000000000027, + "492": 90.44999999999993, + "493": 100.94999999999996, + "494": 94.10000000000007, + "495": 89.04999999999993, + "496": 112.30000000000004, + "497": 116.35000000000029, + "498": 93.10000000000001, + "499": 106.70000000000012, + "500": 92.45000000000009, + "501": 108.25000000000006, + "502": 112.25000000000017, + "503": 109.10000000000025, + "504": 113.95000000000012, + "505": 101.30000000000011, + "506": 100.49999999999982, + "507": 111.29999999999997, + "508": 107.75000000000007, + "509": 84.75000000000004, + "510": 113.05000000000013, + "511": 104.8, + "512": 108.99999999999983, + "513": 115.85000000000026, + "514": 82.64999999999993, + "515": 111.50000000000017, + "516": 108.45000000000009, + "517": 118.00000000000026, + "518": 109.50000000000006, + "519": 115.20000000000024, + "520": 110.29999999999988, + "521": 110.55000000000015, + "522": 105.7999999999998, + "523": 113.75000000000018, + "524": 98.10000000000002, + "525": 108.10000000000005, + "526": 99.60000000000011, + "527": 88.50000000000007, + "528": 87.39999999999999, + "529": 108.14999999999992, + "530": 69.89999999999984, + "531": 104.35000000000018, + "532": 110.94999999999995, + "533": 117.00000000000028, + "534": 114.80000000000025, + "535": 84.70000000000006, + "536": 75.59999999999997, + "537": 113.30000000000004, + "538": 113.40000000000005, + "539": 115.00000000000013, + "540": 114.25000000000011, + "541": 110.99999999999999, + "542": -2.3999999999999826, + "543": 102.7500000000001, + "544": 108.59999999999987, + "545": 106.05000000000004, + "546": 114.10000000000004, + "547": 95.15000000000005, + "548": 102.79999999999983, + "549": 84.24999999999997, + "550": 99.19999999999987, + "551": 90.59999999999998, + "552": 111.30000000000022, + "553": 110.74999999999999, + "554": 114.85000000000015, + "555": 109.94999999999999, + "556": 97.69999999999993, + "557": 81.8, + "558": 104.99999999999991, + "559": 112.05000000000022, + "560": 112.30000000000011, + "561": 111.90000000000015, + "562": 110.04999999999976, + "563": 76.05000000000003, + "564": 114.35000000000011, + "565": 112.10000000000008, + "566": 113.4500000000001, + "567": 115.60000000000025, + "568": 91.09999999999995, + "569": 106.6000000000002, + "570": 35.999999999999766, + "571": 84.25000000000004, + "572": 116.05000000000024, + "573": 19.99999999999998, + "574": 104.0000000000002, + "575": 114.55000000000003, + "576": 112.25000000000018, + "577": 109.65000000000018, + "578": 116.35000000000026, + "579": 85.10000000000012, + "580": 94.75000000000004, + "581": 114.00000000000007, + "582": 111.24999999999999, + "583": 105.75000000000009, + "584": 105.34999999999984, + "585": 112.75000000000013, + "586": 95.90000000000006, + "587": 105.90000000000012, + "588": 102.35000000000002, + "589": 58.94999999999994, + "590": 110.69999999999993, + "591": 92.74999999999993, + "592": 111.75000000000016, + "593": 89.99999999999983, + "594": 65.89999999999993, + "595": 95.3000000000001, + "596": 101.20000000000014, + "597": 104.50000000000001, + "598": 109.60000000000014, + "599": 109.49999999999997, + "600": 98.40000000000012, + "601": 107.8499999999999, + "602": 109.05, + "603": 107.35000000000008, + "604": 105.75000000000011, + "605": 101.40000000000009, + "606": 107.90000000000002, + "607": 112.75000000000014, + "608": 99.59999999999988, + "609": 112.90000000000018, + "610": 107.24999999999993, + "611": 100.84999999999988, + "612": 105.90000000000002, + "613": 101.7500000000001, + "614": 95.8499999999999, + "615": 108.09999999999997, + "616": 104.44999999999976, + "617": 110.65000000000012, + "618": 111.20000000000013, + "619": 101.90000000000003, + "620": 98.19999999999997, + "621": 104.20000000000007, + "622": 110.60000000000015, + "623": 110.40000000000015, + "624": 115.35000000000025, + "625": 107.70000000000009, + "626": 96.10000000000005, + "627": 108.8000000000002, + "628": 108.74999999999994, + "629": 106.19999999999996, + "630": 109.85000000000004, + "631": 109.50000000000013, + "632": 113.95000000000024, + "633": 100.54999999999993, + "634": 109.90000000000005, + "635": 107.90000000000019, + "636": 105.75000000000014, + "637": 85.64999999999995, + "638": 114.95000000000014, + "639": 107.94999999999999, + "640": 113.85000000000018, + "641": 99.44999999999987, + "642": 103.24999999999991, + "643": 111.80000000000014, + "644": 91.64999999999989, + "645": 104.35000000000014, + "646": 111.25000000000004, + "647": 112.75000000000004, + "648": 114.10000000000022, + "649": 108.69999999999989, + "650": 103.84999999999987, + "651": 103.39999999999982, + "652": 74.64999999999984, + "653": 105.7500000000002, + "654": 110.39999999999998, + "655": 111.15000000000023, + "656": 109.90000000000006, + "657": 110.84999999999998, + "658": 94.09999999999991, + "659": 108.85000000000002, + "660": 108.44999999999997, + "661": 111.40000000000002, + "662": 107.35000000000005, + "663": 86.05000000000004, + "664": 25.500000000000014, + "665": 111.05000000000008, + "666": 93.99999999999999, + "667": 112.90000000000015, + "668": 105.75000000000007, + "669": 95.7999999999999, + "670": 110.80000000000013, + "671": 89.74999999999982, + "672": 113.80000000000011, + "673": 110.40000000000002, + "674": 112.30000000000007, + "675": 108.39999999999999, + "676": 113.4500000000002, + "677": 114.05000000000022, + "678": 112.80000000000007, + "679": 109.04999999999983, + "680": 110.65000000000009, + "681": 111.75000000000009, + "682": 109.20000000000005, + "683": 111.95000000000007, + "684": 112.75000000000013, + "685": 114.20000000000022, + "686": 111.79999999999997, + "687": 108.90000000000008, + "688": 104.50000000000011, + "689": 112.6500000000001, + "690": 95.84999999999981, + "691": 102.39999999999998, + "692": 110.09999999999998, + "693": 102.95000000000006, + "694": 106.10000000000005, + "695": 104.39999999999996, + "696": 110.45000000000017, + "697": 109.05000000000013, + "698": 105.70000000000012, + "699": 110.90000000000022, + "700": 106.70000000000002, + "701": 114.25000000000003, + "702": 113.00000000000011, + "703": 99.50000000000009, + "704": 110.50000000000004, + "705": 93.1499999999998, + "706": 113.45000000000019, + "707": 101.25000000000013, + "708": 113.20000000000016, + "709": 114.35000000000021, + "710": 114.75000000000023, + "711": 98.15, + "712": 96.20000000000009, + "713": 105.54999999999971, + "714": 114.70000000000024, + "715": 111.09999999999998, + "716": 107.35000000000014, + "717": 107.05000000000017, + "718": 113.20000000000026, + "719": 113.40000000000008, + "720": 88.94999999999995, + "721": 112.6000000000001, + "722": 35.699999999999974, + "723": 108.9499999999998, + "724": 106.9, + "725": 107.9499999999998, + "726": 86.30000000000001, + "727": 108.79999999999997, + "728": 107.69999999999999, + "729": 109.80000000000018, + "730": 112.90000000000015, + "731": 111.20000000000007, + "732": 111.85000000000008, + "733": 113.70000000000024, + "734": 113.10000000000018, + "735": 88.14999999999993, + "736": 102.90000000000009, + "737": 107.9, + "738": 111.05000000000011, + "739": 115.00000000000024, + "740": 105.74999999999996, + "741": 111.35000000000021, + "742": 100.60000000000007, + "743": 101.2999999999999, + "744": 107.25000000000007, + "745": 98.39999999999989, + "746": 107.70000000000014, + "747": 110.90000000000009, + "748": 109.85000000000008, + "749": 110.50000000000006, + "750": 111.85000000000016, + "751": 109.99999999999979, + "752": -17.299999999999997, + "753": 93.00000000000011, + "754": 113.80000000000011, + "755": 111.04999999999993, + "756": 63.69999999999992, + "757": 97.79999999999977, + "758": 112.85000000000004, + "759": 109.85, + "760": 96.49999999999999, + "761": 112.90000000000028, + "762": 87.54999999999995, + "763": 113.35000000000015, + "764": -3.5499999999999683, + "765": 103.79999999999994, + "766": 96.99999999999984, + "767": 113.95000000000027, + "768": -65.89999999999998, + "769": 94.65, + "770": 111.2000000000001, + "771": 112.70000000000009, + "772": 100.5500000000001, + "773": 112.00000000000021, + "774": 111.10000000000012, + "775": 117.15000000000028, + "776": 112.64999999999999, + "777": 107.69999999999995, + "778": 49.99999999999994, + "779": 103.04999999999988, + "780": 108.50000000000003, + "781": 73.99999999999986, + "782": 114.25000000000013, + "783": 115.40000000000025, + "784": 86.69999999999997, + "785": 113.35000000000015, + "786": 86.34999999999998, + "787": 108.00000000000003, + "788": 111.75000000000026, + "789": 108.45, + "790": 96.20000000000009, + "791": 90.75000000000009, + "792": 112.85000000000002, + "793": -5.299999999999993, + "794": 95.19999999999989, + "795": 103.9, + "796": 86.40000000000003, + "797": 112.65000000000005, + "798": 105.25000000000004, + "799": 115.75000000000023, + "800": 107.49999999999999, + "801": 109.80000000000005, + "802": -1.7499999999999785, + "803": 110.34999999999984, + "804": 115.55000000000017, + "805": 92.7500000000001, + "806": 109.1499999999999, + "807": 114.6000000000002, + "808": 112.10000000000004, + "809": 101.99999999999976, + "810": 112.15000000000022, + "811": 113.65000000000022, + "812": 114.55000000000003, + "813": 113.60000000000029, + "814": 105.80000000000018, + "815": 112.55000000000022, + "816": 113.00000000000018, + "817": 108.94999999999996, + "818": 116.65000000000026, + "819": 88.29999999999997, + "820": 111.80000000000004, + "821": 115.15000000000025, + "822": 112.90000000000019, + "823": 113.90000000000012, + "824": 94.50000000000013, + "825": 106.29999999999991, + "826": 98.39999999999998, + "827": 101.55, + "828": 98.09999999999981, + "829": 99.45000000000012, + "830": 92.54999999999977, + "831": 63.3499999999999, + "832": 114.90000000000023, + "833": 89.65000000000003, + "834": 114.35000000000016, + "835": -10.999999999999986, + "836": 113.00000000000007, + "837": 93.10000000000001, + "838": 110.65000000000022, + "839": 112.70000000000009, + "840": 100.59999999999977, + "841": 91.59999999999992, + "842": 110.5500000000001, + "843": 84.60000000000008, + "844": 112.75000000000017, + "845": 112.95000000000019, + "846": 106.90000000000009, + "847": 111.25000000000007, + "848": 113.55000000000025, + "849": 112.0500000000001, + "850": 80.79999999999991, + "851": 113.3000000000002, + "852": 105.75000000000013, + "853": 116.70000000000022, + "854": 103.0, + "855": 110.15000000000022, + "856": 112.55000000000004, + "857": 91.09999999999991, + "858": 114.65000000000023, + "859": 100.54999999999994, + "860": 75.8, + "861": 87.85000000000007, + "862": 102.49999999999979, + "863": 102.4, + "864": 110.05000000000018, + "865": -36.5, + "866": 113.45000000000017, + "867": 111.24999999999991, + "868": 108.7000000000002, + "869": 113.35000000000024, + "870": 113.85000000000016, + "871": 113.35000000000022, + "872": -44.799999999999955, + "873": 112.14999999999999, + "874": 113.40000000000013, + "875": 111.55000000000021, + "876": 115.60000000000025, + "877": 119.30000000000032, + "878": 97.45000000000013, + "879": 108.70000000000003, + "880": 113.75000000000018, + "881": 95.04999999999984, + "882": 90.45, + "883": 102.20000000000012, + "884": -76.15, + "885": 99.65000000000016, + "886": 109.65000000000016, + "887": 79.44999999999985, + "888": 114.50000000000026, + "889": 101.35000000000008, + "890": 111.5500000000002, + "891": 88.89999999999995, + "892": 98.60000000000014, + "893": 111.4500000000001, + "894": 100.70000000000003, + "895": 108.25, + "896": 115.45000000000026, + "897": 94.19999999999996, + "898": 89.80000000000005, + "899": 86.1999999999999, + "900": 118.9500000000003, + "901": 99.79999999999994, + "902": 115.20000000000024, + "903": 84.34999999999992, + "904": 114.00000000000017, + "905": 98.6500000000001, + "906": 111.50000000000017, + "907": 101.4500000000001, + "908": -20.749999999999986, + "909": 93.54999999999997, + "910": 85.10000000000007, + "911": 96.15, + "912": 110.79999999999991, + "913": 104.79999999999976, + "914": 105.00000000000016, + "915": 96.04999999999995, + "916": 110.19999999999996, + "917": 113.35000000000002, + "918": 102.85000000000004, + "919": 109.75000000000009, + "920": 113.10000000000026, + "921": 90.15000000000005, + "922": 113.15000000000013, + "923": 91.55000000000005, + "924": 101.80000000000004, + "925": 81.90000000000005, + "926": 110.14999999999986, + "927": 93.2500000000001, + "928": 99.89999999999985, + "929": 95.70000000000013, + "930": 90.9000000000001, + "931": 112.75000000000006, + "932": 112.9000000000002, + "933": 115.95000000000003, + "934": 109.95000000000002, + "935": 115.10000000000016, + "936": 95.5500000000001, + "937": 109.00000000000026, + "938": 39.49999999999998, + "939": 114.35000000000024, + "940": 110.65000000000019, + "941": 107.85000000000004, + "942": 80.14999999999999, + "943": 113.25000000000013, + "944": 99.10000000000005, + "945": 105.75, + "946": 104.4500000000002, + "947": 111.70000000000007, + "948": 97.10000000000012, + "949": 112.64999999999999, + "950": 113.50000000000016, + "951": 91.85000000000011, + "952": 114.00000000000027, + "953": 115.00000000000023, + "954": 97.65000000000006, + "955": 104.45000000000022, + "956": 102.44999999999996, + "957": 112.80000000000008, + "958": 87.2000000000001, + "959": 70.6499999999999, + "960": 108.04999999999974, + "961": 105.8499999999998, + "962": 81.15000000000005, + "963": 105.09999999999991, + "964": 107.10000000000016, + "965": 112.30000000000017, + "966": 101.60000000000014, + "967": 93.84999999999982, + "968": 103.25000000000001, + "969": 101.05000000000017, + "970": 107.45000000000016, + "971": 103.59999999999985, + "972": 113.79999999999988, + "973": -63.24999999999998, + "974": 107.2499999999999, + "975": 113.20000000000022, + "976": 107.35, + "977": 100.14999999999993, + "978": 114.70000000000013, + "979": 113.2000000000002, + "980": 112.10000000000016, + "981": 112.20000000000013, + "982": 88.85000000000005, + "983": 95.09999999999988, + "984": 92.95000000000002, + "985": 109.49999999999993, + "986": 104.0500000000002, + "987": 106.74999999999994, + "988": 101.55000000000011, + "989": 102.35000000000007, + "990": 101.80000000000003, + "991": 113.90000000000018, + "992": 99.04999999999984, + "993": 108.19999999999995, + "994": 80.74999999999994, + "995": 99.55000000000005, + "996": 101.90000000000018, + "997": 92.49999999999991, + "998": 114.15000000000012, + "999": 55.2499999999998, + "1000": 107.04999999999977 + }, + "2": { + "1": -6.399999999999982, + "2": -13.049999999999981, + "3": -2.949999999999971, + "4": -59.450000000000095, + "5": -22.54999999999998, + "6": -7.65, + "7": -27.64999999999999, + "8": -8.399999999999988, + "9": -50.45000000000013, + "10": -19.64999999999997, + "11": -15.499999999999973, + "12": -41.40000000000003, + "13": -20.949999999999957, + "14": -21.899999999999956, + "15": -33.40000000000003, + "16": -3.699999999999987, + "17": -29.499999999999922, + "18": -24.95, + "19": -82.79999999999995, + "20": -13.899999999999995, + "21": -13.999999999999982, + "22": 23.64999999999994, + "23": -36.85000000000005, + "24": -16.349999999999977, + "25": -76.39999999999992, + "26": -101.80000000000001, + "27": -75.75, + "28": -31.649999999999977, + "29": -1.6500000000000088, + "30": -37.349999999999994, + "31": -20.199999999999964, + "32": -7.050000000000009, + "33": -15.499999999999966, + "34": -12.649999999999988, + "35": -38.500000000000014, + "36": -5.3499999999999925, + "37": -16.549999999999955, + "38": -64.05000000000004, + "39": -66.10000000000008, + "40": -35.500000000000014, + "41": -10.999999999999993, + "42": -10.849999999999989, + "43": -36.00000000000004, + "44": -9.449999999999966, + "45": -21.34999999999996, + "46": -19.199999999999964, + "47": -19.09999999999997, + "48": -14.499999999999984, + "49": -42.60000000000006, + "50": -14.94999999999998, + "51": -86.65, + "52": -51.30000000000011, + "53": -35.650000000000006, + "54": -10.199999999999992, + "55": -19.249999999999964, + "56": -38.90000000000003, + "57": -21.599999999999955, + "58": -13.249999999999982, + "59": -17.649999999999974, + "60": -18.599999999999966, + "61": -15.099999999999984, + "62": -9.999999999999996, + "63": -83.85000000000004, + "64": -10.299999999999994, + "65": -13.14999999999999, + "66": -47.90000000000019, + "67": -50.8, + "68": -56.950000000000095, + "69": -16.299999999999976, + "70": -40.450000000000045, + "71": -91.85, + "72": -20.64999999999996, + "73": -9.749999999999991, + "74": -0.7499999999999669, + "75": -67.64999999999999, + "76": -13.799999999999988, + "77": -6.449999999999992, + "78": -20.44999999999996, + "79": -18.14999999999997, + "80": -2.2499999999999867, + "81": -18.14999999999997, + "82": -64.94999999999999, + "83": -19.94999999999996, + "84": -9.849999999999996, + "85": -1.699999999999971, + "86": -16.349999999999977, + "87": -20.899999999999974, + "88": -29.6, + "89": 1.950000000000004, + "90": -13.599999999999994, + "91": -3.099999999999965, + "92": -11.199999999999985, + "93": -97.29999999999998, + "94": -12.999999999999988, + "95": -10.199999999999992, + "96": -7.2499999999999964, + "97": -15.399999999999974, + "98": -18.14999999999997, + "99": -71.4, + "100": -66.20000000000005, + "101": 9.25000000000005, + "102": -19.199999999999967, + "103": 3.5500000000000536, + "104": -66.70000000000002, + "105": -92.95, + "106": -12.84999999999999, + "107": -6.749999999999985, + "108": -17.74999999999997, + "109": -2.249999999999985, + "110": -6.099999999999991, + "111": -63.54999999999996, + "112": 3.249999999999993, + "113": -64.44999999999999, + "114": -18.999999999999964, + "115": 23.249999999999954, + "116": -3.4000000000000075, + "117": -16.749999999999975, + "118": -15.149999999999975, + "119": -86.25, + "120": -7.499999999999985, + "121": -0.799999999999994, + "122": 0.49999999999999645, + "123": -16.24999999999998, + "124": -3.6999999999999797, + "125": 14.30000000000004, + "126": -26.049999999999937, + "127": 14.600000000000012, + "128": -14.549999999999988, + "129": -22.84999999999995, + "130": -28.79999999999994, + "131": -4.1, + "132": -78.79999999999998, + "133": -78.54999999999987, + "134": 7.8000000000000425, + "135": -23.54999999999995, + "136": -37.64999999999999, + "137": -18.04999999999997, + "138": 1.9999999999999984, + "139": -62.3500000000001, + "140": -20.349999999999962, + "141": -3.4499999999999718, + "142": -6.04999999999999, + "143": 13.750000000000053, + "144": -15.899999999999974, + "145": -19.749999999999986, + "146": -14.449999999999974, + "147": -13.699999999999958, + "148": -87.99999999999997, + "149": -10.99999999999999, + "150": 47.199999999999825, + "151": -13.049999999999985, + "152": 3.550000000000023, + "153": -18.799999999999965, + "154": -51.95000000000017, + "155": -90.15, + "156": -11.599999999999987, + "157": -57.700000000000095, + "158": 14.15000000000003, + "159": -73.35, + "160": -29.150000000000016, + "161": -53.6, + "162": 20.34999999999999, + "163": 2.3500000000000183, + "164": 6.450000000000037, + "165": -46.45, + "166": -65.5000000000001, + "167": -19.399999999999963, + "168": -5.749999999999993, + "169": 28.449999999999825, + "170": -86.45, + "171": -11.399999999999983, + "172": -4.550000000000002, + "173": -102.95, + "174": 14.800000000000082, + "175": 14.899999999999988, + "176": 27.74999999999988, + "177": 12.050000000000065, + "178": -81.35, + "179": -9.549999999999988, + "180": -55.7, + "181": 51.899999999999785, + "182": -102.2, + "183": -60.34999999999997, + "184": 38.04999999999984, + "185": -91.35000000000002, + "186": -30.599999999999998, + "187": -12.200000000000001, + "188": 38.84999999999976, + "189": 12.449999999999964, + "190": 22.599999999999923, + "191": -8.299999999999995, + "192": -69.19999999999995, + "193": 41.04999999999996, + "194": -30.199999999999967, + "195": 27.249999999999886, + "196": -20.44999999999996, + "197": 28.649999999999824, + "198": -80.70000000000002, + "199": -82.5, + "200": 3.1000000000000503, + "201": -36.900000000000055, + "202": -67.54999999999998, + "203": 14.700000000000063, + "204": -82.34999999999995, + "205": 19.899999999999913, + "206": -80.00000000000001, + "207": 0.6500000000000454, + "208": 2.2500000000000435, + "209": 31.349999999999753, + "210": -82.65, + "211": -78.09999999999997, + "212": -34.0, + "213": -45.349999999999994, + "214": -14.899999999999984, + "215": 7.050000000000028, + "216": -90.8, + "217": 43.7999999999998, + "218": -3.3499999999999757, + "219": 5.800000000000023, + "220": -4.899999999999984, + "221": 23.14999999999988, + "222": -8.349999999999994, + "223": 24.799999999999866, + "224": -85.80000000000001, + "225": -17.049999999999976, + "226": -2.150000000000013, + "227": 61.79999999999973, + "228": -65.1, + "229": 13.850000000000026, + "230": 3.2000000000000384, + "231": -30.399999999999984, + "232": 24.349999999999884, + "233": -15.149999999999988, + "234": 9.20000000000001, + "235": -20.349999999999962, + "236": 2.4000000000000394, + "237": 16.700000000000028, + "238": -18.59999999999996, + "239": 12.850000000000051, + "240": -2.399999999999972, + "241": 0.9000000000000248, + "242": 52.34999999999981, + "243": -64.15, + "244": -5.950000000000009, + "245": 6.300000000000021, + "246": 39.049999999999756, + "247": 2.4000000000000377, + "248": -21.29999999999995, + "249": -15.049999999999978, + "250": -74.35, + "251": -29.05000000000004, + "252": 4.1500000000000625, + "253": -17.999999999999968, + "254": -84.05, + "255": -22.749999999999947, + "256": -78.49999999999999, + "257": 0.649999999999993, + "258": 30.39999999999979, + "259": 71.44999999999986, + "260": -14.949999999999989, + "261": 24.30000000000006, + "262": 4.800000000000029, + "263": 58.749999999999815, + "264": -48.70000000000003, + "265": -1.0499999999999634, + "266": 37.19999999999998, + "267": 14.65000000000008, + "268": -29.999999999999968, + "269": 57.64999999999978, + "270": 17.40000000000003, + "271": -72.49999999999999, + "272": 83.10000000000008, + "273": -62.150000000000055, + "274": -81.75000000000009, + "275": 41.29999999999977, + "276": -48.949999999999996, + "277": -4.199999999999985, + "278": 6.850000000000071, + "279": 11.80000000000004, + "280": -0.24999999999997802, + "281": 41.699999999999925, + "282": 58.64999999999977, + "283": -72.14999999999998, + "284": -84.65, + "285": 42.9499999999998, + "286": -6.349999999999988, + "287": 52.749999999999886, + "288": -54.799999999999926, + "289": -29.90000000000001, + "290": 4.600000000000045, + "291": 43.4999999999999, + "292": 0.10000000000002762, + "293": 9.75000000000005, + "294": 29.100000000000005, + "295": -66.65, + "296": -6.549999999999944, + "297": 13.95000000000002, + "298": 58.79999999999974, + "299": 65.69999999999986, + "300": 76.29999999999976, + "301": -14.349999999999973, + "302": -7.650000000000009, + "303": 8.150000000000011, + "304": 84.74999999999999, + "305": -53.19999999999999, + "306": 65.54999999999993, + "307": -91.39999999999998, + "308": -16.549999999999965, + "309": -44.44999999999998, + "310": -34.95000000000003, + "311": 29.299999999999834, + "312": -61.20000000000003, + "313": -12.900000000000055, + "314": 65.29999999999987, + "315": 13.300000000000079, + "316": 73.99999999999994, + "317": -53.900000000000105, + "318": 10.350000000000014, + "319": 44.94999999999996, + "320": 48.7999999999998, + "321": -73.30000000000001, + "322": 40.9999999999998, + "323": -9.99999999999999, + "324": 30.10000000000005, + "325": -39.75000000000014, + "326": 70.64999999999993, + "327": 89.44999999999997, + "328": 44.74999999999988, + "329": -13.549999999999969, + "330": -38.300000000000075, + "331": -44.699999999999946, + "332": 82.59999999999981, + "333": 86.55000000000001, + "334": 73.89999999999984, + "335": -70.1, + "336": 4.750000000000067, + "337": 69.69999999999983, + "338": 45.44999999999983, + "339": 13.249999999999915, + "340": 90.34999999999974, + "341": 92.34999999999981, + "342": -83.00000000000007, + "343": 2.499999999999984, + "344": -53.49999999999997, + "345": 86.44999999999978, + "346": 71.24999999999977, + "347": 62.19999999999985, + "348": 39.39999999999982, + "349": 28.700000000000074, + "350": 63.29999999999988, + "351": 16.2499999999998, + "352": 57.49999999999982, + "353": 77.64999999999976, + "354": 70.29999999999994, + "355": 76.64999999999988, + "356": 1.5000000000000495, + "357": 88.6, + "358": -10.449999999999978, + "359": -9.249999999999998, + "360": -69.69999999999996, + "361": -57.64999999999997, + "362": 103.10000000000001, + "363": 22.64999999999991, + "364": -43.30000000000015, + "365": -30.050000000000058, + "366": 66.69999999999985, + "367": 46.89999999999986, + "368": 17.749999999999975, + "369": 10.150000000000025, + "370": 3.949999999999926, + "371": 66.29999999999978, + "372": 19.400000000000016, + "373": 42.549999999999905, + "374": 13.649999999999972, + "375": 102.2999999999998, + "376": 13.250000000000039, + "377": -30.49999999999997, + "378": 63.4999999999998, + "379": 108.4000000000002, + "380": -78.79999999999998, + "381": 93.14999999999976, + "382": 50.49999999999977, + "383": -30.74999999999998, + "384": 96.70000000000016, + "385": 77.84999999999987, + "386": 32.79999999999999, + "387": 99.89999999999978, + "388": -23.850000000000033, + "389": -65.30000000000001, + "390": 103.59999999999974, + "391": -45.84999999999997, + "392": 111.95000000000027, + "393": -17.20000000000004, + "394": 43.899999999999814, + "395": 81.5999999999998, + "396": 21.04999999999997, + "397": -25.349999999999987, + "398": 111.10000000000025, + "399": 88.19999999999989, + "400": 48.84999999999974, + "401": 98.25000000000026, + "402": 15.000000000000018, + "403": 78.5999999999998, + "404": -13.099999999999959, + "405": 77.39999999999979, + "406": 4.450000000000009, + "407": 40.99999999999988, + "408": -20.99999999999998, + "409": 103.54999999999974, + "410": 75.99999999999986, + "411": 93.19999999999978, + "412": 50.09999999999988, + "413": -32.39999999999997, + "414": -72.1, + "415": -26.10000000000001, + "416": 93.24999999999997, + "417": -49.74999999999996, + "418": 57.64999999999984, + "419": 4.049999999999951, + "420": -2.6499999999999835, + "421": 83.09999999999975, + "422": 107.89999999999988, + "423": -73.90000000000002, + "424": 50.2, + "425": 59.599999999999795, + "426": -30.20000000000003, + "427": -35.250000000000014, + "428": 18.899999999999945, + "429": 105.20000000000016, + "430": 105.79999999999991, + "431": 64.49999999999977, + "432": 113.85000000000018, + "433": 105.95000000000009, + "434": 35.99999999999978, + "435": 11.000000000000027, + "436": 107.04999999999973, + "437": 105.54999999999987, + "438": 99.94999999999979, + "439": 67.79999999999987, + "440": 107.40000000000009, + "441": 50.049999999999756, + "442": 114.15000000000012, + "443": 91.04999999999978, + "444": 90.20000000000006, + "445": 104.49999999999977, + "446": 14.499999999999996, + "447": 82.30000000000003, + "448": -0.3500000000000014, + "449": 83.05000000000003, + "450": -75.39999999999999, + "451": 108.79999999999976, + "452": -10.95000000000001, + "453": 38.299999999999976, + "454": 101.40000000000015, + "455": 33.74999999999989, + "456": 76.15000000000015, + "457": 98.95000000000009, + "458": 105.9, + "459": 87.2500000000001, + "460": 105.19999999999976, + "461": 78.94999999999976, + "462": 71.39999999999988, + "463": 95.70000000000002, + "464": 99.30000000000005, + "465": 105.74999999999986, + "466": -26.79999999999999, + "467": 2.95000000000001, + "468": 87.59999999999977, + "469": -63.69999999999996, + "470": -72.9, + "471": 105.09999999999977, + "472": 38.449999999999946, + "473": 21.499999999999964, + "474": 109.75000000000021, + "475": -36.09999999999997, + "476": 8.399999999999947, + "477": 90.45000000000022, + "478": 108.75000000000013, + "479": 105.75000000000017, + "480": 84.24999999999984, + "481": 98.89999999999989, + "482": 79.29999999999983, + "483": 77.14999999999976, + "484": 35.54999999999984, + "485": 60.04999999999994, + "486": 70.09999999999997, + "487": 99.59999999999977, + "488": 69.19999999999995, + "489": 102.74999999999979, + "490": 26.799999999999994, + "491": 107.05000000000001, + "492": 104.34999999999994, + "493": 112.70000000000016, + "494": 110.1999999999998, + "495": 97.59999999999974, + "496": 70.64999999999993, + "497": 101.49999999999976, + "498": 108.00000000000021, + "499": 104.54999999999974, + "500": 34.499999999999964, + "501": 84.99999999999997, + "502": 68.19999999999995, + "503": 63.499999999999794, + "504": 105.14999999999995, + "505": 107.39999999999974, + "506": 73.39999999999999, + "507": 52.84999999999995, + "508": 105.94999999999997, + "509": 56.59999999999975, + "510": 108.74999999999996, + "511": 38.699999999999946, + "512": 38.35, + "513": 73.95000000000019, + "514": 107.09999999999972, + "515": 117.30000000000028, + "516": 111.35000000000024, + "517": 111.69999999999997, + "518": 38.9499999999999, + "519": 74.54999999999995, + "520": 111.64999999999989, + "521": 102.5999999999998, + "522": 38.34999999999992, + "523": 105.34999999999984, + "524": 108.94999999999976, + "525": 86.09999999999984, + "526": 111.19999999999987, + "527": 66.79999999999987, + "528": 110.89999999999988, + "529": 108.79999999999991, + "530": 104.64999999999972, + "531": 101.99999999999973, + "532": 106.74999999999983, + "533": 28.299999999999915, + "534": 101.89999999999976, + "535": 104.34999999999975, + "536": 110.64999999999976, + "537": 111.60000000000015, + "538": 80.00000000000003, + "539": 102.84999999999977, + "540": 91.85000000000012, + "541": 100.29999999999978, + "542": 66.94999999999989, + "543": 96.14999999999988, + "544": 110.19999999999987, + "545": 74.89999999999995, + "546": 46.999999999999865, + "547": 89.19999999999982, + "548": 72.54999999999984, + "549": 79.7499999999998, + "550": 102.65000000000002, + "551": 80.10000000000007, + "552": 107.59999999999974, + "553": 63.79999999999991, + "554": 99.34999999999977, + "555": 60.99999999999995, + "556": 91.54999999999991, + "557": 103.84999999999974, + "558": 109.8499999999998, + "559": 91.59999999999981, + "560": 82.89999999999975, + "561": 101.25000000000003, + "562": 103.34999999999977, + "563": 87.49999999999982, + "564": 105.99999999999979, + "565": 103.64999999999984, + "566": 108.54999999999995, + "567": 110.15000000000003, + "568": 104.24999999999999, + "569": 107.84999999999972, + "570": 109.09999999999975, + "571": 95.24999999999976, + "572": 101.54999999999997, + "573": 86.64999999999979, + "574": -66.35000000000007, + "575": 106.24999999999986, + "576": 105.69999999999975, + "577": 104.74999999999977, + "578": 95.59999999999988, + "579": 111.15000000000026, + "580": 27.09999999999997, + "581": 5.250000000000066, + "582": 102.19999999999976, + "583": 47.849999999999746, + "584": 96.19999999999995, + "585": 77.54999999999978, + "586": 38.64999999999988, + "587": 74.29999999999973, + "588": 98.99999999999993, + "589": 31.099999999999813, + "590": 76.94999999999976, + "591": 92.84999999999982, + "592": 93.54999999999976, + "593": 34.49999999999976, + "594": 96.9999999999999, + "595": 95.80000000000021, + "596": 82.19999999999976, + "597": 111.04999999999993, + "598": 91.09999999999974, + "599": 82.55000000000014, + "600": 87.04999999999977, + "601": 29.349999999999866, + "602": 95.04999999999986, + "603": -7.5499999999999945, + "604": 109.90000000000008, + "605": 100.39999999999972, + "606": 104.64999999999974, + "607": 104.64999999999972, + "608": 17.300000000000086, + "609": 106.59999999999975, + "610": 109.35000000000012, + "611": 104.79999999999976, + "612": 116.90000000000019, + "613": 77.89999999999986, + "614": 86.15000000000005, + "615": 111.14999999999996, + "616": 109.89999999999984, + "617": 84.94999999999973, + "618": 117.05000000000025, + "619": -31.700000000000003, + "620": 40.99999999999973, + "621": 100.19999999999997, + "622": 98.44999999999979, + "623": 82.70000000000003, + "624": 94.35000000000011, + "625": 101.90000000000012, + "626": 103.04999999999974, + "627": 74.29999999999993, + "628": 99.94999999999999, + "629": 102.54999999999991, + "630": 36.699999999999996, + "631": 92.14999999999998, + "632": 86.00000000000004, + "633": 92.69999999999978, + "634": 79.59999999999981, + "635": 55.09999999999986, + "636": 52.00000000000002, + "637": 102.95000000000017, + "638": 72.8499999999999, + "639": 105.40000000000015, + "640": 101.70000000000006, + "641": 104.05000000000017, + "642": 103.95000000000009, + "643": -16.20000000000005, + "644": 110.04999999999998, + "645": 107.75000000000013, + "646": 36.04999999999999, + "647": 108.74999999999976, + "648": 102.2999999999999, + "649": 107.74999999999974, + "650": 90.19999999999979, + "651": 102.19999999999986, + "652": 95.84999999999987, + "653": 113.85000000000015, + "654": 99.19999999999975, + "655": 98.44999999999973, + "656": 102.64999999999976, + "657": 102.89999999999995, + "658": 93.75000000000001, + "659": 93.55, + "660": 96.44999999999997, + "661": 104.69999999999973, + "662": 104.49999999999982, + "663": 62.39999999999992, + "664": 81.55000000000001, + "665": 80.20000000000002, + "666": 43.5999999999998, + "667": 106.8500000000002, + "668": 111.30000000000014, + "669": 66.44999999999995, + "670": 95.45000000000005, + "671": 99.15000000000015, + "672": 103.04999999999977, + "673": 118.3500000000003, + "674": 114.70000000000024, + "675": 107.94999999999982, + "676": 97.70000000000014, + "677": 116.25000000000023, + "678": 110.45000000000002, + "679": 114.7500000000002, + "680": -3.7499999999999662, + "681": 113.90000000000023, + "682": 105.10000000000015, + "683": 93.79999999999978, + "684": 108.25000000000021, + "685": 106.14999999999985, + "686": 120.05000000000034, + "687": 83.89999999999988, + "688": 97.89999999999993, + "689": 112.85000000000018, + "690": 112.2000000000001, + "691": 98.25000000000017, + "692": 48.94999999999988, + "693": 117.30000000000022, + "694": 100.25000000000007, + "695": 106.8999999999998, + "696": 74.70000000000005, + "697": 78.60000000000001, + "698": 113.10000000000002, + "699": 84.25000000000003, + "700": 116.65000000000019, + "701": 112.60000000000016, + "702": 84.95000000000003, + "703": 117.05000000000025, + "704": 104.69999999999973, + "705": 113.95000000000016, + "706": 98.49999999999977, + "707": 103.34999999999978, + "708": 105.49999999999972, + "709": 88.59999999999984, + "710": 111.25000000000004, + "711": 112.74999999999999, + "712": 80.70000000000005, + "713": 112.60000000000011, + "714": 81.99999999999994, + "715": 100.60000000000015, + "716": 82.15000000000005, + "717": 63.19999999999997, + "718": 64.04999999999988, + "719": 114.75000000000027, + "720": 117.15000000000023, + "721": 91.30000000000005, + "722": 95.45000000000003, + "723": 109.84999999999995, + "724": 114.1499999999999, + "725": 70.25000000000026, + "726": 71.10000000000008, + "727": 95.69999999999997, + "728": 64.30000000000004, + "729": 94.95000000000009, + "730": 63.09999999999995, + "731": 88.69999999999989, + "732": 92.04999999999993, + "733": 73.95, + "734": 4.449999999999994, + "735": 107.44999999999986, + "736": 104.00000000000007, + "737": 102.55000000000018, + "738": 105.04999999999974, + "739": 55.64999999999993, + "740": 117.85000000000026, + "741": 28.799999999999834, + "742": 116.15000000000026, + "743": 103.49999999999999, + "744": 85.19999999999996, + "745": 113.15000000000006, + "746": 101.05000000000014, + "747": 112.95000000000006, + "748": 17.45000000000006, + "749": 112.59999999999992, + "750": 43.10000000000004, + "751": 82.15000000000003, + "752": 100.04999999999974, + "753": 112.15000000000018, + "754": 103.84999999999978, + "755": 108.9999999999998, + "756": 94.64999999999995, + "757": 82.75000000000006, + "758": 111.50000000000007, + "759": 100.3999999999998, + "760": 114.70000000000024, + "761": 113.70000000000006, + "762": 110.0500000000002, + "763": 110.35000000000018, + "764": 113.45000000000019, + "765": 51.999999999999915, + "766": 113.55000000000025, + "767": 110.19999999999979, + "768": 90.59999999999992, + "769": 97.54999999999993, + "770": 105.10000000000016, + "771": 78.29999999999993, + "772": 82.70000000000006, + "773": 118.50000000000023, + "774": 84.29999999999997, + "775": 111.75000000000007, + "776": 101.05000000000008, + "777": 110.94999999999999, + "778": 103.75000000000014, + "779": 114.70000000000012, + "780": 76.00000000000004, + "781": 87.35000000000008, + "782": 115.65000000000026, + "783": 106.04999999999974, + "784": 108.80000000000011, + "785": 94.14999999999988, + "786": 112.30000000000007, + "787": 106.75000000000007, + "788": 111.49999999999994, + "789": 98.70000000000005, + "790": 115.30000000000027, + "791": 49.34999999999987, + "792": 98.00000000000016, + "793": 79.20000000000003, + "794": 110.05000000000001, + "795": 82.30000000000011, + "796": 90.89999999999989, + "797": 117.15000000000028, + "798": 88.29999999999995, + "799": 79.7499999999999, + "800": 112.39999999999993, + "801": 110.60000000000001, + "802": 95.85000000000007, + "803": 105.99999999999972, + "804": 102.05000000000017, + "805": 113.40000000000008, + "806": 41.99999999999995, + "807": 91.09999999999994, + "808": 72.64999999999993, + "809": 105.59999999999995, + "810": 33.50000000000001, + "811": 88.89999999999999, + "812": 108.04999999999997, + "813": 103.95000000000007, + "814": 106.35000000000007, + "815": 45.99999999999995, + "816": 114.50000000000021, + "817": 112.1500000000001, + "818": 109.50000000000003, + "819": 106.10000000000007, + "820": 95.29999999999998, + "821": 115.05000000000025, + "822": 94.19999999999992, + "823": 105.4499999999999, + "824": 115.3000000000002, + "825": 108.84999999999998, + "826": 107.54999999999986, + "827": 113.85000000000007, + "828": 92.00000000000014, + "829": 108.69999999999979, + "830": 110.2000000000001, + "831": 113.55000000000021, + "832": 113.70000000000014, + "833": 90.20000000000019, + "834": 115.20000000000022, + "835": 109.89999999999993, + "836": 115.05000000000018, + "837": 116.25000000000024, + "838": 11.550000000000006, + "839": 71.39999999999995, + "840": 114.2000000000002, + "841": 112.40000000000023, + "842": 92.60000000000005, + "843": 114.40000000000022, + "844": 113.85000000000024, + "845": 104.0000000000001, + "846": 96.20000000000002, + "847": 108.60000000000016, + "848": 92.45000000000005, + "849": 101.94999999999985, + "850": 111.60000000000018, + "851": 112.80000000000024, + "852": 93.45000000000007, + "853": 113.55000000000008, + "854": 100.80000000000018, + "855": 80.69999999999999, + "856": 110.65000000000022, + "857": 99.59999999999992, + "858": 81.89999999999996, + "859": 105.8499999999998, + "860": 115.15000000000025, + "861": 78.1500000000001, + "862": 92.3, + "863": 73.24999999999997, + "864": 113.15000000000005, + "865": 27.449999999999992, + "866": 115.00000000000023, + "867": 86.60000000000002, + "868": 113.90000000000025, + "869": 70.24999999999983, + "870": 101.59999999999994, + "871": 102.25000000000016, + "872": 115.85000000000025, + "873": 109.10000000000025, + "874": 108.30000000000021, + "875": 58.99999999999993, + "876": 110.84999999999991, + "877": 115.00000000000024, + "878": 110.45000000000006, + "879": 89.65000000000009, + "880": 72.64999999999998, + "881": 60.099999999999895, + "882": 112.30000000000011, + "883": 115.50000000000017, + "884": 70.54999999999993, + "885": 111.44999999999996, + "886": 107.50000000000018, + "887": 95.70000000000012, + "888": 116.40000000000025, + "889": 93.05000000000003, + "890": 113.40000000000012, + "891": 111.7, + "892": 107.55000000000004, + "893": 92.69999999999982, + "894": 72.94999999999997, + "895": 109.05000000000013, + "896": 97.10000000000014, + "897": 60.09999999999993, + "898": 114.35000000000022, + "899": 114.60000000000012, + "900": 91.70000000000007, + "901": 80.99999999999996, + "902": 99.75000000000006, + "903": 87.1, + "904": 93.65, + "905": 113.45000000000014, + "906": 108.20000000000013, + "907": 93.8500000000001, + "908": 72.44999999999985, + "909": 61.899999999999935, + "910": 110.7500000000001, + "911": 93.50000000000009, + "912": 113.50000000000003, + "913": 80.35000000000025, + "914": 113.25000000000014, + "915": 99.0, + "916": 104.85000000000016, + "917": 115.60000000000026, + "918": 101.25000000000011, + "919": 97.80000000000007, + "920": 77.39999999999998, + "921": 111.40000000000023, + "922": 114.05000000000024, + "923": 116.60000000000028, + "924": 106.44999999999973, + "925": 107.90000000000018, + "926": 87.4, + "927": 113.7500000000002, + "928": 114.5000000000002, + "929": 89.90000000000008, + "930": 112.95000000000016, + "931": 104.89999999999985, + "932": 107.45000000000005, + "933": 81.64999999999985, + "934": 114.35000000000021, + "935": 31.199999999999974, + "936": 71.99999999999999, + "937": 104.3500000000002, + "938": 118.85000000000015, + "939": 115.10000000000015, + "940": 113.0999999999999, + "941": 111.15000000000005, + "942": 101.95000000000019, + "943": 111.80000000000005, + "944": 115.45000000000024, + "945": 92.1500000000001, + "946": 52.54999999999997, + "947": 103.30000000000018, + "948": 108.40000000000022, + "949": 37.39999999999997, + "950": 92.20000000000017, + "951": 94.00000000000006, + "952": 113.50000000000009, + "953": 115.80000000000025, + "954": 110.95000000000023, + "955": 112.4500000000002, + "956": 114.40000000000025, + "957": 80.15000000000005, + "958": 116.65000000000023, + "959": 104.60000000000018, + "960": 115.60000000000024, + "961": 113.90000000000019, + "962": 113.25000000000014, + "963": 110.95000000000006, + "964": 113.80000000000022, + "965": 64.64999999999992, + "966": 114.40000000000023, + "967": 98.65000000000006, + "968": 80.64999999999998, + "969": 73.05000000000001, + "970": 86.80000000000011, + "971": 90.40000000000008, + "972": 112.95000000000017, + "973": 84.34999999999985, + "974": 73.14999999999998, + "975": 91.9999999999999, + "976": 74.15000000000002, + "977": 68.34999999999994, + "978": 105.45000000000006, + "979": 114.00000000000016, + "980": 115.10000000000025, + "981": 111.00000000000006, + "982": 102.45000000000012, + "983": 99.00000000000016, + "984": 112.80000000000011, + "985": 113.45000000000012, + "986": 112.0, + "987": 111.6500000000001, + "988": 114.50000000000001, + "989": 108.6000000000002, + "990": 116.05000000000018, + "991": 73.75, + "992": 116.35000000000026, + "993": 45.64999999999986, + "994": 104.74999999999976, + "995": 89.35000000000004, + "996": 90.15000000000008, + "997": 107.40000000000019, + "998": 99.1999999999999, + "999": 107.45000000000007, + "1000": 111.90000000000013 + }, + "3": { + "1": -53.550000000000075, + "2": -20.14999999999996, + "3": -35.59999999999995, + "4": -57.150000000000084, + "5": -28.14999999999997, + "6": -4.350000000000001, + "7": -19.74999999999996, + "8": -18.84999999999997, + "9": -17.199999999999985, + "10": -11.59999999999999, + "11": -2.149999999999972, + "12": -42.650000000000176, + "13": -15.249999999999966, + "14": -12.549999999999978, + "15": -54.75, + "16": -19.649999999999963, + "17": -57.65000000000006, + "18": -52.050000000000075, + "19": -14.949999999999982, + "20": -81.44999999999982, + "21": -15.399999999999979, + "22": -20.099999999999962, + "23": -43.95000000000005, + "24": -83.29999999999993, + "25": -23.599999999999977, + "26": -20.59999999999997, + "27": -0.949999999999972, + "28": -17.14999999999997, + "29": -3.2499999999999787, + "30": -10.849999999999998, + "31": -11.34999999999999, + "32": -24.950000000000035, + "33": -11.799999999999992, + "34": -86.99999999999994, + "35": -17.499999999999968, + "36": -13.549999999999981, + "37": -53.10000000000008, + "38": -14.849999999999984, + "39": -10.99999999999999, + "40": -44.30000000000016, + "41": -17.8, + "42": -21.39999999999995, + "43": -29.700000000000017, + "44": -13.69999999999999, + "45": -14.099999999999987, + "46": -22.049999999999955, + "47": -17.249999999999975, + "48": -15.299999999999986, + "49": -19.549999999999965, + "50": -1.2499999999999953, + "51": -23.44999999999995, + "52": -17.249999999999954, + "53": -45.250000000000185, + "54": -77.59999999999997, + "55": -46.95, + "56": -77.7, + "57": -12.949999999999985, + "58": -46.89999999999995, + "59": -7.849999999999997, + "60": -41.199999999999974, + "61": -9.000000000000007, + "62": -20.649999999999956, + "63": -13.999999999999972, + "64": -2.8499999999999837, + "65": -6.049999999999981, + "66": -52.10000000000007, + "67": -16.099999999999977, + "68": -18.399999999999967, + "69": -20.999999999999957, + "70": 12.600000000000046, + "71": -16.499999999999975, + "72": -38.150000000000006, + "73": -17.74999999999997, + "74": -12.64999999999999, + "75": -68.99999999999994, + "76": -19.99999999999996, + "77": -5.149999999999989, + "78": -101.79999999999998, + "79": 4.350000000000045, + "80": -87.85000000000001, + "81": -44.49999999999999, + "82": -46.5, + "83": -19.99999999999996, + "84": -6.700000000000016, + "85": -10.949999999999992, + "86": 7.9500000000000535, + "87": -8.700000000000001, + "88": -77.29999999999998, + "89": 0.25000000000000466, + "90": -89.59999999999994, + "91": -83.45, + "92": -8.700000000000001, + "93": -91.20000000000002, + "94": -13.899999999999983, + "95": -69.45000000000006, + "96": -7.9499999999999975, + "97": -21.549999999999958, + "98": -7.999999999999985, + "99": -15.599999999999978, + "100": -52.89999999999998, + "101": -42.95000000000012, + "102": -13.949999999999983, + "103": -62.89999999999999, + "104": 2.9000000000000288, + "105": -79.65, + "106": 4.55000000000002, + "107": -35.60000000000007, + "108": -10.049999999999988, + "109": -68.25000000000003, + "110": -1.1999999999999844, + "111": -0.7999999999999656, + "112": -20.549999999999955, + "113": -19.79999999999996, + "114": -11.399999999999993, + "115": 11.500000000000021, + "116": -18.44999999999997, + "117": -26.44999999999998, + "118": 2.3500000000000147, + "119": -0.2499999999999707, + "120": -13.09999999999998, + "121": -93.8, + "122": -34.90000000000002, + "123": -73.30000000000001, + "124": -19.69999999999996, + "125": -98.69999999999999, + "126": -67.85000000000001, + "127": -19.09999999999997, + "128": -70.14999999999998, + "129": -10.450000000000003, + "130": -5.6999999999999975, + "131": -14.79999999999997, + "132": 40.94999999999987, + "133": -6.749999999999996, + "134": -13.299999999999988, + "135": -20.549999999999955, + "136": -12.849999999999987, + "137": -26.14999999999998, + "138": -4.9999999999999805, + "139": 3.7500000000000444, + "140": -18.09999999999997, + "141": -3.34999999999998, + "142": -8.399999999999988, + "143": -1.7499999999999825, + "144": -7.99999999999999, + "145": -92.65, + "146": 28.699999999999925, + "147": 32.649999999999764, + "148": -22.350000000000023, + "149": -72.85, + "150": 40.84999999999977, + "151": -55.249999999999964, + "152": 17.95000000000005, + "153": 4.500000000000005, + "154": -26.999999999999982, + "155": -5.649999999999988, + "156": 23.549999999999976, + "157": -2.4499999999999433, + "158": -12.249999999999988, + "159": -19.29999999999997, + "160": -31.249999999999996, + "161": 5.600000000000037, + "162": -20.899999999999956, + "163": -12.149999999999991, + "164": -2.399999999999962, + "165": -17.69999999999997, + "166": 51.1499999999999, + "167": -68.9, + "168": -24.700000000000003, + "169": -42.05, + "170": 58.249999999999815, + "171": -7.999999999999994, + "172": 0.8500000000000347, + "173": 0.5499999999999936, + "174": 52.94999999999982, + "175": -1.950000000000001, + "176": -83.05, + "177": -68.65000000000002, + "178": -52.09999999999995, + "179": -25.04999999999999, + "180": 13.950000000000019, + "181": -64.8000000000001, + "182": 25.899999999999892, + "183": 12.50000000000006, + "184": 18.750000000000032, + "185": -12.45000000000004, + "186": -83.85, + "187": 24.15000000000003, + "188": -32.70000000000004, + "189": -55.05000000000005, + "190": -78.85, + "191": -0.6999999999999784, + "192": -14.399999999999991, + "193": -13.199999999999985, + "194": -31.899999999999988, + "195": -17.99999999999997, + "196": -32.10000000000001, + "197": -9.150000000000002, + "198": 5.15000000000006, + "199": 10.850000000000016, + "200": -2.4499999999999673, + "201": 4.850000000000048, + "202": -15.399999999999983, + "203": -39.10000000000001, + "204": -28.24999999999994, + "205": 68.84999999999992, + "206": 33.09999999999985, + "207": -21.69999999999999, + "208": -4.04999999999999, + "209": -6.6499999999999915, + "210": -11.69999999999999, + "211": 18.950000000000003, + "212": -28.25000000000001, + "213": -67.94999999999999, + "214": -40.05, + "215": 22.800000000000047, + "216": -70.75, + "217": -8.949999999999996, + "218": 19.05, + "219": 0.05000000000001714, + "220": -52.25000000000001, + "221": -77.89999999999998, + "222": -74.89999999999998, + "223": -75.94999999999999, + "224": 2.00000000000003, + "225": -6.250000000000014, + "226": 20.74999999999999, + "227": 34.299999999999876, + "228": -0.19999999999999063, + "229": -13.899999999999984, + "230": -32.900000000000006, + "231": 62.999999999999886, + "232": 91.1000000000001, + "233": 44.04999999999978, + "234": -73.54999999999997, + "235": 11.14999999999999, + "236": 7.200000000000019, + "237": -82.75000000000001, + "238": 0.2000000000000306, + "239": 9.250000000000053, + "240": -21.299999999999986, + "241": -58.39999999999999, + "242": -6.349999999999995, + "243": 60.79999999999984, + "244": -8.649999999999995, + "245": 35.999999999999794, + "246": -63.99999999999995, + "247": 67.49999999999999, + "248": 60.499999999999986, + "249": 15.800000000000033, + "250": 24.649999999999896, + "251": 23.30000000000001, + "252": 51.749999999999794, + "253": 94.15000000000012, + "254": -45.50000000000002, + "255": -74.7, + "256": 64.94999999999992, + "257": -83.44999999999999, + "258": 8.500000000000018, + "259": 14.150000000000059, + "260": -22.49999999999995, + "261": 80.95, + "262": -48.10000000000007, + "263": 86.49999999999986, + "264": -96.99999999999997, + "265": 99.79999999999984, + "266": 24.79999999999985, + "267": -16.24999999999997, + "268": 31.69999999999989, + "269": 22.949999999999942, + "270": 87.09999999999987, + "271": 78.40000000000006, + "272": -60.49999999999996, + "273": 7.650000000000068, + "274": -63.35000000000004, + "275": 28.399999999999853, + "276": 30.04999999999987, + "277": 19.699999999999953, + "278": 103.05000000000018, + "279": 72.1499999999998, + "280": 98.15000000000028, + "281": -2.4499999999999815, + "282": 40.299999999999805, + "283": -6.800000000000008, + "284": 4.800000000000047, + "285": -15.00000000000006, + "286": 6.90000000000002, + "287": 9.199999999999982, + "288": 81.35000000000008, + "289": 45.999999999999986, + "290": 48.10000000000001, + "291": 63.599999999999824, + "292": 33.69999999999979, + "293": 43.09999999999996, + "294": 41.00000000000003, + "295": 97.65000000000006, + "296": 69.24999999999987, + "297": 50.100000000000016, + "298": 34.40000000000001, + "299": 4.849999999999987, + "300": 44.89999999999994, + "301": 82.09999999999994, + "302": 74.6500000000001, + "303": 79.85000000000002, + "304": 33.999999999999986, + "305": 89.39999999999999, + "306": 67.84999999999992, + "307": 105.19999999999999, + "308": 75.49999999999996, + "309": 96.49999999999983, + "310": 53.89999999999983, + "311": 90.25000000000009, + "312": 104.45, + "313": 114.6000000000001, + "314": 72.94999999999997, + "315": 91.75000000000004, + "316": 109.70000000000006, + "317": 104.24999999999982, + "318": 94.70000000000009, + "319": 80.79999999999997, + "320": 80.90000000000016, + "321": 90.4500000000001, + "322": 104.75000000000003, + "323": 112.00000000000021, + "324": 88.7500000000001, + "325": -26.700000000000028, + "326": 84.2499999999998, + "327": 92.0, + "328": 105.85000000000015, + "329": 83.8500000000001, + "330": 81.14999999999996, + "331": 72.85000000000002, + "332": 97.20000000000005, + "333": 99.80000000000008, + "334": 6.599999999999982, + "335": 94.99999999999997, + "336": 22.749999999999947, + "337": 102.95, + "338": 91.55000000000004, + "339": 87.95, + "340": 99.15000000000012, + "341": 100.05000000000004, + "342": 106.9000000000001, + "343": 94.64999999999998, + "344": 87.85000000000014, + "345": 101.95000000000003, + "346": 92.05000000000001, + "347": 84.55000000000008, + "348": 102.95, + "349": 100.7000000000002, + "350": 106.89999999999993, + "351": 111.75000000000023, + "352": 108.75000000000014, + "353": 103.4000000000001, + "354": 24.199999999999935, + "355": 94.60000000000001, + "356": 113.10000000000002, + "357": -46.949999999999974, + "358": 83.95000000000007, + "359": 9.500000000000039, + "360": 113.0000000000001, + "361": 111.0000000000002, + "362": 98.5500000000001, + "363": 117.7500000000001, + "364": 64.09999999999997, + "365": 86.45000000000005, + "366": 77.00000000000006, + "367": 112.15000000000012, + "368": 98.35000000000001, + "369": 100.70000000000003, + "370": 100.85000000000011, + "371": 114.85000000000005, + "372": 101.19999999999999, + "373": 105.85000000000005, + "374": 109.94999999999999, + "375": 108.50000000000007, + "376": 109.35000000000018, + "377": 115.55000000000007, + "378": 108.15000000000006, + "379": 78.30000000000011, + "380": 106.50000000000004, + "381": 93.90000000000006, + "382": 82.45000000000002, + "383": 91.20000000000003, + "384": 101.35000000000021, + "385": 93.55000000000003, + "386": 111.15, + "387": 106.10000000000002, + "388": 99.75000000000013, + "389": 60.99999999999983, + "390": 105.35000000000014, + "391": 122.20000000000012, + "392": 105.80000000000001, + "393": 102.10000000000012, + "394": 104.20000000000014, + "395": 105.60000000000022, + "396": 104.79999999999993, + "397": 111.45, + "398": 109.85000000000011, + "399": 113.15000000000003, + "400": 84.19999999999997, + "401": 112.0, + "402": 117.55000000000004, + "403": 103.25000000000011, + "404": 111.2000000000002, + "405": 16.950000000000035, + "406": 81.15000000000013, + "407": 112.30000000000004, + "408": 106.5500000000001, + "409": 118.55000000000007, + "410": 95.30000000000017, + "411": 79.64999999999998, + "412": 97.2000000000001, + "413": 108.25000000000013, + "414": 107.55000000000022, + "415": 108.00000000000011, + "416": 99.15000000000006, + "417": 111.85000000000008, + "418": 114.1500000000002, + "419": 118.05, + "420": 104.55000000000003, + "421": 109.84999999999997, + "422": 114.55000000000015, + "423": 99.8, + "424": 110.30000000000001, + "425": 106.30000000000011, + "426": 112.65000000000023, + "427": 109.15000000000009, + "428": 100.55000000000013, + "429": 110.19999999999996, + "430": 107.84999999999998, + "431": 108.69999999999997, + "432": 111.80000000000018, + "433": 110.9000000000001, + "434": 106.30000000000001, + "435": 56.899999999999835, + "436": 115.1000000000002, + "437": 110.85000000000005, + "438": 105.80000000000008, + "439": 106.65000000000005, + "440": 111.34999999999987, + "441": 112.65000000000016, + "442": 116.40000000000018, + "443": 110.00000000000017, + "444": 110.65000000000002, + "445": 105.20000000000002, + "446": 98.70000000000003, + "447": 101.09999999999992, + "448": 98.55000000000007, + "449": 114.75000000000011, + "450": 109.95000000000009, + "451": 123.15000000000002, + "452": 110.75000000000016, + "453": 97.15000000000019, + "454": 109.70000000000017, + "455": 110.85000000000014, + "456": 118.20000000000009, + "457": 104.2500000000001, + "458": 120.05000000000015, + "459": 105.10000000000002, + "460": 114.80000000000005, + "461": 111.70000000000006, + "462": 115.45, + "463": 112.05000000000005, + "464": 107.30000000000008, + "465": 111.09999999999998, + "466": 122.70000000000013, + "467": 117.10000000000004, + "468": 96.45000000000005, + "469": 118.10000000000002, + "470": 109.15000000000003, + "471": 35.94999999999987, + "472": 105.10000000000016, + "473": 114.34999999999995, + "474": 116.55000000000011, + "475": 105.50000000000013, + "476": 113.80000000000001, + "477": 117.30000000000007, + "478": 112.1, + "479": 95.85000000000008, + "480": 116.2500000000001, + "481": 113.15000000000013, + "482": 101.15, + "483": 96.40000000000015, + "484": 117.65000000000012, + "485": 108.95000000000007, + "486": 114.3500000000001, + "487": 88.24999999999994, + "488": 107.40000000000005, + "489": 105.80000000000008, + "490": 121.55000000000001, + "491": 115.05000000000013, + "492": 101.14999999999993, + "493": 112.00000000000004, + "494": 115.15000000000015, + "495": 117.04999999999997, + "496": 45.54999999999983, + "497": 111.64999999999995, + "498": 116.50000000000004, + "499": 110.05000000000003, + "500": 106.4000000000001, + "501": 118.35000000000014, + "502": 106.19999999999997, + "503": 101.75000000000009, + "504": 114.85000000000007, + "505": 113.00000000000014, + "506": 69.84999999999997, + "507": 98.64999999999995, + "508": 112.25000000000004, + "509": 97.50000000000018, + "510": 103.65000000000003, + "511": 94.55000000000011, + "512": 110.95000000000014, + "513": 105.2, + "514": 106.05000000000017, + "515": 112.30000000000014, + "516": 114.15000000000002, + "517": 107.50000000000011, + "518": 96.80000000000013, + "519": 108.7000000000001, + "520": 108.44999999999999, + "521": 108.30000000000014, + "522": 115.55000000000022, + "523": 118.80000000000004, + "524": 102.00000000000007, + "525": 106.70000000000014, + "526": 106.35000000000015, + "527": 111.45000000000003, + "528": 106.39999999999995, + "529": 104.30000000000005, + "530": 105.90000000000013, + "531": 106.89999999999999, + "532": 108.25, + "533": 107.29999999999997, + "534": 114.80000000000008, + "535": 113.05000000000008, + "536": 110.70000000000016, + "537": 92.00000000000001, + "538": 118.85000000000001, + "539": 115.50000000000004, + "540": 116.64999999999996, + "541": 117.60000000000011, + "542": 102.15000000000003, + "543": 113.85000000000008, + "544": 109.30000000000014, + "545": 100.79999999999995, + "546": 119.40000000000009, + "547": 112.15000000000018, + "548": 115.40000000000005, + "549": 114.30000000000007, + "550": 115.25000000000014, + "551": 107.80000000000005, + "552": 113.65000000000016, + "553": 102.20000000000014, + "554": 108.74999999999999, + "555": 109.00000000000013, + "556": 110.60000000000012, + "557": 111.95000000000012, + "558": 107.29999999999995, + "559": 112.65000000000008, + "560": 105.10000000000002, + "561": 109.55000000000014, + "562": 113.19999999999989, + "563": 115.3000000000001, + "564": 108.35000000000011, + "565": 104.95000000000003, + "566": 109.70000000000005, + "567": 111.80000000000008, + "568": 106.05000000000008, + "569": 116.70000000000003, + "570": 67.69999999999993, + "571": 114.10000000000021, + "572": 108.9, + "573": 100.60000000000002, + "574": 84.54999999999997, + "575": 113.30000000000004, + "576": 101.90000000000013, + "577": 115.29999999999995, + "578": 106.54999999999984, + "579": 109.45000000000005, + "580": 105.35000000000007, + "581": 56.299999999999855, + "582": 119.75000000000004, + "583": 117.95000000000014, + "584": 121.90000000000002, + "585": 55.59999999999996, + "586": 109.10000000000007, + "587": 101.65, + "588": 99.85000000000001, + "589": 114.4500000000001, + "590": 114.2000000000001, + "591": 7.550000000000013, + "592": 45.199999999999825, + "593": 11.499999999999938, + "594": 7.550000000000017, + "595": -6.349999999999991, + "596": 0.8500000000000087, + "597": 30.149999999999935, + "598": -7.799999999999986, + "599": 43.99999999999995, + "600": 6.649999999999995, + "601": -0.8499999999999986, + "602": 78.4500000000001, + "603": 56.14999999999995, + "604": 3.3500000000000036, + "605": -2.59999999999999, + "606": 1.049999999999999, + "607": 16.049999999999912, + "608": -3.25000000000001, + "609": 7.999999999999995, + "610": 107.85000000000011, + "611": 101.4, + "612": 42.84999999999995, + "613": 38.549999999999905, + "614": 55.44999999999999, + "615": 59.499999999999986, + "616": 16.450000000000017, + "617": 14.149999999999933, + "618": 31.599999999999923, + "619": 55.799999999999926, + "620": 41.14999999999994, + "621": 9.900000000000004, + "622": 45.499999999999986, + "623": -4.599999999999982, + "624": 118.75000000000001, + "625": 25.349999999999888, + "626": 106.85, + "627": 82.49999999999996, + "628": 38.79999999999987, + "629": 7.599999999999958, + "630": 98.75000000000014, + "631": 37.399999999999906, + "632": 21.499999999999936, + "633": 69.80000000000008, + "634": 107.90000000000022, + "635": 52.09999999999994, + "636": 6.349999999999993, + "637": 75.75000000000004, + "638": 101.09999999999988, + "639": 115.05000000000008, + "640": -1.6542323066914832e-14, + "641": 116.00000000000013, + "642": 32.64999999999988, + "643": 109.15000000000003, + "644": 60.199999999999925, + "645": -3.199999999999987, + "646": 87.80000000000003, + "647": 114.25000000000003, + "648": 112.40000000000008, + "649": 103.50000000000006, + "650": 5.44999999999999, + "651": 32.59999999999995, + "652": 113.25000000000026, + "653": 108.85000000000007, + "654": 110.75000000000011, + "655": 111.55000000000004, + "656": 111.40000000000003, + "657": 107.55000000000007, + "658": 94.50000000000016, + "659": 57.25, + "660": 29.34999999999989, + "661": 118.30000000000011, + "662": 100.70000000000022, + "663": 115.64999999999993, + "664": 112.75000000000004, + "665": 111.90000000000003, + "666": -2.4999999999999973, + "667": 91.10000000000008, + "668": 79.74999999999994, + "669": 1.7000000000000086, + "670": 112.70000000000007, + "671": 114.80000000000014, + "672": 70.25000000000006, + "673": 17.999999999999943, + "674": 109.25000000000009, + "675": 110.44999999999999, + "676": 113.75000000000011, + "677": 111.85000000000012, + "678": 102.95000000000012, + "679": 110.85000000000007, + "680": 110.15000000000015, + "681": 80.5000000000002, + "682": 105.70000000000007, + "683": 111.8500000000002, + "684": 110.0499999999999, + "685": 112.10000000000002, + "686": 111.3000000000001, + "687": 102.14999999999998, + "688": 99.70000000000002, + "689": 113.40000000000013, + "690": 76.29999999999997, + "691": 100.55000000000004, + "692": 116.85000000000012, + "693": 98.15000000000006, + "694": 107.44999999999999, + "695": 104.05000000000007, + "696": 110.20000000000024, + "697": 111.15000000000012, + "698": 111.00000000000011, + "699": 99.95000000000017, + "700": 102.60000000000008, + "701": 102.90000000000006, + "702": 108.9500000000001, + "703": 113.04999999999997, + "704": 107.4000000000002, + "705": 106.85000000000004, + "706": 113.50000000000003, + "707": 111.20000000000019, + "708": 120.10000000000004, + "709": 116.55000000000008, + "710": 115.0, + "711": 115.30000000000013, + "712": 109.35000000000011, + "713": 65.55000000000004, + "714": 112.6, + "715": 111.90000000000018, + "716": 104.45000000000007, + "717": 105.9500000000001, + "718": 99.30000000000005, + "719": 116.65000000000002, + "720": 114.40000000000002, + "721": 112.60000000000025, + "722": 113.44999999999999, + "723": 117.3499999999999, + "724": 68.85000000000007, + "725": 119.35000000000014, + "726": 92.50000000000006, + "727": 108.80000000000001, + "728": 112.90000000000013, + "729": 115.45000000000029, + "730": 114.35000000000001, + "731": 113.70000000000014, + "732": 114.50000000000028, + "733": 110.45000000000014, + "734": 113.60000000000026, + "735": 109.65000000000005, + "736": 111.69999999999997, + "737": 100.70000000000013, + "738": 110.80000000000001, + "739": 102.50000000000006, + "740": 112.20000000000007, + "741": 117.85000000000014, + "742": 106.85000000000004, + "743": 118.15000000000003, + "744": 104.40000000000019, + "745": 20.749999999999943, + "746": 112.25, + "747": 109.75000000000006, + "748": 98.79999999999997, + "749": 111.34999999999997, + "750": 104.59999999999995, + "751": 102.95, + "752": 106.3500000000002, + "753": 11.650000000000034, + "754": 116.75000000000003, + "755": 101.20000000000019, + "756": 111.6000000000001, + "757": 106.30000000000015, + "758": 113.95000000000007, + "759": 106.60000000000012, + "760": 104.55000000000005, + "761": 107.70000000000006, + "762": 112.95000000000016, + "763": 110.05000000000003, + "764": 113.20000000000014, + "765": 47.599999999999945, + "766": 114.30000000000008, + "767": 111.10000000000011, + "768": 112.55000000000007, + "769": 111.50000000000003, + "770": 91.80000000000018, + "771": 119.40000000000003, + "772": 76.39999999999989, + "773": 118.85000000000012, + "774": 116.45, + "775": 109.59999999999998, + "776": 116.55000000000001, + "777": 112.60000000000015, + "778": 105.64999999999995, + "779": 69.94999999999993, + "780": 109.85000000000005, + "781": 111.75000000000014, + "782": 116.14999999999996, + "783": 101.80000000000001, + "784": 49.04999999999993, + "785": 62.25, + "786": 114.50000000000001, + "787": 112.05, + "788": 108.85000000000002, + "789": 118.45000000000006, + "790": 117.30000000000007, + "791": 112.90000000000013, + "792": 108.90000000000008, + "793": 110.55000000000008, + "794": 114.60000000000007, + "795": 114.30000000000003, + "796": 113.50000000000016, + "797": 114.84999999999994, + "798": 116.50000000000004, + "799": 111.04999999999994, + "800": 113.25000000000007, + "801": 112.05000000000024, + "802": 115.55, + "803": 108.50000000000006, + "804": 116.55000000000008, + "805": 109.2500000000001, + "806": 100.95000000000003, + "807": 112.60000000000008, + "808": 114.15000000000012, + "809": 111.80000000000008, + "810": 83.30000000000001, + "811": 111.75000000000004, + "812": 107.55000000000013, + "813": 112.05000000000008, + "814": 17.350000000000037, + "815": 110.55000000000001, + "816": 111.20000000000014, + "817": 112.00000000000003, + "818": 110.1000000000002, + "819": 106.00000000000001, + "820": 114.30000000000004, + "821": 105.54999999999995, + "822": 101.95000000000013, + "823": 96.69999999999996, + "824": 113.40000000000012, + "825": 52.699999999999854, + "826": 41.34999999999981, + "827": 101.90000000000012, + "828": 105.35000000000012, + "829": 111.29999999999995, + "830": 111.8, + "831": 112.85, + "832": 67.69999999999975, + "833": 112.85000000000005, + "834": 105.40000000000012, + "835": 112.85, + "836": 95.84999999999998, + "837": 95.95000000000006, + "838": 100.40000000000013, + "839": 107.00000000000001, + "840": 111.00000000000004, + "841": 114.95000000000027, + "842": 107.65000000000003, + "843": 111.65000000000009, + "844": 105.15000000000008, + "845": 106.30000000000004, + "846": 107.65000000000013, + "847": 113.90000000000019, + "848": 120.30000000000007, + "849": 108.80000000000003, + "850": 105.74999999999999, + "851": 86.49999999999994, + "852": 115.50000000000011, + "853": 116.00000000000016, + "854": 122.55000000000011, + "855": 112.6500000000001, + "856": 108.15, + "857": 83.30000000000001, + "858": 114.8000000000001, + "859": 116.40000000000005, + "860": 108.70000000000009, + "861": 116.45000000000003, + "862": 102.40000000000002, + "863": 113.60000000000011, + "864": 112.15000000000009, + "865": 110.20000000000002, + "866": 113.19999999999993, + "867": 113.00000000000003, + "868": 111.55000000000015, + "869": 117.75000000000006, + "870": 116.8000000000001, + "871": 110.45000000000017, + "872": 114.00000000000013, + "873": 118.70000000000026, + "874": 113.95000000000012, + "875": 113.00000000000001, + "876": 112.60000000000014, + "877": 107.50000000000009, + "878": 112.49999999999999, + "879": 116.94999999999999, + "880": 107.10000000000014, + "881": 106.70000000000013, + "882": 106.05000000000015, + "883": 111.7000000000001, + "884": 112.85000000000015, + "885": 112.7500000000001, + "886": 109.8500000000001, + "887": 111.55000000000008, + "888": 112.30000000000005, + "889": 110.10000000000001, + "890": 110.60000000000024, + "891": 114.55, + "892": 49.55000000000004, + "893": 115.2500000000001, + "894": 104.69999999999996, + "895": 109.15000000000008, + "896": 119.15000000000003, + "897": 100.55000000000003, + "898": 119.84999999999998, + "899": 116.70000000000003, + "900": 112.85000000000007, + "901": 115.74999999999997, + "902": 118.5500000000001, + "903": 106.89999999999996, + "904": 110.29999999999997, + "905": 115.04999999999998, + "906": 112.35000000000011, + "907": 80.84999999999994, + "908": 109.55000000000008, + "909": 109.49999999999994, + "910": 102.29999999999998, + "911": 108.25000000000001, + "912": 104.90000000000008, + "913": 109.20000000000009, + "914": 118.90000000000015, + "915": 108.95000000000014, + "916": 109.24999999999994, + "917": 104.50000000000018, + "918": 100.55000000000015, + "919": 111.64999999999998, + "920": 112.80000000000014, + "921": 97.7, + "922": 114.40000000000013, + "923": 110.60000000000001, + "924": 118.20000000000009, + "925": 108.90000000000005, + "926": 110.24999999999999, + "927": 102.64999999999982, + "928": 110.95000000000002, + "929": 110.20000000000005, + "930": 97.55, + "931": 114.70000000000006, + "932": 96.09999999999998, + "933": 88.59999999999991, + "934": 110.6000000000001, + "935": -8.149999999999997, + "936": 112.25, + "937": 114.44999999999995, + "938": 111.9500000000001, + "939": 115.40000000000006, + "940": 117.05000000000015, + "941": 70.84999999999978, + "942": 121.45000000000014, + "943": 72.14999999999992, + "944": 110.30000000000011, + "945": 109.19999999999995, + "946": 109.3000000000001, + "947": 105.3500000000001, + "948": 118.29999999999998, + "949": 116.50000000000018, + "950": 111.85, + "951": 106.59999999999994, + "952": 66.55000000000011, + "953": 109.50000000000007, + "954": 89.90000000000006, + "955": 108.9000000000001, + "956": 108.95000000000017, + "957": 99.69999999999996, + "958": 104.95000000000014, + "959": 109.55000000000017, + "960": 107.94999999999999, + "961": 111.85000000000016, + "962": 96.05000000000004, + "963": 114.35, + "964": 119.25000000000007, + "965": 115.3, + "966": 108.30000000000018, + "967": 108.85000000000015, + "968": 114.40000000000003, + "969": 109.8, + "970": 111.40000000000009, + "971": 102.10000000000011, + "972": 111.75000000000013, + "973": 112.60000000000014, + "974": 113.00000000000009, + "975": 110.10000000000011, + "976": 100.70000000000005, + "977": 27.900000000000002, + "978": 120.10000000000014, + "979": 113.35000000000008, + "980": 113.19999999999993, + "981": 51.39999999999995, + "982": 111.25000000000017, + "983": 114.05000000000007, + "984": 112.95000000000016, + "985": 105.80000000000007, + "986": 118.20000000000012, + "987": 110.4, + "988": 113.35000000000004, + "989": 108.35000000000005, + "990": 118.95000000000014, + "991": 107.3500000000002, + "992": 102.2000000000001, + "993": 112.35000000000005, + "994": 95.69999999999989, + "995": 106.35000000000015, + "996": 113.55, + "997": 117.9500000000001, + "998": 112.35000000000011, + "999": 115.20000000000002, + "1000": 102.64999999999988 + }, + "4": { + "1": -88.3, + "2": -78.44999999999999, + "3": -61.60000000000013, + "4": -17.999999999999968, + "5": -21.299999999999958, + "6": -21.499999999999957, + "7": -6.500000000000002, + "8": -50.60000000000009, + "9": -20.799999999999958, + "10": -52.50000000000008, + "11": -54.60000000000007, + "12": -17.84999999999996, + "13": -79.69999999999996, + "14": -24.249999999999947, + "15": -57.80000000000017, + "16": -23.14999999999996, + "17": -16.99999999999997, + "18": -52.100000000000115, + "19": -88.49999999999997, + "20": -15.949999999999974, + "21": -14.149999999999984, + "22": -24.14999999999995, + "23": -17.89999999999998, + "24": -8.249999999999972, + "25": -11.549999999999986, + "26": -97.44999999999996, + "27": -16.699999999999978, + "28": -8.849999999999987, + "29": -16.449999999999974, + "30": -8.900000000000004, + "31": -16.24999999999999, + "32": -8.849999999999998, + "33": -55.550000000000075, + "34": -19.79999999999996, + "35": -11.799999999999988, + "36": -32.45000000000004, + "37": -63.150000000000105, + "38": -6.25000000000001, + "39": -97.9, + "40": -17.59999999999997, + "41": -23.49999999999995, + "42": -3.9499999999999806, + "43": -18.799999999999965, + "44": -99.0, + "45": -21.249999999999957, + "46": -17.599999999999973, + "47": -20.94999999999996, + "48": -55.05, + "49": -93.09999999999998, + "50": -17.899999999999963, + "51": -46.900000000000006, + "52": -73.60000000000002, + "53": -61.09999999999998, + "54": -94.55, + "55": -19.649999999999963, + "56": -19.949999999999967, + "57": -4.849999999999982, + "58": -9.449999999999992, + "59": -17.049999999999972, + "60": -25.69999999999994, + "61": -21.049999999999958, + "62": -17.74999999999997, + "63": -91.25, + "64": -18.54999999999997, + "65": -71.75000000000003, + "66": -23.49999999999995, + "67": -8.799999999999985, + "68": -14.549999999999978, + "69": -19.699999999999964, + "70": -20.149999999999988, + "71": -95.95000000000006, + "72": -60.55, + "73": -22.649999999999952, + "74": -21.949999999999957, + "75": 1.0000000000000002, + "76": -13.749999999999986, + "77": -9.049999999999994, + "78": -81.24999999999999, + "79": -99.44999999999999, + "80": -14.549999999999985, + "81": -17.99999999999997, + "82": -10.299999999999999, + "83": -18.599999999999973, + "84": -19.399999999999974, + "85": -13.29999999999998, + "86": -17.449999999999964, + "87": -6.199999999999989, + "88": -13.64999999999999, + "89": -15.29999999999998, + "90": -21.299999999999958, + "91": -22.649999999999952, + "92": -37.04999999999996, + "93": -43.75, + "94": -18.949999999999967, + "95": 7.350000000000055, + "96": 0.45000000000000284, + "97": -15.599999999999978, + "98": -10.99999999999999, + "99": -23.39999999999995, + "100": 8.149999999999983, + "101": -21.499999999999957, + "102": -5.899999999999998, + "103": -60.800000000000004, + "104": -17.399999999999977, + "105": -11.449999999999996, + "106": -3.8499999999999934, + "107": -5.199999999999982, + "108": 6.199999999999969, + "109": -18.09999999999997, + "110": -7.749999999999988, + "111": -21.899999999999956, + "112": -4.249999999999994, + "113": -15.649999999999974, + "114": 13.999999999999961, + "115": -17.449999999999974, + "116": -76.54999999999998, + "117": -16.999999999999975, + "118": 0.5500000000000362, + "119": -96.30000000000001, + "120": -10.299999999999992, + "121": -9.849999999999998, + "122": 1.1000000000000212, + "123": -19.29999999999997, + "124": -51.999999999999986, + "125": -63.09999999999992, + "126": -6.800000000000003, + "127": 4.050000000000029, + "128": -26.9, + "129": -14.49999999999999, + "130": -5.649999999999986, + "131": -11.649999999999988, + "132": 16.399999999999977, + "133": -7.700000000000009, + "134": -20.84999999999996, + "135": -86.35, + "136": -1.7499999999999682, + "137": -10.099999999999994, + "138": -20.84999999999996, + "139": -11.149999999999988, + "140": -51.24999999999998, + "141": -57.450000000000074, + "142": -59.95, + "143": -61.0500000000001, + "144": -13.949999999999996, + "145": -44.30000000000004, + "146": -12.449999999999985, + "147": -13.099999999999984, + "148": -88.30000000000001, + "149": 29.049999999999894, + "150": -14.549999999999974, + "151": -17.399999999999952, + "152": -4.199999999999987, + "153": -8.450000000000014, + "154": -54.95000000000001, + "155": -10.699999999999996, + "156": -13.64999999999999, + "157": -83.3, + "158": -77.10000000000002, + "159": 19.20000000000006, + "160": -89.9, + "161": -21.899999999999956, + "162": 2.450000000000035, + "163": -0.9999999999999734, + "164": -82.25, + "165": -17.69999999999997, + "166": 24.500000000000004, + "167": -94.69999999999997, + "168": -35.55000000000001, + "169": 3.6499999999999897, + "170": -58.60000000000009, + "171": 9.400000000000055, + "172": 4.549999999999919, + "173": -36.60000000000006, + "174": -41.00000000000005, + "175": -20.550000000000004, + "176": 42.949999999999854, + "177": -2.3499999999999632, + "178": 25.49999999999991, + "179": -24.199999999999953, + "180": -82.60000000000001, + "181": -79.80000000000005, + "182": -80.19999999999999, + "183": -90.00000000000004, + "184": -55.65000000000001, + "185": 5.35000000000006, + "186": -10.599999999999994, + "187": -1.1999999999999567, + "188": -74.04999999999995, + "189": 3.849999999999965, + "190": 33.14999999999997, + "191": -12.94999999999999, + "192": -88.75000000000006, + "193": -39.90000000000005, + "194": -61.14999999999996, + "195": -93.74999999999997, + "196": -67.25000000000001, + "197": -17.799999999999976, + "198": -7.799999999999997, + "199": 11.099999999999955, + "200": 60.500000000000085, + "201": -24.500000000000046, + "202": -77.94999999999999, + "203": 8.100000000000044, + "204": -23.799999999999947, + "205": -9.949999999999998, + "206": -22.049999999999955, + "207": -7.25, + "208": -6.500000000000011, + "209": 14.650000000000045, + "210": -33.500000000000036, + "211": -34.94999999999998, + "212": -18.80000000000001, + "213": 16.199999999999925, + "214": -69.09999999999991, + "215": -26.499999999999993, + "216": -57.64999999999995, + "217": -78.80000000000001, + "218": -52.29999999999998, + "219": 2.2000000000000286, + "220": -75.5, + "221": -5.999999999999991, + "222": -60.20000000000003, + "223": 25.90000000000001, + "224": -6.9999999999999805, + "225": -89.2, + "226": -65.49999999999989, + "227": 62.649999999999885, + "228": -59.1, + "229": 5.9499999999999895, + "230": 5.449999999999991, + "231": -69.70000000000003, + "232": 16.650000000000027, + "233": -65.65, + "234": -50.750000000000014, + "235": -91.8, + "236": -63.10000000000002, + "237": 1.8500000000000345, + "238": -3.850000000000064, + "239": 22.999999999999805, + "240": -48.10000000000006, + "241": 21.850000000000033, + "242": -18.35000000000004, + "243": -72.15000000000002, + "244": -19.65000000000002, + "245": -81.54999999999998, + "246": -82.14999999999998, + "247": -63.40000000000003, + "248": -35.45000000000002, + "249": 18.00000000000001, + "250": -12.24999999999999, + "251": -15.899999999999975, + "252": 5.30000000000001, + "253": -16.29999999999998, + "254": -71.85000000000002, + "255": -64.99999999999996, + "256": 11.650000000000006, + "257": 1.6431300764452317e-14, + "258": 40.499999999999815, + "259": 21.899999999999974, + "260": -30.40000000000009, + "261": -57.69999999999998, + "262": 55.14999999999978, + "263": 31.349999999999795, + "264": 16.000000000000025, + "265": -14.250000000000025, + "266": -33.50000000000003, + "267": 11.750000000000016, + "268": -40.1, + "269": -72.89999999999999, + "270": -12.149999999999991, + "271": -74.6999999999999, + "272": 17.350000000000005, + "273": -11.649999999999961, + "274": -69.14999999999998, + "275": -62.8, + "276": -73.35000000000002, + "277": 18.50000000000003, + "278": 84.69999999999979, + "279": -78.19999999999999, + "280": -55.99999999999995, + "281": 22.24999999999989, + "282": 8.999999999999998, + "283": -34.75000000000002, + "284": 4.550000000000049, + "285": -10.39999999999997, + "286": -20.699999999999996, + "287": 2.400000000000027, + "288": 5.150000000000025, + "289": 1.950000000000022, + "290": -49.15, + "291": 19.70000000000004, + "292": 19.249999999999975, + "293": -30.650000000000023, + "294": 19.449999999999992, + "295": 71.89999999999985, + "296": 30.750000000000032, + "297": 6.350000000000021, + "298": -42.299999999999976, + "299": 46.899999999999835, + "300": -83.29999999999998, + "301": -51.74999999999998, + "302": -68.84999999999997, + "303": -34.95, + "304": 8.850000000000065, + "305": 66.49999999999997, + "306": 18.150000000000055, + "307": -38.549999999999955, + "308": -14.05000000000004, + "309": 15.250000000000071, + "310": -1.8999999999999382, + "311": -2.3499999999999543, + "312": -59.44999999999996, + "313": -7.699999999999989, + "314": 12.500000000000021, + "315": 59.79999999999999, + "316": 50.99999999999974, + "317": -49.24999999999995, + "318": -84.79999999999998, + "319": -82.55000000000001, + "320": 7.45000000000005, + "321": -63.84999999999997, + "322": -43.64999999999999, + "323": -92.05, + "324": 41.19999999999977, + "325": -75.6, + "326": -6.3499999999999845, + "327": 16.800000000000008, + "328": -10.900000000000002, + "329": -26.049999999999986, + "330": 6.650000000000029, + "331": -68.70000000000002, + "332": 7.45000000000005, + "333": -73.35000000000001, + "334": 22.799999999999915, + "335": -54.39999999999998, + "336": -66.99999999999991, + "337": -75.5, + "338": 67.44999999999995, + "339": -55.09999999999995, + "340": -42.15000000000001, + "341": -85.85000000000002, + "342": -47.39999999999994, + "343": 0.10000000000002318, + "344": 60.74999999999988, + "345": -37.14999999999996, + "346": 57.14999999999979, + "347": 100.45000000000013, + "348": -32.450000000000024, + "349": 61.499999999999815, + "350": 23.199999999999974, + "351": -30.900000000000002, + "352": 6.449999999999954, + "353": 13.850000000000115, + "354": 34.800000000000026, + "355": -10.750000000000037, + "356": 15.450000000000012, + "357": -20.99999999999998, + "358": 26.75, + "359": -52.849999999999966, + "360": -60.4, + "361": 60.04999999999992, + "362": 27.249999999999886, + "363": -62.199999999999896, + "364": 74.85, + "365": -21.550000000000022, + "366": 62.699999999999974, + "367": -44.59999999999998, + "368": -62.69999999999995, + "369": 58.54999999999974, + "370": -69.65, + "371": -20.899999999999928, + "372": -16.750000000000032, + "373": -2.9999999999999973, + "374": 35.84999999999978, + "375": 7.59999999999997, + "376": 56.09999999999981, + "377": -40.700000000000074, + "378": 58.19999999999996, + "379": 22.800000000000047, + "380": 85.55000000000005, + "381": 79.44999999999973, + "382": -25.95000000000004, + "383": 42.44999999999986, + "384": 32.199999999999754, + "385": 7.05000000000005, + "386": -65.84999999999991, + "387": 98.69999999999973, + "388": 2.8999999999999675, + "389": 59.199999999999775, + "390": 12.29999999999999, + "391": 73.34999999999981, + "392": 36.99999999999997, + "393": 96.69999999999973, + "394": 42.84999999999977, + "395": -23.49999999999994, + "396": 5.150000000000007, + "397": 73.8499999999998, + "398": -47.99999999999994, + "399": 81.15000000000003, + "400": -10.250000000000023, + "401": -55.30000000000001, + "402": 69.94999999999996, + "403": 16.04999999999995, + "404": 17.94999999999997, + "405": 58.1499999999998, + "406": 3.6500000000000163, + "407": -28.950000000000063, + "408": -45.950000000000095, + "409": 51.90000000000002, + "410": 58.849999999999866, + "411": 93.74999999999972, + "412": 1.7000000000000013, + "413": -11.400000000000016, + "414": 17.250000000000046, + "415": 8.399999999999967, + "416": -65.70000000000002, + "417": 97.54999999999977, + "418": 63.49999999999979, + "419": -41.25000000000003, + "420": 38.34999999999996, + "421": 25.50000000000001, + "422": 5.85000000000004, + "423": -10.34999999999994, + "424": 113.75000000000013, + "425": 91.14999999999978, + "426": 77.60000000000011, + "427": 93.74999999999974, + "428": 86.59999999999978, + "429": 68.34999999999972, + "430": 111.74999999999979, + "431": 110.94999999999986, + "432": 92.4000000000001, + "433": 95.25000000000013, + "434": 60.549999999999855, + "435": 59.84999999999999, + "436": -38.15000000000002, + "437": 91.24999999999974, + "438": -12.849999999999984, + "439": 80.79999999999986, + "440": 87.94999999999975, + "441": 103.79999999999976, + "442": 115.2, + "443": 14.149999999999999, + "444": 97.59999999999998, + "445": -8.300000000000017, + "446": 5.3000000000000345, + "447": 95.74999999999972, + "448": 46.69999999999977, + "449": 89.84999999999988, + "450": 59.14999999999998, + "451": 58.19999999999991, + "452": 81.89999999999978, + "453": 106.59999999999971, + "454": 48.99999999999993, + "455": 103.59999999999981, + "456": -40.000000000000014, + "457": 23.30000000000003, + "458": 29.900000000000063, + "459": 105.54999999999974, + "460": -11.549999999999962, + "461": 60.79999999999976, + "462": 86.69999999999978, + "463": 41.04999999999997, + "464": -27.25000000000002, + "465": 104.04999999999981, + "466": -27.650000000000016, + "467": -3.3500000000000316, + "468": 84.35000000000014, + "469": 77.80000000000004, + "470": 83.64999999999974, + "471": 66.09999999999975, + "472": -49.750000000000085, + "473": 75.44999999999985, + "474": 81.49999999999987, + "475": 99.64999999999978, + "476": -1.8499999999999952, + "477": 86.69999999999975, + "478": 75.34999999999982, + "479": 103.39999999999974, + "480": 90.59999999999975, + "481": 97.60000000000001, + "482": 88.44999999999979, + "483": 105.8499999999998, + "484": 75.69999999999975, + "485": 60.599999999999824, + "486": 105.09999999999974, + "487": 102.14999999999975, + "488": 106.64999999999972, + "489": 1.4499999999999973, + "490": 103.24999999999991, + "491": 80.89999999999979, + "492": 73.14999999999975, + "493": 88.54999999999977, + "494": 73.39999999999982, + "495": 59.099999999999724, + "496": 102.59999999999977, + "497": 96.49999999999976, + "498": 87.39999999999975, + "499": 105.84999999999981, + "500": 48.39999999999987, + "501": 101.14999999999976, + "502": 96.14999999999986, + "503": 105.09999999999974, + "504": 102.44999999999975, + "505": 70.54999999999993, + "506": 55.6999999999999, + "507": 92.24999999999984, + "508": 112.09999999999981, + "509": 96.54999999999976, + "510": 106.24999999999973, + "511": 83.54999999999973, + "512": 104.14999999999976, + "513": 103.54999999999977, + "514": 91.89999999999995, + "515": 99.64999999999976, + "516": 80.99999999999976, + "517": 105.09999999999982, + "518": 84.49999999999982, + "519": 99.64999999999976, + "520": 104.89999999999976, + "521": 103.54999999999977, + "522": 108.74999999999974, + "523": 85.44999999999973, + "524": 97.3999999999998, + "525": 102.79999999999977, + "526": 103.99999999999976, + "527": 80.1499999999998, + "528": 112.09999999999981, + "529": 99.59999999999977, + "530": 101.39999999999975, + "531": 97.39999999999972, + "532": 100.79999999999977, + "533": 106.39999999999972, + "534": 104.99999999999977, + "535": 62.59999999999974, + "536": 105.59999999999974, + "537": 84.59999999999991, + "538": 43.199999999999946, + "539": 79.74999999999993, + "540": 92.34999999999977, + "541": -40.00000000000001, + "542": 107.44999999999978, + "543": 93.84999999999995, + "544": 104.64999999999978, + "545": 105.89999999999975, + "546": 100.79999999999974, + "547": 65.9499999999998, + "548": 106.54999999999973, + "549": 100.04999999999977, + "550": 71.94999999999987, + "551": 97.94999999999992, + "552": 99.09999999999975, + "553": 106.94999999999975, + "554": 92.14999999999974, + "555": 109.50000000000009, + "556": 103.29999999999977, + "557": 103.49999999999977, + "558": 111.15000000000006, + "559": 88.29999999999978, + "560": 99.84999999999974, + "561": 89.04999999999981, + "562": 46.94999999999975, + "563": 99.0000000000001, + "564": 113.00000000000018, + "565": 107.59999999999972, + "566": 98.09999999999978, + "567": 106.10000000000007, + "568": 107.04999999999978, + "569": 106.49999999999973, + "570": 64.09999999999992, + "571": 104.94999999999976, + "572": 68.59999999999995, + "573": 49.799999999999805, + "574": 18.150000000000034, + "575": 99.19999999999976, + "576": 1.5499999999999903, + "577": 103.69999999999976, + "578": 1.5500000000000538, + "579": 111.64999999999978, + "580": 89.24999999999977, + "581": 100.39999999999976, + "582": 102.79999999999991, + "583": 112.25000000000021, + "584": 64.59999999999992, + "585": 108.79999999999986, + "586": 108.89999999999984, + "587": 117.20000000000027, + "588": 85.94999999999976, + "589": 98.65000000000008, + "590": 112.55000000000005, + "591": 108.29999999999973, + "592": 105.39999999999979, + "593": 101.04999999999976, + "594": 106.99999999999974, + "595": 107.14999999999978, + "596": 72.29999999999994, + "597": 89.94999999999986, + "598": 109.04999999999984, + "599": 105.19999999999975, + "600": 107.69999999999976, + "601": 63.09999999999971, + "602": 96.34999999999978, + "603": 103.89999999999975, + "604": 102.9500000000001, + "605": 105.79999999999974, + "606": 109.30000000000001, + "607": 105.39999999999975, + "608": 73.19999999999999, + "609": 106.7499999999998, + "610": 96.75000000000018, + "611": 105.8499999999999, + "612": 103.14999999999976, + "613": 102.84999999999975, + "614": 104.94999999999976, + "615": 105.39999999999999, + "616": 111.10000000000007, + "617": 107.89999999999995, + "618": 100.80000000000001, + "619": 62.34999999999977, + "620": 111.50000000000007, + "621": 104.64999999999972, + "622": 115.34999999999992, + "623": 112.69999999999997, + "624": 108.19999999999973, + "625": 108.19999999999979, + "626": 105.99999999999994, + "627": 109.59999999999998, + "628": 103.30000000000008, + "629": 109.34999999999998, + "630": 89.59999999999988, + "631": 107.59999999999975, + "632": 107.44999999999973, + "633": 104.94999999999975, + "634": 104.09999999999977, + "635": 108.14999999999975, + "636": 103.10000000000005, + "637": 102.35000000000005, + "638": 98.79999999999977, + "639": 106.69999999999973, + "640": 107.24999999999973, + "641": 107.4499999999998, + "642": 102.64999999999976, + "643": 108.84999999999978, + "644": 103.29999999999977, + "645": 91.54999999999983, + "646": 109.49999999999974, + "647": 107.69999999999987, + "648": 97.14999999999978, + "649": 107.10000000000022, + "650": 112.1500000000001, + "651": 105.59999999999974, + "652": 107.30000000000004, + "653": -72.25000000000003, + "654": 107.20000000000016, + "655": 109.49999999999991, + "656": 112.95000000000009, + "657": 109.80000000000001, + "658": 107.24999999999989, + "659": 111.50000000000009, + "660": 116.40000000000026, + "661": 116.3500000000001, + "662": 62.04999999999983, + "663": 104.7000000000002, + "664": 74.59999999999997, + "665": 90.09999999999974, + "666": 100.7499999999999, + "667": 60.29999999999991, + "668": 110.74999999999983, + "669": 106.59999999999975, + "670": 110.70000000000006, + "671": 97.94999999999976, + "672": 116.85000000000015, + "673": 110.35000000000001, + "674": 56.799999999999756, + "675": 109.64999999999986, + "676": 86.40000000000002, + "677": 108.45, + "678": 108.24999999999991, + "679": 46.59999999999973, + "680": 115.65000000000005, + "681": 112.10000000000002, + "682": 108.00000000000007, + "683": 111.0, + "684": 109.4999999999999, + "685": 111.10000000000008, + "686": 109.89999999999992, + "687": 111.54999999999986, + "688": 107.49999999999976, + "689": 105.40000000000002, + "690": 79.29999999999998, + "691": 111.15000000000022, + "692": 90.50000000000014, + "693": 85.20000000000009, + "694": -0.19999999999996576, + "695": 116.10000000000002, + "696": 108.0499999999998, + "697": 112.40000000000003, + "698": 109.14999999999999, + "699": 112.05000000000014, + "700": 99.54999999999998, + "701": 109.75000000000003, + "702": 114.19999999999993, + "703": 110.75000000000011, + "704": 122.45000000000006, + "705": 94.0499999999999, + "706": 107.29999999999977, + "707": 112.75000000000024, + "708": 110.55000000000004, + "709": 104.4999999999999, + "710": 104.05000000000018, + "711": 114.10000000000012, + "712": 112.55000000000003, + "713": 110.6000000000002, + "714": 104.59999999999982, + "715": 113.1000000000002, + "716": 109.8000000000002, + "717": 112.8000000000002, + "718": 91.75000000000006, + "719": 99.00000000000007, + "720": 98.2500000000001, + "721": 88.24999999999999, + "722": 98.45000000000007, + "723": 107.29999999999976, + "724": 109.55, + "725": 19.300000000000004, + "726": 95.39999999999988, + "727": 112.60000000000024, + "728": 113.60000000000018, + "729": 108.80000000000013, + "730": 108.85000000000002, + "731": 107.00000000000013, + "732": 111.39999999999996, + "733": 105.55000000000018, + "734": 95.75000000000009, + "735": 107.70000000000019, + "736": 97.75000000000001, + "737": 97.35000000000004, + "738": 111.05000000000007, + "739": 99.40000000000016, + "740": 102.39999999999998, + "741": 112.2000000000001, + "742": 85.64999999999993, + "743": 107.20000000000006, + "744": 107.90000000000016, + "745": 83.19999999999999, + "746": 100.15000000000018, + "747": 100.99999999999976, + "748": 94.60000000000002, + "749": 110.55000000000024, + "750": 100.65000000000023, + "751": 86.7, + "752": 86.10000000000004, + "753": 104.75000000000016, + "754": 98.20000000000003, + "755": 99.54999999999995, + "756": 113.40000000000013, + "757": 111.95000000000005, + "758": 106.85000000000007, + "759": 105.00000000000016, + "760": 112.85000000000015, + "761": 111.75000000000011, + "762": 105.54999999999973, + "763": 104.2000000000001, + "764": 114.30000000000027, + "765": 113.60000000000011, + "766": 97.99999999999993, + "767": 112.35000000000024, + "768": 95.6, + "769": 109.84999999999995, + "770": 113.05000000000011, + "771": 57.799999999999976, + "772": 97.75000000000009, + "773": 114.35000000000018, + "774": 112.55000000000011, + "775": 108.95000000000002, + "776": 94.55000000000014, + "777": 99.85000000000012, + "778": 113.3500000000002, + "779": 102.70000000000017, + "780": 109.20000000000013, + "781": 101.4500000000001, + "782": 114.10000000000002, + "783": 92.20000000000003, + "784": 106.64999999999998, + "785": 105.95000000000012, + "786": 113.70000000000007, + "787": 117.80000000000008, + "788": 121.95000000000019, + "789": 96.99999999999983, + "790": 111.90000000000005, + "791": -4.449999999999992, + "792": 77.60000000000005, + "793": -13.999999999999984, + "794": 79.89999999999989, + "795": 110.35000000000008, + "796": 100.30000000000007, + "797": 108.1, + "798": 115.70000000000006, + "799": 46.099999999999774, + "800": 91.54999999999978, + "801": 30.349999999999792, + "802": 54.19999999999978, + "803": 109.40000000000006, + "804": 69.19999999999999, + "805": 57.74999999999975, + "806": 99.95000000000003, + "807": 93.6999999999999, + "808": 82.65000000000002, + "809": 110.75000000000011, + "810": 102.60000000000016, + "811": 98.89999999999988, + "812": 50.1999999999998, + "813": 119.15000000000005, + "814": 101.45, + "815": 114.05000000000028, + "816": 106.44999999999986, + "817": 104.64999999999989, + "818": 91.6500000000001, + "819": -9.500000000000005, + "820": 118.60000000000002, + "821": 116.09999999999997, + "822": 113.94999999999996, + "823": 116.80000000000018, + "824": 110.5500000000001, + "825": 48.799999999999855, + "826": 72.84999999999991, + "827": 93.25000000000016, + "828": 106.90000000000008, + "829": 26.050000000000008, + "830": 107.10000000000014, + "831": 111.50000000000011, + "832": 107.39999999999993, + "833": 103.9500000000002, + "834": 103.80000000000015, + "835": 108.59999999999995, + "836": 108.2, + "837": 107.05000000000018, + "838": 109.40000000000009, + "839": 109.3500000000001, + "840": 108.55000000000004, + "841": 107.30000000000014, + "842": 107.59999999999997, + "843": 108.90000000000015, + "844": 101.15000000000016, + "845": 108.59999999999997, + "846": 104.0000000000002, + "847": 113.90000000000026, + "848": 92.89999999999999, + "849": 108.64999999999999, + "850": 96.40000000000015, + "851": 109.1000000000001, + "852": 91.90000000000018, + "853": 103.45000000000014, + "854": 102.50000000000011, + "855": 106.90000000000003, + "856": 112.90000000000006, + "857": 79.85000000000002, + "858": 106.50000000000003, + "859": 106.00000000000016, + "860": 102.80000000000018, + "861": 109.79999999999991, + "862": 112.55000000000011, + "863": 102.9500000000001, + "864": 111.00000000000017, + "865": 111.90000000000006, + "866": 103.40000000000002, + "867": 87.09999999999995, + "868": 106.69999999999983, + "869": 115.10000000000011, + "870": 110.1999999999999, + "871": 114.50000000000017, + "872": 96.70000000000007, + "873": 109.5000000000001, + "874": 108.20000000000003, + "875": 110.75000000000006, + "876": 109.35000000000011, + "877": 109.35000000000012, + "878": 112.80000000000004, + "879": 117.99999999999996, + "880": 45.04999999999988, + "881": 39.900000000000006, + "882": 115.1, + "883": 108.25000000000017, + "884": 111.95, + "885": 112.15000000000013, + "886": 122.84999999999997, + "887": 110.80000000000004, + "888": 105.1, + "889": 120.40000000000012, + "890": 113.40000000000006, + "891": 113.45000000000009, + "892": 109.59999999999998, + "893": 106.19999999999996, + "894": 107.35000000000001, + "895": 112.50000000000003, + "896": 114.75000000000018, + "897": 120.6000000000002, + "898": 107.75000000000013, + "899": 111.10000000000012, + "900": 112.9000000000001, + "901": 111.60000000000005, + "902": 114.85000000000004, + "903": 112.85000000000002, + "904": 119.55000000000013, + "905": 117.30000000000004, + "906": 108.60000000000011, + "907": 103.45000000000006, + "908": 116.95000000000009, + "909": 113.75000000000013, + "910": 98.75000000000004, + "911": 74.19999999999996, + "912": 91.95, + "913": 67.64999999999986, + "914": 114.20000000000009, + "915": 105.7, + "916": 111.15000000000002, + "917": 102.25000000000004, + "918": 106.80000000000001, + "919": 106.50000000000001, + "920": 108.55000000000005, + "921": 98.45000000000002, + "922": 109.84999999999997, + "923": 99.05000000000005, + "924": 122.60000000000001, + "925": 114.85000000000022, + "926": 108.44999999999993, + "927": 99.99999999999994, + "928": 110.05000000000025, + "929": 95.64999999999982, + "930": 111.55000000000004, + "931": 112.80000000000005, + "932": 117.2500000000002, + "933": 109.00000000000001, + "934": 103.60000000000001, + "935": 108.19999999999999, + "936": 113.1500000000001, + "937": 103.10000000000004, + "938": 106.90000000000009, + "939": 102.30000000000001, + "940": 86.45, + "941": 96.80000000000003, + "942": 115.00000000000007, + "943": 108.25000000000003, + "944": 106.54999999999998, + "945": 106.50000000000003, + "946": 117.50000000000017, + "947": 110.20000000000013, + "948": 113.50000000000006, + "949": 107.35000000000011, + "950": 114.90000000000013, + "951": 114.50000000000001, + "952": 116.50000000000007, + "953": 98.49999999999999, + "954": 113.45000000000013, + "955": 110.69999999999997, + "956": 117.35000000000008, + "957": 112.55000000000013, + "958": 114.2000000000001, + "959": 107.94999999999987, + "960": 107.50000000000011, + "961": 111.7500000000001, + "962": 98.25000000000016, + "963": 101.80000000000014, + "964": 112.00000000000006, + "965": 106.20000000000006, + "966": 99.4, + "967": 111.10000000000005, + "968": 111.55000000000007, + "969": 114.00000000000004, + "970": 113.85000000000012, + "971": 115.25000000000016, + "972": 104.60000000000002, + "973": 111.85000000000005, + "974": 109.54999999999993, + "975": 109.24999999999994, + "976": 104.84999999999998, + "977": 112.49999999999997, + "978": 108.60000000000022, + "979": 103.84999999999997, + "980": 109.85000000000015, + "981": 108.65000000000005, + "982": 109.5500000000002, + "983": 111.00000000000014, + "984": 108.75000000000014, + "985": 113.75000000000013, + "986": 105.85000000000004, + "987": 95.80000000000007, + "988": 116.80000000000005, + "989": 110.35000000000011, + "990": 109.20000000000014, + "991": 100.09999999999995, + "992": 102.65000000000013, + "993": 110.00000000000021, + "994": 108.15000000000019, + "995": 116.64999999999996, + "996": 113.40000000000012, + "997": 118.90000000000003, + "998": 110.64999999999996, + "999": 98.74999999999996, + "1000": 111.2000000000001 + }, + "5": { + "1": -91.15000000000013, + "2": -51.900000000000084, + "3": -62.6500000000001, + "4": -10.700000000000012, + "5": -33.2, + "6": -36.80000000000003, + "7": -38.95000000000003, + "8": -17.79999999999997, + "9": -68.50000000000006, + "10": -23.29999999999995, + "11": -20.49999999999996, + "12": -17.849999999999973, + "13": -49.700000000000074, + "14": -7.550000000000016, + "15": -21.649999999999956, + "16": -21.04999999999996, + "17": -15.299999999999972, + "18": -47.15000000000004, + "19": -32.25000000000002, + "20": -99.40000000000003, + "21": -36.70000000000004, + "22": -24.350000000000012, + "23": -18.44999999999996, + "24": -19.599999999999984, + "25": -22.199999999999953, + "26": -47.850000000000065, + "27": -11.299999999999995, + "28": -14.199999999999978, + "29": -55.75000000000013, + "30": -61.1000000000001, + "31": -100.15, + "32": -16.399999999999974, + "33": -11.399999999999986, + "34": -32.850000000000016, + "35": -12.199999999999985, + "36": -20.39999999999996, + "37": -30.299999999999976, + "38": -5.399999999999988, + "39": -14.649999999999988, + "40": -61.5000000000001, + "41": -81.95000000000002, + "42": -66.4, + "43": -14.949999999999982, + "44": -79.80000000000007, + "45": -16.349999999999977, + "46": -9.849999999999989, + "47": -18.14999999999997, + "48": -16.099999999999973, + "49": -5.450000000000002, + "50": -11.700000000000003, + "51": -10.099999999999987, + "52": -14.549999999999981, + "53": -21.64999999999995, + "54": -63.35000000000011, + "55": -14.399999999999988, + "56": -12.199999999999996, + "57": -12.499999999999968, + "58": -18.649999999999963, + "59": -15.499999999999977, + "60": -49.250000000000064, + "61": -15.54999999999998, + "62": -17.39999999999997, + "63": -17.94999999999997, + "64": -2.399999999999996, + "65": -29.950000000000006, + "66": -34.349999999999994, + "67": -16.049999999999976, + "68": -6.849999999999996, + "69": -11.449999999999994, + "70": -18.04999999999997, + "71": -67.4, + "72": -46.099999999999994, + "73": -65.1000000000001, + "74": -19.399999999999963, + "75": -34.250000000000014, + "76": -12.199999999999987, + "77": -19.09999999999997, + "78": -17.499999999999975, + "79": -3.149999999999984, + "80": -82.30000000000005, + "81": -18.399999999999967, + "82": -45.00000000000006, + "83": -17.599999999999973, + "84": -6.2999999999999945, + "85": -11.699999999999989, + "86": -72.3, + "87": 29.89999999999986, + "88": -17.39999999999997, + "89": -17.099999999999977, + "90": -15.049999999999974, + "91": -14.399999999999983, + "92": -64.35000000000011, + "93": -8.299999999999992, + "94": -5.400000000000002, + "95": 4.699999999999991, + "96": -39.84999999999997, + "97": -21.999999999999954, + "98": -0.9000000000000068, + "99": 4.200000000000021, + "100": -0.4999999999999927, + "101": -13.249999999999977, + "102": -42.09999999999998, + "103": -11.649999999999972, + "104": -31.550000000000004, + "105": -8.05, + "106": -18.399999999999963, + "107": -41.15000000000005, + "108": -29.550000000000022, + "109": 22.550000000000004, + "110": -62.49999999999995, + "111": -5.900000000000001, + "112": -17.149999999999984, + "113": -16.599999999999973, + "114": -79.15000000000008, + "115": 0.800000000000026, + "116": -87.55000000000001, + "117": -27.199999999999953, + "118": -92.80000000000001, + "119": -15.999999999999982, + "120": -9.999999999999995, + "121": -81.20000000000002, + "122": -20.149999999999967, + "123": -22.84999999999995, + "124": 8.300000000000042, + "125": -3.6499999999999826, + "126": -64.0, + "127": -9.699999999999989, + "128": -1.5499999999999772, + "129": -22.99999999999995, + "130": 2.4500000000000313, + "131": -23.69999999999995, + "132": 1.250000000000063, + "133": -77.99999999999997, + "134": -9.849999999999998, + "135": 6.800000000000063, + "136": -41.50000000000009, + "137": -18.19999999999997, + "138": -42.15000000000015, + "139": -39.04999999999997, + "140": -13.1, + "141": -70.95000000000002, + "142": -17.699999999999967, + "143": 9.450000000000006, + "144": -8.199999999999957, + "145": -6.749999999999992, + "146": -5.199999999999982, + "147": -9.249999999999993, + "148": -1.049999999999974, + "149": 2.4500000000000135, + "150": -8.999999999999996, + "151": -92.0, + "152": -6.999999999999993, + "153": -18.09999999999997, + "154": -15.149999999999979, + "155": -56.999999999999986, + "156": -1.7999999999999725, + "157": -95.25, + "158": 3.000000000000017, + "159": 28.299999999999866, + "160": -76.94999999999996, + "161": -73.75, + "162": -3.799999999999974, + "163": 15.850000000000026, + "164": -9.449999999999989, + "165": -76.4, + "166": -1.249999999999967, + "167": -14.549999999999981, + "168": -96.15, + "169": -24.14999999999995, + "170": -12.149999999999984, + "171": -16.899999999999974, + "172": -13.099999999999978, + "173": -73.5, + "174": -11.549999999999986, + "175": -20.44999999999996, + "176": -64.80000000000003, + "177": -11.85, + "178": -20.999999999999957, + "179": -18.54999999999997, + "180": -13.099999999999985, + "181": -14.499999999999984, + "182": 1.450000000000031, + "183": -15.799999999999972, + "184": 78.49999999999999, + "185": -17.049999999999972, + "186": -14.149999999999984, + "187": -4.45000000000001, + "188": -8.599999999999996, + "189": -4.349999999999988, + "190": -6.249999999999987, + "191": 33.04999999999976, + "192": -22.950000000000053, + "193": -13.099999999999987, + "194": -56.30000000000006, + "195": -8.600000000000003, + "196": -7.299999999999995, + "197": -74.55, + "198": 3.2000000000000277, + "199": -10.349999999999985, + "200": -7.349999999999974, + "201": -1.0999999999999703, + "202": 37.35000000000006, + "203": 35.499999999999794, + "204": -16.099999999999977, + "205": 2.400000000000027, + "206": -10.399999999999988, + "207": -65.20000000000002, + "208": 24.149999999999903, + "209": -83.04999999999994, + "210": 0.5000000000000036, + "211": 25.65000000000005, + "212": -14.449999999999985, + "213": -78.99999999999999, + "214": -73.7, + "215": -46.69999999999998, + "216": -18.69999999999997, + "217": -29.05000000000006, + "218": -24.849999999999987, + "219": 7.049999999999965, + "220": -74.75000000000001, + "221": 41.84999999999983, + "222": 16.04999999999997, + "223": 27.05000000000003, + "224": -68.60000000000002, + "225": -69.19999999999999, + "226": -59.94999999999998, + "227": -84.09999999999994, + "228": 31.349999999999802, + "229": -73.39999999999999, + "230": 18.099999999999962, + "231": -1.4000000000000472, + "232": -84.40000000000002, + "233": -65.29999999999995, + "234": -79.49999999999997, + "235": -56.6, + "236": -16.449999999999978, + "237": -12.39999999999999, + "238": -39.600000000000016, + "239": 0.7500000000000113, + "240": 2.049999999999933, + "241": -11.149999999999956, + "242": -64.05, + "243": 65.99999999999993, + "244": -12.099999999999982, + "245": 4.750000000000041, + "246": 27.600000000000055, + "247": -65.59999999999997, + "248": -25.85000000000006, + "249": -26.449999999999964, + "250": -7.700000000000044, + "251": -6.049999999999983, + "252": 18.450000000000017, + "253": 61.799999999999976, + "254": -54.24999999999999, + "255": -12.699999999999989, + "256": 0.05000000000002558, + "257": 27.049999999999866, + "258": -47.849999999999994, + "259": -1.7999999999999938, + "260": -41.45, + "261": 41.2499999999998, + "262": 17.900000000000063, + "263": 30.999999999999986, + "264": 33.649999999999956, + "265": 8.650000000000029, + "266": -11.800000000000031, + "267": 15.000000000000025, + "268": -3.100000000000004, + "269": -23.19999999999996, + "270": -38.35000000000007, + "271": 79.55000000000008, + "272": 35.34999999999975, + "273": -3.7499999999999645, + "274": -19.79999999999999, + "275": -13.549999999999983, + "276": 29.949999999999786, + "277": 8.60000000000001, + "278": 46.4499999999999, + "279": 79.50000000000001, + "280": 21.899999999999988, + "281": 16.750000000000018, + "282": 18.250000000000053, + "283": 55.69999999999982, + "284": -80.39999999999999, + "285": 36.59999999999987, + "286": 19.649999999999963, + "287": 24.900000000000013, + "288": -36.99999999999999, + "289": -40.349999999999994, + "290": 25.14999999999997, + "291": 19.850000000000023, + "292": 102.10000000000004, + "293": 5.449999999999989, + "294": 7.399999999999948, + "295": 74.25000000000024, + "296": 72.94999999999992, + "297": 46.74999999999995, + "298": 71.84999999999995, + "299": 84.64999999999985, + "300": 11.900000000000054, + "301": -40.25000000000002, + "302": -49.69999999999996, + "303": -8.099999999999989, + "304": -47.54999999999997, + "305": -38.9500000000001, + "306": 90.04999999999991, + "307": 62.799999999999784, + "308": -30.949999999999996, + "309": -33.4, + "310": -18.349999999999998, + "311": 100.50000000000013, + "312": 40.049999999999756, + "313": -10.050000000000004, + "314": 20.400000000000002, + "315": 6.900000000000025, + "316": 43.59999999999993, + "317": 111.95000000000012, + "318": -33.15000000000002, + "319": -89.35000000000011, + "320": 51.94999999999981, + "321": 65.74999999999987, + "322": -7.000000000000031, + "323": 66.5999999999998, + "324": 86.14999999999996, + "325": -23.79999999999999, + "326": 55.599999999999866, + "327": 32.049999999999955, + "328": 80.85000000000001, + "329": 13.750000000000057, + "330": 54.049999999999855, + "331": 111.9500000000003, + "332": -65.95000000000002, + "333": 118.90000000000023, + "334": 32.34999999999996, + "335": -24.05, + "336": 92.4500000000001, + "337": -5.849999999999974, + "338": 48.09999999999994, + "339": 22.3, + "340": 3.0499999999999527, + "341": 82.05000000000017, + "342": 66.09999999999991, + "343": 22.499999999999922, + "344": 76.49999999999994, + "345": 8.550000000000034, + "346": 96.9500000000001, + "347": 101.94999999999999, + "348": 74.69999999999989, + "349": 81.49999999999991, + "350": 37.24999999999996, + "351": 50.39999999999997, + "352": 36.600000000000044, + "353": 83.44999999999997, + "354": 27.549999999999955, + "355": 41.64999999999997, + "356": 51.84999999999993, + "357": -12.500000000000005, + "358": 25.950000000000088, + "359": -54.200000000000074, + "360": 28.59999999999988, + "361": 96.4000000000002, + "362": 56.449999999999996, + "363": 1.149999999999966, + "364": -28.799999999999976, + "365": 5.599999999999952, + "366": 57.849999999999895, + "367": -55.8499999999999, + "368": 58.95000000000009, + "369": 27.55000000000001, + "370": 65.14999999999985, + "371": 97.99999999999997, + "372": -25.74999999999995, + "373": -11.499999999999986, + "374": 39.749999999999964, + "375": 103.19999999999993, + "376": 90.75000000000006, + "377": 59.29999999999992, + "378": 39.19999999999992, + "379": 113.74999999999999, + "380": 73.04999999999995, + "381": 87.70000000000007, + "382": 45.05000000000005, + "383": 58.49999999999996, + "384": -64.64999999999988, + "385": 101.40000000000015, + "386": -73.69999999999985, + "387": 85.49999999999993, + "388": 53.599999999999774, + "389": 111.04999999999994, + "390": 81.44999999999993, + "391": 19.499999999999943, + "392": 106.20000000000017, + "393": 79.75000000000001, + "394": 110.00000000000009, + "395": 75.39999999999998, + "396": -13.350000000000001, + "397": 78.54999999999983, + "398": -1.7000000000000186, + "399": 81.30000000000003, + "400": 118.55000000000011, + "401": 99.05000000000005, + "402": 78.80000000000004, + "403": 46.599999999999824, + "404": -30.59999999999997, + "405": 27.04999999999996, + "406": -11.949999999999967, + "407": 117.15000000000012, + "408": 47.44999999999978, + "409": 88.74999999999989, + "410": 83.00000000000003, + "411": 55.39999999999994, + "412": 11.850000000000012, + "413": 101.45, + "414": 96.10000000000012, + "415": 44.24999999999995, + "416": 112.14999999999995, + "417": 71.5999999999999, + "418": 11.450000000000042, + "419": 29.199999999999996, + "420": 101.04999999999984, + "421": 75.70000000000002, + "422": 95.29999999999991, + "423": 81.19999999999985, + "424": 39.6999999999998, + "425": 35.89999999999996, + "426": 80.65, + "427": 79.35000000000002, + "428": 47.1999999999999, + "429": 80.59999999999977, + "430": 105.19999999999983, + "431": 98.64999999999999, + "432": 79.69999999999999, + "433": 73.25000000000003, + "434": 102.9000000000001, + "435": 103.04999999999994, + "436": 58.699999999999925, + "437": 78.24999999999994, + "438": 53.89999999999997, + "439": 91.95000000000009, + "440": 115.15000000000008, + "441": 102.15000000000013, + "442": 79.99999999999994, + "443": 66.94999999999995, + "444": 102.05000000000017, + "445": 86.6499999999999, + "446": 99.00000000000009, + "447": 47.59999999999991, + "448": 10.200000000000035, + "449": 83.54999999999984, + "450": 100.89999999999999, + "451": 110.15000000000026, + "452": 14.400000000000011, + "453": 79.75, + "454": 65.09999999999991, + "455": 108.25000000000007, + "456": 47.54999999999999, + "457": 117.35000000000007, + "458": 75.20000000000005, + "459": 99.89999999999998, + "460": 63.50000000000004, + "461": 106.09999999999991, + "462": 98.00000000000017, + "463": 111.80000000000018, + "464": 109.65000000000005, + "465": 93.25000000000004, + "466": 99.95000000000014, + "467": 115.15000000000032, + "468": 107.39999999999978, + "469": 55.34999999999979, + "470": 105.74999999999996, + "471": 74.09999999999995, + "472": 101.55, + "473": 111.75000000000007, + "474": 77.95000000000003, + "475": 113.3999999999998, + "476": 101.55000000000014, + "477": 113.25000000000001, + "478": 110.85000000000014, + "479": 113.40000000000012, + "480": 98.20000000000003, + "481": 106.69999999999992, + "482": 83.50000000000003, + "483": 49.49999999999982, + "484": 116.85000000000004, + "485": 72.69999999999982, + "486": 114.65000000000025, + "487": 99.50000000000003, + "488": 109.15000000000018, + "489": 111.3999999999999, + "490": 103.1500000000002, + "491": 112.75000000000013, + "492": 98.29999999999995, + "493": 103.69999999999993, + "494": 48.14999999999975, + "495": 98.05000000000001, + "496": 113.40000000000015, + "497": 94.79999999999991, + "498": 86.40000000000005, + "499": 110.50000000000023, + "500": 108.60000000000002, + "501": 106.65000000000022, + "502": 114.05000000000017, + "503": 92.55000000000004, + "504": 105.40000000000005, + "505": 112.7000000000001, + "506": 92.65000000000029, + "507": 114.00000000000014, + "508": 106.45000000000019, + "509": 112.85000000000015, + "510": 111.10000000000012, + "511": 87.5, + "512": 90.40000000000003, + "513": 75.6, + "514": 110.40000000000025, + "515": 113.85000000000016, + "516": 106.2500000000002, + "517": 108.1500000000002, + "518": 87.80000000000007, + "519": 101.80000000000008, + "520": 107.14999999999974, + "521": 101.74999999999973, + "522": 113.10000000000015, + "523": 111.0500000000001, + "524": 113.75000000000023, + "525": 87.50000000000009, + "526": 112.75000000000023, + "527": 32.65, + "528": 105.85000000000001, + "529": 89.04999999999986, + "530": 113.55000000000011, + "531": 102.15000000000005, + "532": 111.75000000000013, + "533": 101.64999999999992, + "534": 114.50000000000026, + "535": 101.55000000000007, + "536": 101.44999999999992, + "537": 109.45000000000014, + "538": 99.60000000000014, + "539": 107.60000000000021, + "540": 106.5, + "541": 90.50000000000001, + "542": 86.55000000000003, + "543": 105.05000000000005, + "544": 77.39999999999998, + "545": 80.04999999999977, + "546": 104.15000000000015, + "547": 114.95000000000022, + "548": 78.49999999999993, + "549": 109.79999999999987, + "550": 84.55000000000003, + "551": 107.50000000000003, + "552": 113.80000000000011, + "553": 112.09999999999998, + "554": 112.10000000000016, + "555": 107.69999999999996, + "556": 94.10000000000004, + "557": 110.25000000000004, + "558": 111.55000000000021, + "559": 109.70000000000003, + "560": 108.90000000000026, + "561": 83.9000000000001, + "562": 107.89999999999985, + "563": 111.85000000000018, + "564": 109.99999999999987, + "565": 103.85000000000015, + "566": 104.10000000000016, + "567": 112.00000000000026, + "568": 114.0500000000001, + "569": 103.45000000000006, + "570": 113.75000000000021, + "571": 109.35000000000018, + "572": 107.74999999999984, + "573": 115.85000000000002, + "574": 113.25000000000016, + "575": 113.10000000000025, + "576": 112.00000000000026, + "577": 113.05000000000013, + "578": 112.85000000000014, + "579": 74.75000000000001, + "580": 100.2, + "581": 105.40000000000009, + "582": 105.60000000000018, + "583": 111.30000000000022, + "584": 109.15000000000015, + "585": 113.80000000000024, + "586": 110.39999999999996, + "587": 100.85000000000015, + "588": 103.04999999999987, + "589": 109.85000000000021, + "590": 113.00000000000016, + "591": 100.54999999999994, + "592": 111.70000000000012, + "593": 110.10000000000018, + "594": 103.19999999999982, + "595": 109.15000000000016, + "596": 109.50000000000016, + "597": 108.09999999999988, + "598": 101.60000000000011, + "599": 112.25000000000007, + "600": 105.35000000000004, + "601": 103.45000000000009, + "602": 110.74999999999996, + "603": 109.80000000000013, + "604": 95.35000000000011, + "605": 107.75000000000023, + "606": 103.45000000000019, + "607": 7.800000000000029, + "608": 114.05000000000007, + "609": 101.24999999999996, + "610": 112.15000000000005, + "611": 56.99999999999985, + "612": -1.8999999999999908, + "613": 1.9000000000000141, + "614": 60.399999999999835, + "615": 112.75000000000016, + "616": 103.05000000000013, + "617": 60.249999999999844, + "618": 19.349999999999955, + "619": 105.45000000000019, + "620": 24.24999999999988, + "621": -8.549999999999995, + "622": -15.699999999999978, + "623": 1.0000000000000235, + "624": -4.400000000000001, + "625": 4.799999999999985, + "626": -1.7000000000000013, + "627": 13.749999999999996, + "628": -24.650000000000016, + "629": 34.04999999999989, + "630": -5.750000000000003, + "631": 4.800000000000043, + "632": -15.649999999999991, + "633": -3.449999999999973, + "634": 14.79999999999994, + "635": 0.5500000000000266, + "636": -7.099999999999992, + "637": 1.950000000000029, + "638": -9.999999999999986, + "639": -4.799999999999984, + "640": -7.399999999999976, + "641": -2.84999999999998, + "642": -21.54999999999996, + "643": -10.34999999999999, + "644": -8.99999999999999, + "645": -6.199999999999989, + "646": 46.09999999999994, + "647": -3.0499999999999865, + "648": -0.6499999999999873, + "649": -22.14999999999997, + "650": -12.049999999999986, + "651": -11.299999999999986, + "652": -18.800000000000043, + "653": 77.50000000000024, + "654": 40.64999999999985, + "655": 16.199999999999893, + "656": 5.450000000000059, + "657": 38.7, + "658": 10.549999999999946, + "659": -11.200000000000003, + "660": 2.1500000000000252, + "661": 60.09999999999999, + "662": 37.849999999999866, + "663": 40.49999999999983, + "664": 45.04999999999991, + "665": -2.849999999999974, + "666": 21.549999999999937, + "667": 3.20000000000004, + "668": 2.650000000000035, + "669": 12.149999999999991, + "670": 53.549999999999834, + "671": 53.09999999999984, + "672": 48.29999999999994, + "673": -24.400000000000006, + "674": 30.299999999999844, + "675": 99.19999999999999, + "676": 82.05000000000008, + "677": 14.149999999999933, + "678": -52.099999999999994, + "679": 21.400000000000055, + "680": 19.19999999999986, + "681": 1.7500000000000244, + "682": 95.95000000000013, + "683": 71.04999999999991, + "684": 32.899999999999835, + "685": 56.199999999999946, + "686": 9.999999999999956, + "687": 4.200000000000049, + "688": 98.44999999999992, + "689": 51.049999999999855, + "690": 59.24999999999994, + "691": 66.95000000000019, + "692": 76.60000000000004, + "693": 15.04999999999993, + "694": 55.54999999999983, + "695": 109.90000000000019, + "696": 100.95000000000009, + "697": 115.15000000000028, + "698": 44.89999999999987, + "699": 41.49999999999992, + "700": 41.449999999999875, + "701": 33.19999999999982, + "702": 22.49999999999996, + "703": -15.749999999999975, + "704": 87.34999999999997, + "705": 70.75000000000001, + "706": -15.199999999999982, + "707": 24.299999999999876, + "708": 53.09999999999979, + "709": 93.05000000000008, + "710": 10.74999999999999, + "711": 83.40000000000003, + "712": 57.24999999999984, + "713": 2.000000000000024, + "714": 111.70000000000014, + "715": -5.549999999999987, + "716": 88.05000000000008, + "717": 74.9500000000001, + "718": 109.25000000000021, + "719": 105.70000000000023, + "720": 104.75000000000007, + "721": 35.20000000000003, + "722": 104.15000000000019, + "723": 66.35000000000008, + "724": 113.35000000000001, + "725": 102.10000000000002, + "726": 50.94999999999982, + "727": 109.85, + "728": 1.7500000000000377, + "729": 108.45000000000014, + "730": 83.94999999999983, + "731": 108.80000000000015, + "732": 108.70000000000013, + "733": 43.09999999999986, + "734": 92.60000000000014, + "735": 0.7999999999999722, + "736": 81.80000000000001, + "737": 114.20000000000016, + "738": 87.60000000000001, + "739": 106.40000000000008, + "740": 96.40000000000012, + "741": 115.10000000000022, + "742": 98.40000000000008, + "743": 87.25000000000009, + "744": 21.599999999999905, + "745": 62.49999999999997, + "746": 104.70000000000012, + "747": 67.80000000000013, + "748": 97.35000000000022, + "749": 91.25000000000007, + "750": -76.0, + "751": 116.84999999999997, + "752": 89.15000000000026, + "753": 116.30000000000008, + "754": 108.60000000000011, + "755": 116.00000000000006, + "756": 115.25000000000013, + "757": 102.39999999999992, + "758": 115.25000000000026, + "759": 53.04999999999993, + "760": 107.09999999999997, + "761": 104.94999999999997, + "762": 104.45000000000017, + "763": 111.15000000000026, + "764": 100.25000000000003, + "765": 109.75000000000016, + "766": 111.35000000000011, + "767": 103.35000000000002, + "768": 111.69999999999997, + "769": 104.65, + "770": 11.049999999999994, + "771": 104.2, + "772": 111.65000000000002, + "773": 107.05000000000013, + "774": 107.05000000000013, + "775": 103.60000000000001, + "776": 99.4500000000002, + "777": 100.35000000000018, + "778": 101.55000000000007, + "779": 96.0500000000002, + "780": 98.24999999999999, + "781": 109.75000000000017, + "782": 117.10000000000007, + "783": 114.30000000000011, + "784": 105.5500000000001, + "785": 106.8500000000001, + "786": 115.5000000000001, + "787": 117.60000000000014, + "788": 112.9500000000001, + "789": 109.8000000000001, + "790": 111.15, + "791": 108.30000000000018, + "792": 101.55000000000004, + "793": 110.35000000000012, + "794": 81.20000000000003, + "795": 66.09999999999994, + "796": 105.40000000000009, + "797": 107.95000000000007, + "798": 97.75000000000004, + "799": 104.80000000000014, + "800": 110.0000000000001, + "801": 61.44999999999985, + "802": 116.25000000000026, + "803": 100.3, + "804": 104.15000000000005, + "805": 106.30000000000011, + "806": 114.35000000000008, + "807": 2.000000000000009, + "808": 91.30000000000004, + "809": 101.55000000000005, + "810": 106.75000000000017, + "811": 72.79999999999993, + "812": 111.40000000000009, + "813": 107.35000000000018, + "814": 65.05000000000018, + "815": 100.35, + "816": 101.00000000000009, + "817": 102.0500000000001, + "818": 102.50000000000006, + "819": 100.25, + "820": 107.95000000000009, + "821": 111.95000000000017, + "822": 104.2000000000001, + "823": 97.3500000000001, + "824": 103.50000000000003, + "825": 111.35000000000002, + "826": 114.09999999999998, + "827": 103.75000000000006, + "828": 81.94999999999995, + "829": 115.75000000000001, + "830": 60.59999999999999, + "831": 119.0500000000001, + "832": 90.90000000000012, + "833": 97.84999999999998, + "834": 93.9, + "835": 107.59999999999998, + "836": 98.30000000000003, + "837": 108.35000000000011, + "838": 112.85000000000012, + "839": 113.10000000000004, + "840": 109.75000000000009, + "841": 115.35000000000001, + "842": 104.3500000000001, + "843": 82.3, + "844": 110.35000000000005, + "845": 113.50000000000006, + "846": 111.3500000000001, + "847": 115.6500000000001, + "848": 114.40000000000002, + "849": 110.5, + "850": 110.74999999999997, + "851": 63.39999999999985, + "852": 61.750000000000014, + "853": 98.70000000000002, + "854": 104.09999999999992, + "855": 94.55000000000001, + "856": 103.25000000000003, + "857": 109.29999999999994, + "858": 119.35000000000002, + "859": 111.54999999999998, + "860": 101.35000000000014, + "861": 84.05000000000005, + "862": 95.35000000000004, + "863": 52.349999999999916, + "864": 114.14999999999995, + "865": 112.34999999999998, + "866": 115.95000000000016, + "867": 111.60000000000016, + "868": 115.20000000000005, + "869": 115.35, + "870": 110.40000000000012, + "871": 112.1000000000001, + "872": 34.349999999999945, + "873": 94.95000000000022, + "874": 94.04999999999984, + "875": 108.90000000000003, + "876": 101.09999999999992, + "877": 111.45000000000016, + "878": 109.80000000000008, + "879": 118.05000000000005, + "880": 111.2, + "881": 104.45000000000016, + "882": 101.85000000000002, + "883": 101.40000000000006, + "884": 109.6499999999999, + "885": 104.94999999999999, + "886": 116.85000000000005, + "887": 115.60000000000012, + "888": 113.55000000000005, + "889": 88.50000000000003, + "890": 77.25, + "891": 102.75000000000001, + "892": 122.10000000000005, + "893": 104.15000000000015, + "894": 111.80000000000008, + "895": 102.80000000000013, + "896": 103.40000000000002, + "897": 117.95000000000003, + "898": 113.00000000000011, + "899": 109.05000000000003, + "900": 114.30000000000011, + "901": 114.20000000000016, + "902": 109.50000000000003, + "903": 108.20000000000013, + "904": 112.40000000000005, + "905": 112.10000000000011, + "906": 107.75000000000016, + "907": 112.95000000000007, + "908": 120.25000000000009, + "909": 112.05000000000007, + "910": 65.89999999999988, + "911": 109.3500000000002, + "912": 93.05000000000008, + "913": 115.20000000000003, + "914": 110.60000000000007, + "915": 99.85, + "916": 8.90000000000009, + "917": 116.50000000000007, + "918": 103.3500000000001, + "919": 112.50000000000003, + "920": 101.0500000000001, + "921": 113.4, + "922": 107.85000000000007, + "923": 105.44999999999992, + "924": 106.79999999999995, + "925": 82.00000000000009, + "926": 119.50000000000014, + "927": 111.20000000000016, + "928": 115.75, + "929": 107.70000000000002, + "930": 112.80000000000017, + "931": 112.75000000000011, + "932": 110.20000000000019, + "933": 111.05000000000005, + "934": 106.80000000000017, + "935": 102.60000000000012, + "936": 113.90000000000002, + "937": 109.40000000000002, + "938": 104.00000000000018, + "939": 88.60000000000002, + "940": 110.69999999999992, + "941": 116.95000000000013, + "942": 40.09999999999982, + "943": 117.45000000000007, + "944": 116.05000000000011, + "945": 110.75000000000009, + "946": 108.60000000000015, + "947": 107.09999999999997, + "948": 106.40000000000016, + "949": 107.75000000000017, + "950": 116.65, + "951": 91.60000000000008, + "952": 92.65000000000003, + "953": 100.40000000000005, + "954": 112.09999999999994, + "955": 111.55000000000011, + "956": 96.14999999999999, + "957": 106.55000000000004, + "958": 115.50000000000007, + "959": 110.24999999999996, + "960": 112.40000000000008, + "961": 107.90000000000003, + "962": 48.24999999999995, + "963": 112.8500000000001, + "964": 114.70000000000007, + "965": 106.94999999999996, + "966": 118.7000000000001, + "967": 107.44999999999999, + "968": 104.59999999999994, + "969": 107.70000000000019, + "970": 121.3000000000001, + "971": 109.04999999999994, + "972": 115.45000000000012, + "973": 117.50000000000013, + "974": 120.14999999999998, + "975": 101.65000000000013, + "976": 114.35000000000015, + "977": 103.10000000000004, + "978": 109.80000000000008, + "979": 115.5500000000001, + "980": 91.84999999999997, + "981": 114.70000000000022, + "982": 111.20000000000009, + "983": 117.40000000000008, + "984": 110.6, + "985": 109.40000000000013, + "986": 117.45000000000009, + "987": 114.00000000000009, + "988": 105.80000000000011, + "989": 110.80000000000008, + "990": 105.65000000000005, + "991": 108.45000000000014, + "992": 109.35000000000004, + "993": 115.85000000000005, + "994": 114.05000000000014, + "995": 111.05000000000005, + "996": 111.49999999999997, + "997": 99.10000000000015, + "998": 114.95000000000014, + "999": 119.40000000000015, + "1000": 104.3 + } + }, + "config": { + "io_settings": { + "save_agent_actions": true, + "save_step_metadata": false, + "save_pcap_logs": false, + "save_sys_logs": false, + "sys_log_level": "WARNING" + }, + "game": { + "max_episode_length": 128, + "ports": [ + "HTTP", + "POSTGRES_SERVER" + ], + "protocols": [ + "ICMP", + "TCP", + "UDP" + ], + "thresholds": { + "nmne": { + "high": 10, + "medium": 5, + "low": 0 + } + } + }, + "agents": [ + { + "ref": "client_2_green_user", + "team": "GREEN", + "type": "ProbabilisticAgent", + "agent_settings": { + "action_probabilities": { + "0": 0.3, + "1": 0.6, + "2": 0.1 + } + }, + "observation_space": null, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_APPLICATION_EXECUTE" + } + ], + "options": { + "nodes": [ + { + "node_name": "client_2", + "applications": [ + { + "application_name": "WebBrowser" + }, + { + "application_name": "DatabaseClient" + } + ] + } + ], + "max_folders_per_node": 1, + "max_files_per_folder": 1, + "max_services_per_node": 1, + "max_applications_per_node": 2 + }, + "action_map": { + "0": { + "action": "DONOTHING", + "options": {} + }, + "1": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 0 + } + }, + "2": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 1 + } + } + } + }, + "reward_function": { + "reward_components": [ + { + "type": "WEBPAGE_UNAVAILABLE_PENALTY", + "weight": 0.25, + "options": { + "node_hostname": "client_2" + } + }, + { + "type": "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY", + "weight": 0.05, + "options": { + "node_hostname": "client_2" + } + } + ] + } + }, + { + "ref": "client_1_green_user", + "team": "GREEN", + "type": "ProbabilisticAgent", + "agent_settings": { + "action_probabilities": { + "0": 0.3, + "1": 0.6, + "2": 0.1 + } + }, + "observation_space": null, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_APPLICATION_EXECUTE" + } + ], + "options": { + "nodes": [ + { + "node_name": "client_1", + "applications": [ + { + "application_name": "WebBrowser" + }, + { + "application_name": "DatabaseClient" + } + ] + } + ], + "max_folders_per_node": 1, + "max_files_per_folder": 1, + "max_services_per_node": 1, + "max_applications_per_node": 2 + }, + "action_map": { + "0": { + "action": "DONOTHING", + "options": {} + }, + "1": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 0 + } + }, + "2": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 1 + } + } + } + }, + "reward_function": { + "reward_components": [ + { + "type": "WEBPAGE_UNAVAILABLE_PENALTY", + "weight": 0.25, + "options": { + "node_hostname": "client_1" + } + }, + { + "type": "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY", + "weight": 0.05, + "options": { + "node_hostname": "client_1" + } + } + ] + } + }, + { + "ref": "data_manipulation_attacker", + "team": "RED", + "type": "RedDatabaseCorruptingAgent", + "observation_space": null, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_APPLICATION_EXECUTE" + } + ], + "options": { + "nodes": [ + { + "node_name": "client_1", + "applications": [ + { + "application_name": "DataManipulationBot" + } + ] + }, + { + "node_name": "client_2", + "applications": [ + { + "application_name": "DataManipulationBot" + } + ] + } + ], + "max_folders_per_node": 1, + "max_files_per_folder": 1, + "max_services_per_node": 1 + } + }, + "reward_function": { + "reward_components": [ + { + "type": "DUMMY" + } + ] + }, + "agent_settings": { + "start_settings": { + "start_step": 25, + "frequency": 20, + "variance": 5 + } + } + }, + { + "ref": "defender", + "team": "BLUE", + "type": "ProxyAgent", + "observation_space": { + "type": "CUSTOM", + "options": { + "components": [ + { + "type": "NODES", + "label": "NODES", + "options": { + "hosts": [ + { + "hostname": "domain_controller" + }, + { + "hostname": "web_server", + "services": [ + { + "service_name": "WebServer" + } + ] + }, + { + "hostname": "database_server", + "folders": [ + { + "folder_name": "database", + "files": [ + { + "file_name": "database.db" + } + ] + } + ] + }, + { + "hostname": "backup_server" + }, + { + "hostname": "security_suite" + }, + { + "hostname": "client_1" + }, + { + "hostname": "client_2" + } + ], + "num_services": 1, + "num_applications": 0, + "num_folders": 1, + "num_files": 1, + "num_nics": 2, + "include_num_access": false, + "include_nmne": true, + "monitored_traffic": { + "icmp": [ + "NONE" + ], + "tcp": [ + "DNS" + ] + }, + "routers": [ + { + "hostname": "router_1" + } + ], + "num_ports": 0, + "ip_list": [ + "192.168.1.10", + "192.168.1.12", + "192.168.1.14", + "192.168.1.16", + "192.168.1.110", + "192.168.10.21", + "192.168.10.22", + "192.168.10.110" + ], + "wildcard_list": [ + "0.0.0.1" + ], + "port_list": [ + 80, + 5432 + ], + "protocol_list": [ + "ICMP", + "TCP", + "UDP" + ], + "num_rules": 10 + } + }, + { + "type": "LINKS", + "label": "LINKS", + "options": { + "link_references": [ + "router_1:eth-1<->switch_1:eth-8", + "router_1:eth-2<->switch_2:eth-8", + "switch_1:eth-1<->domain_controller:eth-1", + "switch_1:eth-2<->web_server:eth-1", + "switch_1:eth-3<->database_server:eth-1", + "switch_1:eth-4<->backup_server:eth-1", + "switch_1:eth-7<->security_suite:eth-1", + "switch_2:eth-1<->client_1:eth-1", + "switch_2:eth-2<->client_2:eth-1", + "switch_2:eth-7<->security_suite:eth-2" + ] + } + }, + { + "type": "NONE", + "label": "ICS", + "options": {} + } + ] + } + }, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_SERVICE_SCAN" + }, + { + "type": "NODE_SERVICE_STOP" + }, + { + "type": "NODE_SERVICE_START" + }, + { + "type": "NODE_SERVICE_PAUSE" + }, + { + "type": "NODE_SERVICE_RESUME" + }, + { + "type": "NODE_SERVICE_RESTART" + }, + { + "type": "NODE_SERVICE_DISABLE" + }, + { + "type": "NODE_SERVICE_ENABLE" + }, + { + "type": "NODE_SERVICE_FIX" + }, + { + "type": "NODE_FILE_SCAN" + }, + { + "type": "NODE_FILE_CHECKHASH" + }, + { + "type": "NODE_FILE_DELETE" + }, + { + "type": "NODE_FILE_REPAIR" + }, + { + "type": "NODE_FILE_RESTORE" + }, + { + "type": "NODE_FOLDER_SCAN" + }, + { + "type": "NODE_FOLDER_CHECKHASH" + }, + { + "type": "NODE_FOLDER_REPAIR" + }, + { + "type": "NODE_FOLDER_RESTORE" + }, + { + "type": "NODE_OS_SCAN" + }, + { + "type": "NODE_SHUTDOWN" + }, + { + "type": "NODE_STARTUP" + }, + { + "type": "NODE_RESET" + }, + { + "type": "ROUTER_ACL_ADDRULE" + }, + { + "type": "ROUTER_ACL_REMOVERULE" + }, + { + "type": "HOST_NIC_ENABLE" + }, + { + "type": "HOST_NIC_DISABLE" + } + ], + "action_map": { + "0": { + "action": "DONOTHING", + "options": {} + }, + "1": { + "action": "NODE_SERVICE_SCAN", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "2": { + "action": "NODE_SERVICE_STOP", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "3": { + "action": "NODE_SERVICE_START", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "4": { + "action": "NODE_SERVICE_PAUSE", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "5": { + "action": "NODE_SERVICE_RESUME", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "6": { + "action": "NODE_SERVICE_RESTART", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "7": { + "action": "NODE_SERVICE_DISABLE", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "8": { + "action": "NODE_SERVICE_ENABLE", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "9": { + "action": "NODE_FILE_SCAN", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "10": { + "action": "NODE_FILE_CHECKHASH", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "11": { + "action": "NODE_FILE_DELETE", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "12": { + "action": "NODE_FILE_REPAIR", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "13": { + "action": "NODE_SERVICE_FIX", + "options": { + "node_id": 2, + "service_id": 0 + } + }, + "14": { + "action": "NODE_FOLDER_SCAN", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "15": { + "action": "NODE_FOLDER_CHECKHASH", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "16": { + "action": "NODE_FOLDER_REPAIR", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "17": { + "action": "NODE_FOLDER_RESTORE", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "18": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 0 + } + }, + "19": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 0 + } + }, + "20": { + "action": "NODE_STARTUP", + "options": { + "node_id": 0 + } + }, + "21": { + "action": "NODE_RESET", + "options": { + "node_id": 0 + } + }, + "22": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 1 + } + }, + "23": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 1 + } + }, + "24": { + "action": "NODE_STARTUP", + "options": { + "node_id": 1 + } + }, + "25": { + "action": "NODE_RESET", + "options": { + "node_id": 1 + } + }, + "26": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 2 + } + }, + "27": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 2 + } + }, + "28": { + "action": "NODE_STARTUP", + "options": { + "node_id": 2 + } + }, + "29": { + "action": "NODE_RESET", + "options": { + "node_id": 2 + } + }, + "30": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 3 + } + }, + "31": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 3 + } + }, + "32": { + "action": "NODE_STARTUP", + "options": { + "node_id": 3 + } + }, + "33": { + "action": "NODE_RESET", + "options": { + "node_id": 3 + } + }, + "34": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 4 + } + }, + "35": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 4 + } + }, + "36": { + "action": "NODE_STARTUP", + "options": { + "node_id": 4 + } + }, + "37": { + "action": "NODE_RESET", + "options": { + "node_id": 4 + } + }, + "38": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 5 + } + }, + "39": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 5 + } + }, + "40": { + "action": "NODE_STARTUP", + "options": { + "node_id": 5 + } + }, + "41": { + "action": "NODE_RESET", + "options": { + "node_id": 5 + } + }, + "42": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 6 + } + }, + "43": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 6 + } + }, + "44": { + "action": "NODE_STARTUP", + "options": { + "node_id": 6 + } + }, + "45": { + "action": "NODE_RESET", + "options": { + "node_id": 6 + } + }, + "46": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 1, + "permission": 2, + "source_ip_id": 7, + "dest_ip_id": 1, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 1, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "47": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 2, + "permission": 2, + "source_ip_id": 8, + "dest_ip_id": 1, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 1, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "48": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 3, + "permission": 2, + "source_ip_id": 7, + "dest_ip_id": 3, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "49": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 4, + "permission": 2, + "source_ip_id": 8, + "dest_ip_id": 3, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "50": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 5, + "permission": 2, + "source_ip_id": 7, + "dest_ip_id": 4, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "51": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 6, + "permission": 2, + "source_ip_id": 8, + "dest_ip_id": 4, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "52": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 0 + } + }, + "53": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 1 + } + }, + "54": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 2 + } + }, + "55": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 3 + } + }, + "56": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 4 + } + }, + "57": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 5 + } + }, + "58": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 6 + } + }, + "59": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 7 + } + }, + "60": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 8 + } + }, + "61": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 9 + } + }, + "62": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 0, + "nic_id": 0 + } + }, + "63": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 0, + "nic_id": 0 + } + }, + "64": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 1, + "nic_id": 0 + } + }, + "65": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 1, + "nic_id": 0 + } + }, + "66": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 2, + "nic_id": 0 + } + }, + "67": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 2, + "nic_id": 0 + } + }, + "68": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 3, + "nic_id": 0 + } + }, + "69": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 3, + "nic_id": 0 + } + }, + "70": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 4, + "nic_id": 0 + } + }, + "71": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 4, + "nic_id": 0 + } + }, + "72": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 4, + "nic_id": 1 + } + }, + "73": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 4, + "nic_id": 1 + } + }, + "74": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 5, + "nic_id": 0 + } + }, + "75": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 5, + "nic_id": 0 + } + }, + "76": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 6, + "nic_id": 0 + } + }, + "77": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 6, + "nic_id": 0 + } + } + }, + "options": { + "nodes": [ + { + "node_name": "domain_controller" + }, + { + "node_name": "web_server", + "applications": [ + { + "application_name": "DatabaseClient" + } + ], + "services": [ + { + "service_name": "WebServer" + } + ] + }, + { + "node_name": "database_server", + "folders": [ + { + "folder_name": "database", + "files": [ + { + "file_name": "database.db" + } + ] + } + ], + "services": [ + { + "service_name": "DatabaseService" + } + ] + }, + { + "node_name": "backup_server" + }, + { + "node_name": "security_suite" + }, + { + "node_name": "client_1" + }, + { + "node_name": "client_2" + } + ], + "max_folders_per_node": 2, + "max_files_per_folder": 2, + "max_services_per_node": 2, + "max_nics_per_node": 8, + "max_acl_rules": 10, + "ip_list": [ + "192.168.1.10", + "192.168.1.12", + "192.168.1.14", + "192.168.1.16", + "192.168.1.110", + "192.168.10.21", + "192.168.10.22", + "192.168.10.110" + ] + } + }, + "reward_function": { + "reward_components": [ + { + "type": "DATABASE_FILE_INTEGRITY", + "weight": 0.4, + "options": { + "node_hostname": "database_server", + "folder_name": "database", + "file_name": "database.db" + } + }, + { + "type": "SHARED_REWARD", + "weight": 1.0, + "options": { + "agent_name": "client_1_green_user" + } + }, + { + "type": "SHARED_REWARD", + "weight": 1.0, + "options": { + "agent_name": "client_2_green_user" + } + } + ] + }, + "agent_settings": { + "flatten_obs": true + } + } + ], + "simulation": { + "network": { + "nmne_config": { + "capture_nmne": true, + "nmne_capture_keywords": [ + "DELETE" + ] + }, + "nodes": [ + { + "hostname": "router_1", + "type": "router", + "num_ports": 5, + "ports": { + "1": { + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0" + }, + "2": { + "ip_address": "192.168.10.1", + "subnet_mask": "255.255.255.0" + } + }, + "acl": { + "18": { + "action": "PERMIT", + "src_port": "POSTGRES_SERVER", + "dst_port": "POSTGRES_SERVER" + }, + "19": { + "action": "PERMIT", + "src_port": "DNS", + "dst_port": "DNS" + }, + "20": { + "action": "PERMIT", + "src_port": "FTP", + "dst_port": "FTP" + }, + "21": { + "action": "PERMIT", + "src_port": "HTTP", + "dst_port": "HTTP" + }, + "22": { + "action": "PERMIT", + "src_port": "ARP", + "dst_port": "ARP" + }, + "23": { + "action": "PERMIT", + "protocol": "ICMP" + } + } + }, + { + "hostname": "switch_1", + "type": "switch", + "num_ports": 8 + }, + { + "hostname": "switch_2", + "type": "switch", + "num_ports": 8 + }, + { + "hostname": "domain_controller", + "type": "server", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "services": [ + { + "type": "DNSServer", + "options": { + "domain_mapping": { + "arcd.com": "192.168.1.12" + } + } + } + ] + }, + { + "hostname": "web_server", + "type": "server", + "ip_address": "192.168.1.12", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "services": [ + { + "type": "WebServer" + } + ], + "applications": [ + { + "type": "DatabaseClient", + "options": { + "db_server_ip": "192.168.1.14" + } + } + ] + }, + { + "hostname": "database_server", + "type": "server", + "ip_address": "192.168.1.14", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "services": [ + { + "type": "DatabaseService", + "options": { + "backup_server_ip": "192.168.1.16" + } + }, + { + "type": "FTPClient" + } + ] + }, + { + "hostname": "backup_server", + "type": "server", + "ip_address": "192.168.1.16", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "services": [ + { + "type": "FTPServer" + } + ] + }, + { + "hostname": "security_suite", + "type": "server", + "ip_address": "192.168.1.110", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "network_interfaces": { + "2": { + "ip_address": "192.168.10.110", + "subnet_mask": "255.255.255.0" + } + } + }, + { + "hostname": "client_1", + "type": "computer", + "ip_address": "192.168.10.21", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": "192.168.1.10", + "applications": [ + { + "type": "DataManipulationBot", + "options": { + "port_scan_p_of_success": 0.8, + "data_manipulation_p_of_success": 0.8, + "payload": "DELETE", + "server_ip": "192.168.1.14" + } + }, + { + "type": "WebBrowser", + "options": { + "target_url": "http://arcd.com/users/" + } + }, + { + "type": "DatabaseClient", + "options": { + "db_server_ip": "192.168.1.14" + } + } + ], + "services": [ + { + "type": "DNSClient" + } + ] + }, + { + "hostname": "client_2", + "type": "computer", + "ip_address": "192.168.10.22", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": "192.168.1.10", + "applications": [ + { + "type": "WebBrowser", + "options": { + "target_url": "http://arcd.com/users/" + } + }, + { + "type": "DataManipulationBot", + "options": { + "port_scan_p_of_success": 0.8, + "data_manipulation_p_of_success": 0.8, + "payload": "DELETE", + "server_ip": "192.168.1.14" + } + }, + { + "type": "DatabaseClient", + "options": { + "db_server_ip": "192.168.1.14" + } + } + ], + "services": [ + { + "type": "DNSClient" + } + ] + } + ], + "links": [ + { + "endpoint_a_hostname": "router_1", + "endpoint_a_port": 1, + "endpoint_b_hostname": "switch_1", + "endpoint_b_port": 8 + }, + { + "endpoint_a_hostname": "router_1", + "endpoint_a_port": 2, + "endpoint_b_hostname": "switch_2", + "endpoint_b_port": 8 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 1, + "endpoint_b_hostname": "domain_controller", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 2, + "endpoint_b_hostname": "web_server", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 3, + "endpoint_b_hostname": "database_server", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 4, + "endpoint_b_hostname": "backup_server", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 7, + "endpoint_b_hostname": "security_suite", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_2", + "endpoint_a_port": 1, + "endpoint_b_hostname": "client_1", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_2", + "endpoint_a_port": 2, + "endpoint_b_hostname": "client_2", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_2", + "endpoint_a_port": 7, + "endpoint_b_hostname": "security_suite", + "endpoint_b_port": 2 + } + ] + } + } + } +} diff --git a/docs/index.rst b/docs/index.rst index 5749ad56..431dea28 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -123,6 +123,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/environment source/customising_scenarios source/varying_config_files + source/action_masking .. toctree:: :caption: Notebooks: diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst new file mode 100644 index 00000000..3e5b967b --- /dev/null +++ b/docs/source/action_masking.rst @@ -0,0 +1,80 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +Action Masking +************** +The PrimAITE simulation is able to provide action masks in the environment output. These action masks let the agents know +about which actions are invalid based on the current environment state. For instance, it's not possible to install +software on a node that is turned off. Therefore, if an agent has a NODE_SOFTWARE_INSTALL in it's action map for that node, +the action mask will show `0` in the corresponding entry. + +Configuration +============= +Action masking is supported for agents that use the `ProxyAgent` class (the class used for connecting to RL algorithms). +In order to use action masking, set the agent_settings.action_masking parameter to True in the config file. + +Masking Logic +============= +The following logic is applied: + +* **DONOTHING** : Always possible +* **NODE_HOST_SERVICE_SCAN** : Node is on. Service is running. +* **NODE_HOST_SERVICE_STOP** : Node is on. Service is running. +* **NODE_HOST_SERVICE_START** : Node is on. Service is stopped. +* **NODE_HOST_SERVICE_PAUSE** : Node is on. Service is running. +* **NODE_HOST_SERVICE_RESUME** : Node is on. Service is paused. +* **NODE_HOST_SERVICE_RESTART** : Node is on. Service is running. +* **NODE_HOST_SERVICE_DISABLE** : Node is on. +* **NODE_HOST_SERVICE_ENABLE** : Node is on. Service is disabled. +* **NODE_HOST_SERVICE_FIX** : Node is on. Service is running. +* **NODE_HOST_APPLICATION_EXECUTE** : Node is on. +* **NODE_HOST_APPLICATION_SCAN** : Node is on. Application is running. +* **NODE_HOST_APPLICATION_CLOSE** : Node is on. Application is running. +* **NODE_HOST_APPLICATION_FIX** : Node is on. Application is running. +* **NODE_HOST_APPLICATION_INSTALL** : Node is on. +* **NODE_HOST_APPLICATION_REMOVE** : Node is on. +* **NODE_HOST_FILE_SCAN** : Node is on. File exists. File not deleted. +* **NODE_HOST_FILE_CREATE** : Node is on. +* **NODE_HOST_FILE_CHECKHASH** : Node is on. File exists. File not deleted. +* **NODE_HOST_FILE_DELETE** : Node is on. File exists. +* **NODE_HOST_FILE_REPAIR** : Node is on. File exists. File not deleted. +* **NODE_HOST_FILE_RESTORE** : Node is on. File exists. File is deleted. +* **NODE_HOST_FILE_CORRUPT** : Node is on. File exists. File not deleted. +* **NODE_HOST_FILE_ACCESS** : Node is on. File exists. File not deleted. +* **NODE_HOST_FOLDER_CREATE** : Node is on. +* **NODE_HOST_FOLDER_SCAN** : Node is on. Folder exists. Folder not deleted. +* **NODE_HOST_FOLDER_CHECKHASH** : Node is on. Folder exists. Folder not deleted. +* **NODE_HOST_FOLDER_REPAIR** : Node is on. Folder exists. Folder not deleted. +* **NODE_HOST_FOLDER_RESTORE** : Node is on. Folder exists. Folder is deleted. +* **NODE_HOST_OS_SCAN** : Node is on. +* **NODE_HOST_NIC_ENABLE** : NIC is disabled. Node is on. +* **NODE_HOST_NIC_DISABLE** : NIC is enabled. Node is on. +* **NODE_HOST_SHUTDOWN** : Node is on. +* **NODE_HOST_STARTUP** : Node is off. +* **NODE_HOST_RESET** : Node is on. +* **NODE_HOST_NMAP_PING_SCAN** : Node is on. +* **NODE_HOST_NMAP_PORT_SCAN** : Node is on. +* **NODE_HOST_NMAP_NETWORK_SERVICE_RECON** : Node is on. +* **NODE_ROUTER_PORT_ENABLE** : Router is on. +* **NODE_ROUTER_PORT_DISABLE** : Router is on. +* **NODE_ROUTER_ACL_ADDRULE** : Router is on. +* **NODE_ROUTER_ACL_REMOVERULE** : Router is on. +* **NODE_FIREWALL_PORT_ENABLE** : Firewall is on. +* **NODE_FIREWALL_PORT_DISABLE** : Firewall is on. +* **NODE_FIREWALL_ACL_ADDRULE** : Firewall is on. +* **NODE_FIREWALL_ACL_REMOVERULE** : Firewall is on. + + +Mechanism +========= +The environment iterates over the RL agent's ``action_map`` and generates the corresponding simulator request string. +It uses the ``RequestManager.check_valid()`` method to invoke the relevant ``RequestPermissionValidator`` without +actually running the request on the simulation. + +Current Limitations +=================== +Currently, action masking only considers whether the action as a whole is possible, it doesn't verify that the exact +parameter combination passed to the action make sense in the current context. For instance, if ACL rule 3 on router_1 is +already populated, the action for adding another rule at position 3 will be available regardless, as long as that router +is turned on. This will never block valid actions. It will just occasionally allow invalid actions. diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index 82fd7408..1c9585c9 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -18,8 +18,11 @@ This section configures how PrimAITE saves data during simulation and training. save_step_metadata: False save_pcap_logs: False save_sys_logs: False + save_agent_logs: False write_sys_log_to_terminal: False + write_agent_log_to_terminal: False sys_log_level: WARNING + agent_log_level: INFO ``save_logs`` @@ -57,6 +60,12 @@ Optional. Default value is ``False``. If ``True``, then the log files which contain all node actions during the simulation will be saved. +``save_agent_logs`` +----------------- + +Optional. Default value is ``False``. + +If ``True``, then the log files which contain all human readable agent behaviour during the simulation will be saved. ``write_sys_log_to_terminal`` ----------------------------- @@ -65,16 +74,25 @@ Optional. Default value is ``False``. If ``True``, PrimAITE will print sys log to the terminal. +``write_agent_log_to_terminal`` +----------------------------- -``sys_log_level`` -------------- +Optional. Default value is ``False``. + +If ``True``, PrimAITE will print all human readable agent behaviour logs to the terminal. + + +``sys_log_level & agent_log_level`` +--------------------------------- Optional. Default value is ``WARNING``. -The level of logging that should be visible in the sys logs or the logs output to the terminal. +The level of logging that should be visible in the syslog, agent logs or the logs output to the terminal. ``save_sys_logs`` or ``write_sys_log_to_terminal`` has to be set to ``True`` for this setting to be used. +This is also true for agent behaviour logging. + Available options are: - ``DEBUG``: Debug level items and the items below diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index 2bcc8b66..48b857d9 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -7,7 +7,7 @@ ============== In this section the network layout is defined. This part of the config follows a hierarchical structure. Almost every component defines a ``ref`` field which acts as a human-readable unique identifier, used by other parts of the config, such as agents. -At the top level of the network are ``nodes`` and ``links``. +At the top level of the network are ``nodes``, ``links`` and ``airspace``. e.g. @@ -19,6 +19,9 @@ e.g. ... links: ... + airspace: + ... + ``nodes`` --------- @@ -101,3 +104,27 @@ This accepts an integer value e.g. if port 1 is to be connected, the configurati ``bandwidth`` This is an integer value specifying the allowed bandwidth across the connection. Units are in Mbps. + +``airspace`` +------------ + +This section configures settings specific to the wireless network's virtual airspace. + +``frequency_max_capacity_mbps`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This setting allows the user to override the default maximum bandwidth capacity set for each frequency. The key should +be the AirSpaceFrequency name and the value be the desired maximum bandwidth capacity in mbps (megabits per second) for +a single timestep. + +The below example would permit 123.45 megabits to be transmit across the WiFi 2.4 GHz frequency in a single timestep. +Setting a frequencies max capacity to 0.0 blocks that frequency on the airspace. + +.. code-block:: yaml + + simulation: + network: + airspace: + frequency_max_capacity_mbps: + WIFI_2_4: 123.45 + WIFI_5: 0.0 diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index a8870cb4..cc723e40 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -27,6 +27,7 @@ Contents simulation_components/network/nodes/firewall simulation_components/network/switch simulation_components/network/network + simulation_components/network/airspace simulation_components/system/internal_frame_processing simulation_components/system/sys_log simulation_components/system/pcap diff --git a/docs/source/simulation_components/network/airspace.rst b/docs/source/simulation_components/network/airspace.rst new file mode 100644 index 00000000..06a884a7 --- /dev/null +++ b/docs/source/simulation_components/network/airspace.rst @@ -0,0 +1,42 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _airspace: + +AirSpace +======== + + +1. Introduction +--------------- + +The AirSpace class is the central component for wireless networks in PrimAITE and is designed to model and manage the behavior and interactions of wireless network interfaces within a simulated wireless network environment. This documentation provides a detailed overview of the AirSpace class, its components, and how they interact to create a realistic simulation of wireless network dynamics. + +2. Overview of the AirSpace System +---------------------------------- + +The AirSpace is a virtual representation of a physical wireless environment, managing multiple wireless network interfaces that simulate devices connected to the wireless network. These interfaces communicate over radio frequencies, with their interactions influenced by various factors modeled within the AirSpace. + +2.1 Key Components +^^^^^^^^^^^^^^^^^^ + +- **Wireless Network Interfaces**: Representations of network interfaces connected physical devices like routers, computers, or IoT devices that can send and receive data wirelessly. +- **Bandwidth Management**: Tracks data transmission over frequencies to prevent overloading and simulate real-world network congestion. + + +3. Managing Wireless Network Interfaces +--------------------------------------- + +- Interfaces can be dynamically added or removed. +- Configurations can be changed in real-time. +- The AirSpace handles data transmissions, ensuring data sent by an interface is received by all other interfaces on the same frequency. + + +4. AirSpace Inspection +---------------------- + +The AirSpace class provides methods for visualizing network behavior: + +- ``show_wireless_interfaces()``: Displays current state of all interfaces +- ``show_bandwidth_load()``: Shows bandwidth utilisation diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index 29110a52..c78c8419 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -37,7 +37,7 @@ additional steps to configure wireless settings: .. code-block:: python from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter - from primaite.simulator.network.airspace import AirSpaceFrequency + from primaite.simulator.network.airspace import AirSpaceFrequency, ChannelWidth # Instantiate the WirelessRouter wireless_router = WirelessRouter(hostname="MyWirelessRouter") @@ -49,7 +49,7 @@ additional steps to configure wireless settings: wireless_router.configure_wireless_access_point( port=1, ip_address="192.168.2.1", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency.WIFI_2_4 + frequency=AirSpaceFrequency.WIFI_2_4, ) @@ -71,7 +71,7 @@ ICMP traffic, ensuring basic network connectivity and ping functionality. .. code-block:: python - from primaite.simulator.network.airspace import AIR_SPACE, AirSpaceFrequency + from primaite.simulator.network.airspace import AirSpaceFrequency, ChannelWidth from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction @@ -130,13 +130,13 @@ ICMP traffic, ensuring basic network connectivity and ping functionality. port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency.WIFI_2_4 + frequency=AirSpaceFrequency.WIFI_2_4, ) router_2.configure_wireless_access_point( port=1, ip_address="192.168.1.2", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency.WIFI_2_4 + frequency=AirSpaceFrequency.WIFI_2_4, ) # Configure routes for inter-router communication diff --git a/pyproject.toml b/pyproject.toml index a0c2e3eb..9e919604 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ rl = [ "ray[rllib] >= 2.20.0, < 3", "tensorflow==2.12.0", "stable-baselines3[extra]==2.1.0", + "sb3-contrib==2.1.0", ] dev = [ "build==0.10.0", diff --git a/src/primaite/VERSION b/src/primaite/VERSION index fd2a0186..6d0e8e51 100644 --- a/src/primaite/VERSION +++ b/src/primaite/VERSION @@ -1 +1 @@ -3.1.0 +3.3.0-dev0 diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 6d4ec9b4..97442903 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -741,6 +741,7 @@ agents: agent_settings: flatten_obs: true + action_masking: true diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index 2e8221a0..ba666781 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -733,6 +733,7 @@ agents: agent_settings: flatten_obs: true + action_masking: true - ref: defender_2 team: BLUE @@ -1316,6 +1317,7 @@ agents: agent_settings: flatten_obs: true + action_masking: true diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index 3483fc87..144e0733 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -44,3 +44,18 @@ def data_manipulation_config_path() -> Path: _LOGGER.error(msg) raise FileNotFoundError(msg) return path + + +def data_manipulation_marl_config_path() -> Path: + """ + Get the path to the MARL example config. + + :return: Path to yaml config file for the MARL scenario. + :rtype: Path + """ + path = _EXAMPLE_CFG / "data_manipulation_marl.yaml" + if not path.exists(): + msg = f"Example config does not exist: {path}. Have you run `primaite setup`?" + _LOGGER.error(msg) + raise FileNotFoundError(msg) + return path diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index b3b7189c..9a5fedc9 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -49,7 +49,7 @@ class AbstractAction(ABC): objects.""" @abstractmethod - def form_request(self) -> List[str]: + def form_request(self) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [] @@ -67,7 +67,7 @@ class DoNothingAction(AbstractAction): # i.e. a choice between one option. To make enumerating this action easier, we are adding a 'dummy' paramter # with one option. This just aids the Action Manager to enumerate all possibilities. - def form_request(self, **kwargs) -> List[str]: + def form_request(self, **kwargs) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return ["do_nothing"] @@ -86,7 +86,7 @@ class NodeServiceAbstractAction(AbstractAction): self.shape: Dict[str, int] = {"node_id": num_nodes, "service_id": num_services} self.verb: str # define but don't initialise: defends against children classes not defining this - def form_request(self, node_id: int, service_id: int) -> List[str]: + def form_request(self, node_id: int, service_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) service_name = self.manager.get_service_name_by_idx(node_id, service_id) @@ -181,7 +181,7 @@ class NodeApplicationAbstractAction(AbstractAction): self.shape: Dict[str, int] = {"node_id": num_nodes, "application_id": num_applications} self.verb: str # define but don't initialise: defends against children classes not defining this - def form_request(self, node_id: int, application_id: int) -> List[str]: + def form_request(self, node_id: int, application_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) application_name = self.manager.get_application_name_by_idx(node_id, application_id) @@ -229,7 +229,7 @@ class NodeApplicationInstallAction(AbstractAction): super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes} - def form_request(self, node_id: int, application_name: str) -> List[str]: + def form_request(self, node_id: int, application_name: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None: @@ -324,7 +324,7 @@ class NodeApplicationRemoveAction(AbstractAction): super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes} - def form_request(self, node_id: int, application_name: str) -> List[str]: + def form_request(self, node_id: int, application_name: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None: @@ -346,7 +346,7 @@ class NodeFolderAbstractAction(AbstractAction): self.shape: Dict[str, int] = {"node_id": num_nodes, "folder_id": num_folders} self.verb: str # define but don't initialise: defends against children classes not defining this - def form_request(self, node_id: int, folder_id: int) -> List[str]: + def form_request(self, node_id: int, folder_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) folder_name = self.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) @@ -394,7 +394,9 @@ class NodeFileCreateAction(AbstractAction): super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) self.verb: str = "create" - def form_request(self, node_id: int, folder_name: str, file_name: str, force: Optional[bool] = False) -> List[str]: + def form_request( + self, node_id: int, folder_name: str, file_name: str, force: Optional[bool] = False + ) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None or folder_name is None or file_name is None: @@ -409,7 +411,7 @@ class NodeFolderCreateAction(AbstractAction): super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) self.verb: str = "create" - def form_request(self, node_id: int, folder_name: str) -> List[str]: + def form_request(self, node_id: int, folder_name: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None or folder_name is None: @@ -430,7 +432,7 @@ class NodeFileAbstractAction(AbstractAction): self.shape: Dict[str, int] = {"node_id": num_nodes, "folder_id": num_folders, "file_id": num_files} self.verb: str # define but don't initialise: defends against children classes not defining this - def form_request(self, node_id: int, folder_id: int, file_id: int) -> List[str]: + def form_request(self, node_id: int, folder_id: int, file_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) folder_name = self.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) @@ -463,7 +465,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction): super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) self.verb: str = "delete" - def form_request(self, node_id: int, folder_id: int, file_id: int) -> List[str]: + def form_request(self, node_id: int, folder_id: int, file_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) folder_name = self.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) @@ -504,7 +506,7 @@ class NodeFileAccessAction(AbstractAction): super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) self.verb: str = "access" - def form_request(self, node_id: int, folder_name: str, file_name: str) -> List[str]: + def form_request(self, node_id: int, folder_name: str, file_name: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None or folder_name is None or file_name is None: @@ -525,7 +527,7 @@ class NodeAbstractAction(AbstractAction): self.shape: Dict[str, int] = {"node_id": num_nodes} self.verb: str # define but don't initialise: defends against children classes not defining this - def form_request(self, node_id: int) -> List[str]: + def form_request(self, node_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) return ["network", "node", node_name, self.verb] @@ -740,7 +742,7 @@ class RouterACLRemoveRuleAction(AbstractAction): super().__init__(manager=manager) self.shape: Dict[str, int] = {"position": max_acl_rules} - def form_request(self, target_router: str, position: int) -> List[str]: + def form_request(self, target_router: str, position: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return ["network", "node", target_router, "acl", "remove_rule", position] @@ -923,7 +925,7 @@ class HostNICAbstractAction(AbstractAction): self.shape: Dict[str, int] = {"node_id": num_nodes, "nic_id": max_nics_per_node} self.verb: str # define but don't initialise: defends against children classes not defining this - def form_request(self, node_id: int, nic_id: int) -> List[str]: + def form_request(self, node_id: int, nic_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_idx=node_id) nic_num = self.manager.get_nic_num_by_idx(node_idx=node_id, nic_idx=nic_id) @@ -960,7 +962,7 @@ class NetworkPortEnableAction(AbstractAction): super().__init__(manager=manager) self.shape: Dict[str, int] = {"port_id": max_nics_per_node} - def form_request(self, target_nodename: str, port_id: int) -> List[str]: + def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if target_nodename is None or port_id is None: return ["do_nothing"] @@ -979,7 +981,7 @@ class NetworkPortDisableAction(AbstractAction): super().__init__(manager=manager) self.shape: Dict[str, int] = {"port_id": max_nics_per_node} - def form_request(self, target_nodename: str, port_id: int) -> List[str]: + def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if target_nodename is None or port_id is None: return ["do_nothing"] @@ -1315,7 +1317,7 @@ class ActionManager: act_identifier, act_options = self.action_map[action] return act_identifier, act_options - def form_request(self, action_identifier: str, action_options: Dict) -> List[str]: + def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" act_obj = self.actions[action_identifier] return act_obj.form_request(**action_options) diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py new file mode 100644 index 00000000..62ef4884 --- /dev/null +++ b/src/primaite/game/agent/agent_log.py @@ -0,0 +1,188 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import logging +from pathlib import Path + +from prettytable import MARKDOWN, PrettyTable + +from primaite.simulator import LogLevel, SIM_OUTPUT + + +class _NotJSONFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + """ + Determines if a log message does not start and end with '{' and '}' (i.e., it is not a JSON-like message). + + :param record: LogRecord object containing all the information pertinent to the event being logged. + :return: True if log message is not JSON-like, False otherwise. + """ + return not record.getMessage().startswith("{") and not record.getMessage().endswith("}") + + +class AgentLog: + """ + A Agent Log class is a simple logger dedicated to managing and writing logging updates and information for an agent. + + Each log message is written to a file located at: /agent_name/agent_name.log + """ + + def __init__(self, agent_name: str): + """ + Constructs a Agent Log instance for a given hostname. + + :param hostname: The hostname associated with the system logs being recorded. + """ + self.agent_name = agent_name + self.current_episode: int = 1 + self.current_timestep: int = 0 + self.setup_logger() + + @property + def timestep(self) -> int: + """Returns the current timestep. Used for log indexing. + + :return: The current timestep as an Int. + """ + return self.current_timestep + + def update_timestep(self, new_timestep: int): + """ + Updates the self.current_timestep attribute with the given parameter. + + This method is called within .step() to ensure that all instances of Agent Logs + are in sync with one another. + + :param new_timestep: The new timestep. + """ + self.current_timestep = new_timestep + + def setup_logger(self): + """ + Configures the logger for this Agent Log instance. + + The logger is set to the DEBUG level, and is equipped with a handler that writes to a file and filters out + JSON-like messages. + """ + if not SIM_OUTPUT.save_agent_logs: + return + + log_path = self._get_log_path() + file_handler = logging.FileHandler(filename=log_path) + file_handler.setLevel(logging.DEBUG) + + log_format = "%(timestep)s::%(levelname)s::%(message)s" + file_handler.setFormatter(logging.Formatter(log_format)) + + self.logger = logging.getLogger(f"{self.agent_name}_log") + for handler in self.logger.handlers: + self.logger.removeHandler(handler) + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(file_handler) + + def _get_log_path(self) -> Path: + """ + Constructs the path for the log file based on the agent name. + + :return: Path object representing the location of the log file. + """ + root = SIM_OUTPUT.agent_behaviour_path / f"episode_{self.current_episode}" / self.agent_name + root.mkdir(exist_ok=True, parents=True) + return root / f"{self.agent_name}.log" + + def _write_to_terminal(self, msg: str, level: str, to_terminal: bool = False): + if to_terminal or SIM_OUTPUT.write_agent_log_to_terminal: + print(f"{self.agent_name}: ({ self.timestep}) ({level}) {msg}") + + def debug(self, msg: str, to_terminal: bool = False): + """ + Logs a message with the DEBUG level. + + :param msg: The message to be logged. + :param to_terminal: If True, prints to the terminal too. + """ + if SIM_OUTPUT.agent_log_level > LogLevel.DEBUG: + return + + if SIM_OUTPUT.save_agent_logs: + self.logger.debug(msg, extra={"timestep": self.timestep}) + self._write_to_terminal(msg, "DEBUG", to_terminal) + + def info(self, msg: str, to_terminal: bool = False): + """ + Logs a message with the INFO level. + + :param msg: The message to be logged. + :param timestep: The current timestep. + :param to_terminal: If True, prints to the terminal too. + """ + if SIM_OUTPUT.agent_log_level > LogLevel.INFO: + return + + if SIM_OUTPUT.save_agent_logs: + self.logger.info(msg, extra={"timestep": self.timestep}) + self._write_to_terminal(msg, "INFO", to_terminal) + + def warning(self, msg: str, to_terminal: bool = False): + """ + Logs a message with the WARNING level. + + :param msg: The message to be logged. + :param timestep: The current timestep. + :param to_terminal: If True, prints to the terminal too. + """ + if SIM_OUTPUT.agent_log_level > LogLevel.WARNING: + return + + if SIM_OUTPUT.save_agent_logs: + self.logger.warning(msg, extra={"timestep": self.timestep}) + self._write_to_terminal(msg, "WARNING", to_terminal) + + def error(self, msg: str, to_terminal: bool = False): + """ + Logs a message with the ERROR level. + + :param msg: The message to be logged. + :param timestep: The current timestep. + :param to_terminal: If True, prints to the terminal too. + """ + if SIM_OUTPUT.agent_log_level > LogLevel.ERROR: + return + + if SIM_OUTPUT.save_agent_logs: + self.logger.error(msg, extra={"timestep": self.timestep}) + self._write_to_terminal(msg, "ERROR", to_terminal) + + def critical(self, msg: str, to_terminal: bool = False): + """ + Logs a message with the CRITICAL level. + + :param msg: The message to be logged. + :param timestep: The current timestep. + :param to_terminal: If True, prints to the terminal too. + """ + if LogLevel.CRITICAL < SIM_OUTPUT.agent_log_level: + return + + if SIM_OUTPUT.save_agent_logs: + self.logger.critical(msg, extra={"timestep": self.timestep}) + self._write_to_terminal(msg, "CRITICAL", to_terminal) + + def show(self, last_n: int = 10, markdown: bool = False): + """ + Print an Agents Log as a table. + + Generate and print PrettyTable instance that shows the agents behaviour log, with columns Time step, + Level and Message. + + :param markdown: Use Markdown style in table output. Defaults to False. + """ + table = PrettyTable(["Time Step", "Level", "Message"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.agent_name} Behaviour Log" + if self._get_log_path().exists(): + with open(self._get_log_path()) as file: + lines = file.readlines() + for line in lines[-last_n:]: + table.add_row(line.strip().split("::")) + print(table) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 95468331..f57dc191 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -7,6 +7,7 @@ from gymnasium.core import ActType, ObsType from pydantic import BaseModel, model_validator from primaite.game.agent.actions import ActionManager +from primaite.game.agent.agent_log import AgentLog from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.interface.request import RequestFormat, RequestResponse @@ -69,6 +70,8 @@ class AgentSettings(BaseModel): "Configuration for when an agent begins performing it's actions" flatten_obs: bool = True "Whether to flatten the observation space before passing it to the agent. True by default." + action_masking: bool = False + "Whether to return action masks at each step." @classmethod def from_config(cls, config: Optional[Dict]) -> "AgentSettings": @@ -116,6 +119,7 @@ class AbstractAgent(ABC): self.reward_function: Optional[RewardFunction] = reward_function self.agent_settings = agent_settings or AgentSettings() self.history: List[AgentHistoryItem] = [] + self.logger = AgentLog(agent_name) def update_observation(self, state: Dict) -> ObsType: """ @@ -205,6 +209,7 @@ class ProxyAgent(AbstractAgent): ) self.most_recent_action: ActType self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False + self.action_masking: bool = agent_settings.action_masking if agent_settings else False def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 3a91f1fe..129fac1a 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -38,10 +38,11 @@ class DataManipulationAgent(AbstractScriptedAgent): :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: + self.logger.debug(msg="Performing do NOTHING") return "DONOTHING", {} self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) - + self.logger.info(msg="Performing a data manipulation attack!") return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} def setup_agent(self) -> None: @@ -54,3 +55,4 @@ class DataManipulationAgent(AbstractScriptedAgent): # we are assuming that every node in the node manager has a data manipulation application at idx 0 num_nodes = len(self.action_manager.node_names) self.starting_node_idx = random.randint(0, num_nodes - 1) + self.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}") diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index fc168687..f5905ad0 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -85,4 +85,5 @@ class ProbabilisticAgent(AbstractScriptedAgent): :rtype: Tuple[str, Dict] """ choice = self.rng.choice(len(self.action_manager.action_map), p=self.probabilities) + self.logger.info(f"Performing Action: {choice}") return self.action_manager.get_action(choice) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index e7ef4d50..1b1231f6 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from typing import Dict, List, Optional +import numpy as np from pydantic import BaseModel, ConfigDict from primaite import DEFAULT_BANDWIDTH, getLogger @@ -15,6 +16,8 @@ from primaite.game.agent.scripted_agents.probabilistic_agent import Probabilisti from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort +from primaite.simulator import SIM_OUTPUT +from primaite.simulator.network.airspace import AirSpaceFrequency from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC @@ -166,6 +169,8 @@ class PrimaiteGame: for _, agent in self.agents.items(): obs = agent.observation_manager.current_observation action_choice, parameters = agent.get_action(obs, timestep=self.step_counter) + if SIM_OUTPUT.save_agent_logs: + agent.logger.debug(f"Chosen Action: {action_choice}") request = agent.format_request(action_choice, parameters) response = self.simulation.apply_request(request) agent.process_action_response( @@ -184,8 +189,14 @@ class PrimaiteGame: """Advance timestep.""" self.step_counter += 1 _LOGGER.debug(f"Advancing timestep to {self.step_counter} ") + self.update_agent_loggers() self.simulation.apply_timestep(self.step_counter) + def update_agent_loggers(self) -> None: + """Updates Agent Loggers with new timestep.""" + for agent in self.agents.values(): + agent.logger.update_timestep(self.step_counter) + def calculate_truncated(self) -> bool: """Calculate whether the episode is truncated.""" current_step = self.step_counter @@ -194,6 +205,23 @@ class PrimaiteGame: return True return False + def action_mask(self, agent_name: str) -> np.ndarray: + """ + Return the action mask for the agent. + + This is a boolean list corresponding to the agent's action space. A False entry means this action cannot be + performed during this step. + + :return: Action mask + :rtype: List[bool] + """ + agent = self.agents[agent_name] + mask = [True] * len(agent.action_manager.action_map) + for i, action in agent.action_manager.action_map.items(): + request = agent.action_manager.form_request(action_identifier=action[0], action_options=action[1]) + mask[i] = self.simulation._request_manager.check_valid(request, {}) + return np.asarray(mask, dtype=np.int8) + def close(self) -> None: """Close the game, this will close the simulation.""" return NotImplemented @@ -229,6 +257,12 @@ class PrimaiteGame: simulation_config = cfg.get("simulation", {}) network_config = simulation_config.get("network", {}) + airspace_cfg = network_config.get("airspace", {}) + frequency_max_capacity_mbps_cfg = airspace_cfg.get("frequency_max_capacity_mbps", {}) + + frequency_max_capacity_mbps_cfg = {AirSpaceFrequency[k]: v for k, v in frequency_max_capacity_mbps_cfg.items()} + + net.airspace.frequency_max_capacity_mbps_ = frequency_max_capacity_mbps_cfg nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) diff --git a/src/primaite/notebooks/Action-masking.ipynb b/src/primaite/notebooks/Action-masking.ipynb new file mode 100644 index 00000000..0e067b26 --- /dev/null +++ b/src/primaite/notebooks/Action-masking.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Action Masking\n", + "\n", + "PrimAITE environments support action masking. The action mask shows which of the agent's actions are applicable with the current environment state. For example, a node can only be turned on if it is currently turned off." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.config.load import data_manipulation_config_path\n", + "from prettytable import PrettyTable\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env = PrimaiteGymEnv(data_manipulation_config_path())\n", + "env.action_masking = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The action mask is a list of booleans that specifies whether each action in the agent's action map is currently possible. Demonstrated here:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "act_table = PrettyTable((\"number\", \"action\", \"parameters\", \"mask\"))\n", + "mask = env.action_masks()\n", + "actions = env.agent.action_manager.action_map\n", + "max_str_len = 70\n", + "for act,mask in zip(actions.items(), mask):\n", + " act_num, act_data = act\n", + " act_type, act_params = act_data\n", + " act_params = s if len(s:=str(act_params)) np.ndarray: + """ + Return the action mask for the agent. + + This is a boolean list corresponding to the agent's action space. A False entry means this action cannot be + performed during this step. + + :return: Action mask + :rtype: List[bool] + """ + if not self.agent.action_masking: + return np.asarray([True] * len(self.agent.action_manager.action_map)) + else: + return self.game.action_mask(self._agent_name) + @property def agent(self) -> ProxyAgent: """Grab a fresh reference to the agent object because it will be reinstantiated each episode.""" diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 7bfd16f1..78d7cb3c 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -35,10 +35,16 @@ class PrimaiteIO: """Whether to save PCAP logs.""" save_sys_logs: bool = True """Whether to save system logs.""" + save_agent_logs: bool = True + """Whether to save agent logs.""" write_sys_log_to_terminal: bool = False """Whether to write the sys log to the terminal.""" + write_agent_log_to_terminal: bool = False + """Whether to write the agent log to the terminal.""" sys_log_level: LogLevel = LogLevel.INFO - """The level of log that should be included in the logfiles/logged into terminal.""" + """The level of sys logs that should be included in the logfiles/logged into terminal.""" + agent_log_level: LogLevel = LogLevel.INFO + """The level of agent logs that should be included in the logfiles/logged into terminal.""" def __init__(self, settings: Optional[Settings] = None) -> None: """ @@ -51,27 +57,31 @@ class PrimaiteIO: self.session_path: Path = self.generate_session_path() # set global SIM_OUTPUT path SIM_OUTPUT.path = self.session_path / "simulation_output" + SIM_OUTPUT.agent_behaviour_path = self.session_path / "agent_behaviour" SIM_OUTPUT.save_pcap_logs = self.settings.save_pcap_logs SIM_OUTPUT.save_sys_logs = self.settings.save_sys_logs + SIM_OUTPUT.save_agent_logs = self.settings.save_agent_logs + SIM_OUTPUT.write_agent_log_to_terminal = self.settings.write_agent_log_to_terminal SIM_OUTPUT.write_sys_log_to_terminal = self.settings.write_sys_log_to_terminal SIM_OUTPUT.sys_log_level = self.settings.sys_log_level + SIM_OUTPUT.agent_log_level = self.settings.agent_log_level def generate_session_path(self, timestamp: Optional[datetime] = None) -> Path: """Create a folder for the session and return the path to it.""" - if timestamp is None: - timestamp = datetime.now() - date_str = timestamp.strftime("%Y-%m-%d") - time_str = timestamp.strftime("%H-%M-%S") - - session_path = PRIMAITE_PATHS.user_sessions_path / date_str / time_str + session_path = PRIMAITE_PATHS.user_sessions_path / SIM_OUTPUT.date_str / SIM_OUTPUT.time_str # check if running in dev mode if is_dev_mode(): - session_path = _PRIMAITE_ROOT.parent.parent / "sessions" / date_str / time_str + session_path = _PRIMAITE_ROOT.parent.parent / "sessions" / SIM_OUTPUT.date_str / SIM_OUTPUT.time_str # check if there is an output directory set in config if PRIMAITE_CONFIG["developer_mode"]["output_dir"]: - session_path = Path(PRIMAITE_CONFIG["developer_mode"]["output_dir"]) / "sessions" / date_str / time_str + session_path = ( + Path(PRIMAITE_CONFIG["developer_mode"]["output_dir"]) + / "sessions" + / SIM_OUTPUT.date_str + / SIM_OUTPUT.time_str + ) session_path.mkdir(exist_ok=True, parents=True) return session_path @@ -115,6 +125,9 @@ class PrimaiteIO: if config.get("sys_log_level"): config["sys_log_level"] = LogLevel[config["sys_log_level"].upper()] # convert to enum + if config.get("agent_log_level"): + config["agent_log_level"] = LogLevel[config["agent_log_level"].upper()] # convert to enum + new = cls(settings=cls.Settings(**config)) return new diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index fc5d73d8..1adc324c 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -3,6 +3,7 @@ import json from typing import Dict, SupportsFloat, Tuple import gymnasium +from gymnasium import spaces from gymnasium.core import ActType, ObsType from ray.rllib.env.multi_agent_env import MultiAgentEnv @@ -38,15 +39,19 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): self.terminateds = set() self.truncateds = set() - self.observation_space = gymnasium.spaces.Dict( - { - name: gymnasium.spaces.flatten_space(agent.observation_manager.space) - for name, agent in self.agents.items() - } - ) - self.action_space = gymnasium.spaces.Dict( - {name: agent.action_manager.space for name, agent in self.agents.items()} + self.observation_space = spaces.Dict( + {name: spaces.flatten_space(agent.observation_manager.space) for name, agent in self.agents.items()} ) + for agent_name in self._agent_ids: + agent = self.game.rl_agents[agent_name] + if agent.action_masking: + self.observation_space[agent_name] = spaces.Dict( + { + "action_mask": spaces.MultiBinary(agent.action_manager.space.n), + "observations": self.observation_space[agent_name], + } + ) + self.action_space = spaces.Dict({name: agent.action_manager.space for name, agent in self.agents.items()}) self._obs_space_in_preferred_format = True self._action_space_in_preferred_format = True super().__init__() @@ -131,13 +136,17 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def _get_obs(self) -> Dict[str, ObsType]: """Return the current observation.""" - obs = {} + all_obs = {} for agent_name in self._agent_ids: agent = self.game.rl_agents[agent_name] unflat_space = agent.observation_manager.space unflat_obs = agent.observation_manager.current_observation - obs[agent_name] = gymnasium.spaces.flatten(unflat_space, unflat_obs) - return obs + obs = gymnasium.spaces.flatten(unflat_space, unflat_obs) + if agent.action_masking: + all_obs[agent_name] = {"action_mask": self.game.action_mask(agent_name), "observations": obs} + else: + all_obs[agent_name] = obs + return all_obs def close(self): """Close the simulation.""" @@ -158,15 +167,30 @@ class PrimaiteRayEnv(gymnasium.Env): self.env = PrimaiteGymEnv(env_config=env_config) # self.env.episode_counter -= 1 self.action_space = self.env.action_space - self.observation_space = self.env.observation_space + if self.env.agent.action_masking: + self.observation_space = spaces.Dict( + {"action_mask": spaces.MultiBinary(self.env.action_space.n), "observations": self.env.observation_space} + ) + else: + self.observation_space = self.env.observation_space def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" + if self.env.agent.action_masking: + obs, *_ = self.env.reset(seed=seed) + new_obs = {"action_mask": self.env.action_masks(), "observations": obs} + return new_obs, *_ return self.env.reset(seed=seed) def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict]: """Perform a step in the environment.""" - return self.env.step(action) + # if action masking is enabled, intercept the step method and add action mask to observation + if self.env.agent.action_masking: + obs, *_ = self.env.step(action) + new_obs = {"action_mask": self.game.action_mask(self.env._agent_name), "observations": obs} + return new_obs, *_ + else: + return self.env.step(action) def close(self): """Close the simulation.""" diff --git a/src/primaite/setup/_package_data/primaite_config.yaml b/src/primaite/setup/_package_data/primaite_config.yaml index c1caf1f4..e08d951e 100644 --- a/src/primaite/setup/_package_data/primaite_config.yaml +++ b/src/primaite/setup/_package_data/primaite_config.yaml @@ -3,6 +3,8 @@ developer_mode: enabled: False # not enabled by default sys_log_level: DEBUG # level of output for system logs, DEBUG by default + agent_log_level: DEBUG # level of output for agent logs, DEBUG by default + output_agent_logs: False # level of output for system logs, DEBUG by default output_sys_logs: False # system logs not output by default output_pcap_logs: False # pcap logs not output by default output_to_terminal: False # do not output to terminal by default diff --git a/src/primaite/simulator/__init__.py b/src/primaite/simulator/__init__.py index e5fe3cb7..ade1a73b 100644 --- a/src/primaite/simulator/__init__.py +++ b/src/primaite/simulator/__init__.py @@ -34,10 +34,14 @@ class _SimOutput: path = PRIMAITE_PATHS.user_sessions_path / self.date_str / self.time_str self._path = path + self._agent_behaviour_path = path self._save_pcap_logs: bool = False self._save_sys_logs: bool = False + self._save_agent_logs: bool = False self._write_sys_log_to_terminal: bool = False + self._write_agent_log_to_terminal: bool = False self._sys_log_level: LogLevel = LogLevel.WARNING # default log level is at WARNING + self._agent_log_level: LogLevel = LogLevel.WARNING @property def path(self) -> Path: @@ -61,6 +65,28 @@ class _SimOutput: self._path = new_path self._path.mkdir(exist_ok=True, parents=True) + @property + def agent_behaviour_path(self) -> Path: + if is_dev_mode(): + # if dev mode is enabled, if output dir is not set, print to primaite repo root + path: Path = _PRIMAITE_ROOT.parent.parent / "sessions" / self.date_str / self.time_str / "agent_behaviour" + # otherwise print to output dir + if PRIMAITE_CONFIG["developer_mode"]["output_dir"]: + path: Path = ( + Path(PRIMAITE_CONFIG["developer_mode"]["output_dir"]) + / "sessions" + / self.date_str + / self.time_str + / "agent_behaviour" + ) + self._agent_behaviour_path = path + return self._agent_behaviour_path + + @agent_behaviour_path.setter + def agent_behaviour_path(self, new_path: Path) -> None: + self._agent_behaviour_path = new_path + self._agent_behaviour_path.mkdir(exist_ok=True, parents=True) + @property def save_pcap_logs(self) -> bool: if is_dev_mode(): @@ -81,6 +107,16 @@ class _SimOutput: def save_sys_logs(self, save_sys_logs: bool) -> None: self._save_sys_logs = save_sys_logs + @property + def save_agent_logs(self) -> bool: + if is_dev_mode(): + return PRIMAITE_CONFIG.get("developer_mode").get("output_agent_logs") + return self._save_agent_logs + + @save_agent_logs.setter + def save_agent_logs(self, save_agent_logs: bool) -> None: + self._save_agent_logs = save_agent_logs + @property def write_sys_log_to_terminal(self) -> bool: if is_dev_mode(): @@ -91,6 +127,17 @@ class _SimOutput: def write_sys_log_to_terminal(self, write_sys_log_to_terminal: bool) -> None: self._write_sys_log_to_terminal = write_sys_log_to_terminal + # Should this be separate from sys_log? + @property + def write_agent_log_to_terminal(self) -> bool: + if is_dev_mode(): + return PRIMAITE_CONFIG.get("developer_mode").get("output_to_terminal") + return self._write_agent_log_to_terminal + + @write_agent_log_to_terminal.setter + def write_agent_log_to_terminal(self, write_agent_log_to_terminal: bool) -> None: + self._write_agent_log_to_terminal = write_agent_log_to_terminal + @property def sys_log_level(self) -> LogLevel: if is_dev_mode(): @@ -101,5 +148,15 @@ class _SimOutput: def sys_log_level(self, sys_log_level: LogLevel) -> None: self._sys_log_level = sys_log_level + @property + def agent_log_level(self) -> LogLevel: + if is_dev_mode(): + return LogLevel[PRIMAITE_CONFIG.get("developer_mode").get("agent_log_level")] + return self._agent_log_level + + @agent_log_level.setter + def agent_log_level(self, agent_log_level: LogLevel) -> None: + self._agent_log_level = agent_log_level + SIM_OUTPUT = _SimOutput() diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 8d8425ec..848570fe 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -3,9 +3,10 @@ """Core of the PrimAITE Simulator.""" import warnings from abc import abstractmethod -from typing import Callable, Dict, List, Literal, Optional, Union +from typing import Callable, Dict, Iterable, List, Literal, Optional, Tuple, Union from uuid import uuid4 +from prettytable import PrettyTable from pydantic import BaseModel, ConfigDict, Field, validate_call from primaite import getLogger @@ -34,6 +35,20 @@ class RequestPermissionValidator(BaseModel): """Message that is reported when a request is rejected by this validator.""" return "request rejected" + def __add__(self, other: "RequestPermissionValidator") -> "_CombinedValidator": + return _CombinedValidator(validators=[self, other]) + + +class _CombinedValidator(RequestPermissionValidator): + validators: List[RequestPermissionValidator] = [] + + def __call__(self, request, context) -> bool: + return all(x(request, context) for x in self.validators) + + @property + def fail_message(self): + return f"One of the following conditions are not met: {[v.fail_message for v in self.validators]}" + class AllowAllValidator(RequestPermissionValidator): """Always allows the request.""" @@ -150,8 +165,17 @@ class RequestManager(BaseModel): self.request_types.pop(name) - def get_request_types_recursively(self) -> List[List[str]]: - """Recursively generate request tree for this component.""" + def get_request_types_recursively(self) -> List[RequestFormat]: + """ + Recursively generate request tree for this component. + + :param parent_valid: Whether this sub-request's parent request was valid. This value should not be specified by + users, it is used by the recursive call. + :type parent_valid: bool + :returns: A list of tuples where the first tuple element is the request string and the second is whether that + request is currently possible to execute. + :rtype: List[Tuple[RequestFormat, bool]] + """ requests = [] for req_name, req in self.request_types.items(): if isinstance(req.func, RequestManager): @@ -162,6 +186,30 @@ class RequestManager(BaseModel): requests.append([req_name]) return requests + def show(self) -> None: + """Display all currently available requests.""" + table = PrettyTable(["requests"]) + table.align = "l" + table.add_rows([[x] for x in self.get_request_types_recursively()]) + print(table) + + def check_valid(self, request: RequestFormat, context: Dict) -> bool: + """Check if this request would be valid in the current state of the simulation without invoking it.""" + + request_key = request[0] + request_options = request[1:] + + if request_key not in self.request_types: + return False + + request_type = self.request_types[request_key] + + # recurse if we are not at a leaf node + if isinstance(request_type.func, RequestManager): + return request_type.func.check_valid(request_options, context) + + return request_type.validator(request_options, context) + class SimComponent(BaseModel): """Extension of pydantic BaseModel with additional methods that must be defined by all classes in the simulator.""" diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index 37e60aaa..a264ba24 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -52,6 +52,8 @@ class GroupMembershipValidator(RequestPermissionValidator): def __call__(self, request: List[str], context: Dict) -> bool: """Permit the action if the request comes from an account which belongs to the right group.""" # if context request source is part of any groups mentioned in self.allow_groups, return true, otherwise false + if not context: + return False requestor_groups: List[str] = context["request_source"]["groups"] for allowed_group in self.allowed_groups: if allowed_group.name in requestor_groups: diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 456b800c..2162915f 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -6,8 +6,8 @@ from typing import Any, Dict, List, Optional from prettytable import MARKDOWN, PrettyTable -from primaite.interface.request import RequestResponse -from primaite.simulator.core import RequestManager, RequestType, SimComponent +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType, SimComponent from primaite.simulator.file_system.file import File from primaite.simulator.file_system.file_type import FileType from primaite.simulator.file_system.folder import Folder @@ -42,6 +42,10 @@ class FileSystem(SimComponent): More information in user guide and docstring for SimComponent._init_request_manager. """ + self._folder_exists = FileSystem._FolderExistsValidator(file_system=self) + self._folder_not_deleted = FileSystem._FolderNotDeletedValidator(file_system=self) + self._file_exists = FileSystem._FileExistsValidator(file_system=self) + rm = super()._init_request_manager() self._delete_manager = RequestManager() @@ -50,13 +54,15 @@ class FileSystem(SimComponent): request_type=RequestType( func=lambda request, context: RequestResponse.from_bool( self.delete_file(folder_name=request[0], file_name=request[1]) - ) + ), + validator=self._file_exists, ), ) self._delete_manager.add_request( name="folder", request_type=RequestType( - func=lambda request, context: RequestResponse.from_bool(self.delete_folder(folder_name=request[0])) + func=lambda request, context: RequestResponse.from_bool(self.delete_folder(folder_name=request[0])), + validator=self._folder_exists, ), ) rm.add_request( @@ -144,10 +150,13 @@ class FileSystem(SimComponent): ) self._folder_request_manager = RequestManager() - rm.add_request("folder", RequestType(func=self._folder_request_manager)) + rm.add_request( + "folder", + RequestType(func=self._folder_request_manager, validator=self._folder_exists + self._folder_not_deleted), + ) self._file_request_manager = RequestManager() - rm.add_request("file", RequestType(func=self._file_request_manager)) + rm.add_request("file", RequestType(func=self._file_request_manager, validator=self._file_exists)) return rm @@ -626,3 +635,62 @@ class FileSystem(SimComponent): self.sys_log.error(f"Unable to access file that does not exist. (file name: {file_name})") return False + + class _FolderExistsValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the Folder exists. + + Actions cannot be performed on a non-existent folder. + """ + + file_system: FileSystem + """Save a reference to the FileSystem instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Returns True if folder exists.""" + return self.file_system.get_folder(folder_name=request[0]) is not None + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return "Cannot perform request on folder because it does not exist." + + class _FolderNotDeletedValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the Folder has not been deleted. + + Actions cannot be performed on a deleted folder. + """ + + file_system: FileSystem + """Save a reference to the FileSystem instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Returns True if folder exists and is not deleted.""" + # get folder + folder = self.file_system.get_folder(folder_name=request[0], include_deleted=True) + return folder is not None and not folder.deleted + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return "Cannot perform request on folder because it is deleted." + + class _FileExistsValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the File exists. + + Actions cannot be performed on a non-existent file. + """ + + file_system: FileSystem + """Save a reference to the FileSystem instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Returns True if file exists.""" + return self.file_system.get_file(folder_name=request[0], file_name=request[1]) is not None + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return "Cannot perform request on a file that does not exist." diff --git a/src/primaite/simulator/file_system/file_type.py b/src/primaite/simulator/file_system/file_type.py index 8f0cb778..e6e81070 100644 --- a/src/primaite/simulator/file_system/file_type.py +++ b/src/primaite/simulator/file_system/file_type.py @@ -185,5 +185,5 @@ file_type_sizes_bytes = { FileType.ZIP: 1024000, FileType.TAR: 1024000, FileType.GZ: 819200, - FileType.DB: 15360000, + FileType.DB: 5_000_000, } diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index dd2a4c70..c98e4492 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -6,8 +6,8 @@ from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable -from primaite.interface.request import RequestResponse -from primaite.simulator.core import RequestManager, RequestType +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType from primaite.simulator.file_system.file import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemABC, FileSystemItemHealthStatus @@ -55,6 +55,9 @@ class Folder(FileSystemItemABC): More information in user guide and docstring for SimComponent._init_request_manager. """ + self._file_exists = Folder._FileExistsValidator(folder=self) + self._file_not_deleted = Folder._FileNotDeletedValidator(folder=self) + rm = super()._init_request_manager() rm.add_request( name="delete", @@ -65,7 +68,9 @@ class Folder(FileSystemItemABC): self._file_request_manager = RequestManager() rm.add_request( name="file", - request_type=RequestType(func=self._file_request_manager), + request_type=RequestType( + func=self._file_request_manager, validator=self._file_exists + self._file_not_deleted + ), ) return rm @@ -469,3 +474,42 @@ class Folder(FileSystemItemABC): self.deleted = True return True + + class _FileExistsValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the File exists. + + Actions cannot be performed on a non-existent file. + """ + + folder: Folder + """Save a reference to the Folder instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Returns True if file exists.""" + return self.folder.get_file(file_name=request[0]) is not None + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return "Cannot perform request on a file that does not exist." + + class _FileNotDeletedValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the File is not deleted. + + Actions cannot be performed on a deleted file. + """ + + folder: Folder + """Save a reference to the Folder instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Returns True if file exists and is not deleted.""" + file = self.folder.get_file(file_name=request[0]) + return file is not None and not file.deleted + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return "Cannot perform request on a file that is deleted." diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 5fec098b..9c736383 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -3,9 +3,10 @@ from __future__ import annotations from abc import ABC, abstractmethod from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List -from prettytable import PrettyTable +from prettytable import MARKDOWN, PrettyTable +from pydantic import BaseModel, Field from primaite import getLogger from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface @@ -15,90 +16,29 @@ from primaite.simulator.system.core.packet_capture import PacketCapture _LOGGER = getLogger(__name__) -__all__ = ["AirSpaceFrequency", "WirelessNetworkInterface", "IPWirelessNetworkInterface"] +def format_hertz(hertz: float, format_terahertz: bool = False, decimals: int = 3) -> str: + """ + Convert a frequency in Hertz to a formatted string using the most appropriate unit. -class AirSpace: - """Represents a wireless airspace, managing wireless network interfaces and handling wireless transmission.""" + Optionally includes formatting for Terahertz. - def __init__(self): - self._wireless_interfaces: Dict[str, WirelessNetworkInterface] = {} - self._wireless_interfaces_by_frequency: Dict[AirSpaceFrequency, List[WirelessNetworkInterface]] = {} - - def show(self, frequency: Optional[AirSpaceFrequency] = None): - """ - Displays a summary of wireless interfaces in the airspace, optionally filtered by a specific frequency. - - :param frequency: The frequency band to filter devices by. If None, devices for all frequencies are shown. - """ - table = PrettyTable() - table.field_names = ["Connected Node", "MAC Address", "IP Address", "Subnet Mask", "Frequency", "Status"] - - # If a specific frequency is provided, filter by it; otherwise, use all frequencies. - frequencies_to_show = [frequency] if frequency else self._wireless_interfaces_by_frequency.keys() - - for freq in frequencies_to_show: - interfaces = self._wireless_interfaces_by_frequency.get(freq, []) - for interface in interfaces: - status = "Enabled" if interface.enabled else "Disabled" - table.add_row( - [ - interface._connected_node.hostname, # noqa - interface.mac_address, - interface.ip_address if hasattr(interface, "ip_address") else None, - interface.subnet_mask if hasattr(interface, "subnet_mask") else None, - str(freq), - status, - ] - ) - - print(table) - - def add_wireless_interface(self, wireless_interface: WirelessNetworkInterface): - """ - Adds a wireless network interface to the airspace if it's not already present. - - :param wireless_interface: The wireless network interface to be added. - """ - if wireless_interface.mac_address not in self._wireless_interfaces: - self._wireless_interfaces[wireless_interface.mac_address] = wireless_interface - if wireless_interface.frequency not in self._wireless_interfaces_by_frequency: - self._wireless_interfaces_by_frequency[wireless_interface.frequency] = [] - self._wireless_interfaces_by_frequency[wireless_interface.frequency].append(wireless_interface) - - def remove_wireless_interface(self, wireless_interface: WirelessNetworkInterface): - """ - Removes a wireless network interface from the airspace if it's present. - - :param wireless_interface: The wireless network interface to be removed. - """ - if wireless_interface.mac_address in self._wireless_interfaces: - self._wireless_interfaces.pop(wireless_interface.mac_address) - self._wireless_interfaces_by_frequency[wireless_interface.frequency].remove(wireless_interface) - - def clear(self): - """ - Clears all wireless network interfaces and their frequency associations from the airspace. - - After calling this method, the airspace will contain no wireless network interfaces, and transmissions cannot - occur until new interfaces are added again. - """ - self._wireless_interfaces.clear() - self._wireless_interfaces_by_frequency.clear() - - def transmit(self, frame: Frame, sender_network_interface: WirelessNetworkInterface): - """ - Transmits a frame to all enabled wireless network interfaces on a specific frequency within the airspace. - - This ensures that a wireless interface does not receive its own transmission. - - :param frame: The frame to be transmitted. - :param sender_network_interface: The wireless network interface sending the frame. This interface will be - excluded from the list of receivers to prevent it from receiving its own transmission. - """ - for wireless_interface in self._wireless_interfaces_by_frequency.get(sender_network_interface.frequency, []): - if wireless_interface != sender_network_interface and wireless_interface.enabled: - wireless_interface.receive_frame(frame) + :param hertz: Frequency in Hertz. + :param format_terahertz: Whether to format frequency in Terahertz, default is False. + :param decimals: Number of decimal places to round to, default is 3. + :returns: Formatted string with the frequency in the most suitable unit. + """ + format_str = f"{{:.{decimals}f}}" + if format_terahertz and hertz >= 1e12: # Terahertz + return format_str.format(hertz / 1e12) + " THz" + elif hertz >= 1e9: # Gigahertz + return format_str.format(hertz / 1e9) + " GHz" + elif hertz >= 1e6: # Megahertz + return format_str.format(hertz / 1e6) + " MHz" + elif hertz >= 1e3: # Kilohertz + return format_str.format(hertz / 1e3) + " kHz" + else: # Hertz + return format_str.format(hertz) + " Hz" class AirSpaceFrequency(Enum): @@ -110,12 +50,231 @@ class AirSpaceFrequency(Enum): """WiFi 5 GHz. Known for its higher data transmission speeds and reduced interference from other devices.""" def __str__(self) -> str: + hertz_str = format_hertz(hertz=self.value) if self == AirSpaceFrequency.WIFI_2_4: - return "WiFi 2.4 GHz" - elif self == AirSpaceFrequency.WIFI_5: - return "WiFi 5 GHz" - else: - return "Unknown Frequency" + return f"WiFi {hertz_str}" + if self == AirSpaceFrequency.WIFI_5: + return f"WiFi {hertz_str}" + return "Unknown Frequency" + + @property + def maximum_data_rate_bps(self) -> float: + """ + Retrieves the maximum data transmission rate in bits per second (bps) for the frequency. + + The maximum rates are predefined for known frequencies: + - For WIFI_2_4, it returns 100,000,000 bps (100 Mbps). + - For WIFI_5, it returns 500,000,000 bps (500 Mbps). + + :return: The maximum data rate in bits per second. If the frequency is not recognized, returns 0.0. + """ + if self == AirSpaceFrequency.WIFI_2_4: + return 100_000_000.0 # 100 Megabits per second + if self == AirSpaceFrequency.WIFI_5: + return 500_000_000.0 # 500 Megabits per second + return 0.0 + + @property + def maximum_data_rate_mbps(self) -> float: + """ + Retrieves the maximum data transmission rate in megabits per second (Mbps). + + This is derived by converting the maximum data rate from bits per second, as defined + in `maximum_data_rate_bps`, to megabits per second. + + :return: The maximum data rate in megabits per second. + """ + return self.maximum_data_rate_bps / 1_000_000.0 + + +class AirSpace(BaseModel): + """ + Represents a wireless airspace, managing wireless network interfaces and handling wireless transmission. + + This class provides functionalities to manage a collection of wireless network interfaces, each associated with + specific frequencies. It includes methods to add and remove wireless interfaces, and handle data transmission + across these interfaces. + """ + + wireless_interfaces: Dict[str, WirelessNetworkInterface] = Field(default_factory=lambda: {}) + wireless_interfaces_by_frequency: Dict[AirSpaceFrequency, List[WirelessNetworkInterface]] = Field( + default_factory=lambda: {} + ) + bandwidth_load: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) + frequency_max_capacity_mbps_: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) + + def get_frequency_max_capacity_mbps(self, frequency: AirSpaceFrequency) -> float: + """ + Retrieves the maximum data transmission capacity for a specified frequency. + + This method checks a dictionary holding custom maximum capacities. If the frequency is found, it returns the + custom set maximum capacity. If the frequency is not found in the dictionary, it defaults to the standard + maximum data rate associated with that frequency. + + :param frequency: The frequency for which the maximum capacity is queried. + + :return: The maximum capacity in Mbps for the specified frequency. + """ + if frequency in self.frequency_max_capacity_mbps_: + return self.frequency_max_capacity_mbps_[frequency] + return frequency.maximum_data_rate_mbps + + def set_frequency_max_capacity_mbps(self, cfg: Dict[AirSpaceFrequency, float]): + """ + Sets custom maximum data transmission capacities for multiple frequencies. + + :param cfg: A dictionary mapping frequencies to their new maximum capacities in Mbps. + """ + self.frequency_max_capacity_mbps_ = cfg + for freq, mbps in cfg.items(): + print(f"Overriding {freq} max capacity as {mbps:.3f} mbps") + + def show_bandwidth_load(self, markdown: bool = False): + """ + Prints a table of the current bandwidth load for each frequency on the airspace. + + This method prints a tabulated view showing the utilisation of available bandwidth capacities for all + frequencies. The table includes the current capacity usage as a percentage of the maximum capacity, alongside + the absolute maximum capacity values in Mbps. + + :param markdown: Flag indicating if output should be in markdown format. + """ + headers = ["Frequency", "Current Capacity (%)", "Maximum Capacity (Mbit)"] + table = PrettyTable(headers) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = "Airspace Frequency Channel Loads" + for frequency, load in self.bandwidth_load.items(): + maximum_capacity = self.get_frequency_max_capacity_mbps(frequency) + load_percent = load / maximum_capacity if maximum_capacity > 0 else 0.0 + if load_percent > 1.0: + load_percent = 1.0 + table.add_row([format_hertz(frequency.value), f"{load_percent:.0%}", f"{maximum_capacity:.3f}"]) + print(table) + + def show_wireless_interfaces(self, markdown: bool = False): + """ + Prints a table of wireless interfaces in the airspace. + + :param markdown: Flag indicating if output should be in markdown format. + """ + headers = [ + "Connected Node", + "MAC Address", + "IP Address", + "Subnet Mask", + "Frequency", + "Speed (Mbps)", + "Status", + ] + table = PrettyTable(headers) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = "Devices on Air Space" + + for interface in self.wireless_interfaces.values(): + status = "Enabled" if interface.enabled else "Disabled" + table.add_row( + [ + interface._connected_node.hostname, # noqa + interface.mac_address, + interface.ip_address if hasattr(interface, "ip_address") else None, + interface.subnet_mask if hasattr(interface, "subnet_mask") else None, + format_hertz(interface.frequency.value), + f"{interface.speed:.3f}", + status, + ] + ) + print(table.get_string(sortby="Frequency")) + + def show(self, markdown: bool = False): + """ + Prints a summary of the current state of the airspace, including both wireless interfaces and bandwidth loads. + + This method is a convenient wrapper that calls two separate methods to display detailed tables: one for + wireless interfaces and another for bandwidth load across all frequencies managed within the airspace. It + provides a holistic view of the operational status and performance metrics of the airspace. + + :param markdown: Flag indicating if output should be in markdown format. + """ + self.show_wireless_interfaces(markdown) + self.show_bandwidth_load(markdown) + + def add_wireless_interface(self, wireless_interface: WirelessNetworkInterface): + """ + Adds a wireless network interface to the airspace if it's not already present. + + :param wireless_interface: The wireless network interface to be added. + """ + if wireless_interface.mac_address not in self.wireless_interfaces: + self.wireless_interfaces[wireless_interface.mac_address] = wireless_interface + if wireless_interface.frequency not in self.wireless_interfaces_by_frequency: + self.wireless_interfaces_by_frequency[wireless_interface.frequency] = [] + self.wireless_interfaces_by_frequency[wireless_interface.frequency].append(wireless_interface) + + def remove_wireless_interface(self, wireless_interface: WirelessNetworkInterface): + """ + Removes a wireless network interface from the airspace if it's present. + + :param wireless_interface: The wireless network interface to be removed. + """ + if wireless_interface.mac_address in self.wireless_interfaces: + self.wireless_interfaces.pop(wireless_interface.mac_address) + self.wireless_interfaces_by_frequency[wireless_interface.frequency].remove(wireless_interface) + + def clear(self): + """ + Clears all wireless network interfaces and their frequency associations from the airspace. + + After calling this method, the airspace will contain no wireless network interfaces, and transmissions cannot + occur until new interfaces are added again. + """ + self.wireless_interfaces.clear() + self.wireless_interfaces_by_frequency.clear() + + def reset_bandwidth_load(self): + """ + Resets the bandwidth load tracking for all frequencies in the airspace. + + This method clears the current load metrics for all operating frequencies, effectively setting the load to zero. + """ + self.bandwidth_load = {} + + def can_transmit_frame(self, frame: Frame, sender_network_interface: WirelessNetworkInterface) -> bool: + """ + Determines if a frame can be transmitted by the sender network interface based on the current bandwidth load. + + This method checks if adding the size of the frame to the current bandwidth load of the frequency used by the + sender network interface would exceed the maximum allowed bandwidth for that frequency. It returns True if the + frame can be transmitted without exceeding the limit, and False otherwise. + + :param frame: The frame to be transmitted, used to check its size against the frequency's bandwidth limit. + :param sender_network_interface: The network interface attempting to transmit the frame, used to determine the + relevant frequency and its current bandwidth load. + :return: True if the frame can be transmitted within the bandwidth limit, False if it would exceed the limit. + """ + if sender_network_interface.frequency not in self.bandwidth_load: + self.bandwidth_load[sender_network_interface.frequency] = 0.0 + return self.bandwidth_load[ + sender_network_interface.frequency + ] + frame.size_Mbits <= self.get_frequency_max_capacity_mbps(sender_network_interface.frequency) + + def transmit(self, frame: Frame, sender_network_interface: WirelessNetworkInterface): + """ + Transmits a frame to all enabled wireless network interfaces on a specific frequency within the airspace. + + This ensures that a wireless interface does not receive its own transmission. + + :param frame: The frame to be transmitted. + :param sender_network_interface: The wireless network interface sending the frame. This interface will be + excluded from the list of receivers to prevent it from receiving its own transmission. + """ + self.bandwidth_load[sender_network_interface.frequency] += frame.size_Mbits + for wireless_interface in self.wireless_interfaces_by_frequency.get(sender_network_interface.frequency, []): + if wireless_interface != sender_network_interface and wireless_interface.enabled: + wireless_interface.receive_frame(frame) class WirelessNetworkInterface(NetworkInterface, ABC): @@ -185,13 +344,18 @@ class WirelessNetworkInterface(NetworkInterface, ABC): :param frame: The network frame to be sent. :return: True if the frame is sent successfully, False if the network interface is disabled. """ - if self.enabled: - frame.set_sent_timestamp() - self.pcap.capture_outbound(frame) - self.airspace.transmit(frame, self) - return True - # Cannot send Frame as the network interface is not enabled - return False + if not self.enabled: + return False + if not self.airspace.can_transmit_frame(frame, self): + # Drop frame for now. Queuing will happen here (probably) if it's done in the future. + self._connected_node.sys_log.info(f"{self}: Frame dropped as Link is at capacity") + return False + + super().send_frame(frame) + frame.set_sent_timestamp() + self.pcap.capture_outbound(frame) + self.airspace.transmit(frame, self) + return True def receive_frame(self, frame: Frame) -> bool: """ diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 2b9f3e53..0408acde 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -96,6 +96,8 @@ class Network(SimComponent): """Apply pre-timestep logic.""" super().pre_timestep(timestep) + self.airspace.reset_bandwidth_load() + for node in self.nodes.values(): node.pre_timestep(timestep) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 6942d280..15c44821 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -87,7 +87,7 @@ class NetworkInterface(SimComponent, ABC): mac_address: str = Field(default_factory=generate_mac_address) "The MAC address of the interface." - speed: int = 100 + speed: float = 100.0 "The speed of the interface in Mbps. Default is 100 Mbps." mtu: int = 1500 @@ -130,10 +130,25 @@ class NetworkInterface(SimComponent, ABC): More information in user guide and docstring for SimComponent._init_request_manager. """ + _is_network_interface_enabled = NetworkInterface._EnabledValidator(network_interface=self) + _is_network_interface_disabled = NetworkInterface._DisabledValidator(network_interface=self) + rm = super()._init_request_manager() - rm.add_request("enable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.enable()))) - rm.add_request("disable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.disable()))) + rm.add_request( + "enable", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.enable()), + validator=_is_network_interface_disabled, + ), + ) + rm.add_request( + "disable", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.disable()), + validator=_is_network_interface_enabled, + ), + ) return rm @@ -332,6 +347,50 @@ class NetworkInterface(SimComponent, ABC): super().pre_timestep(timestep) self.traffic = {} + class _EnabledValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the NetworkInterface is enabled. + + This is useful because most actions should be being resolved if the NetworkInterface is disabled. + """ + + network_interface: NetworkInterface + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the NetworkInterface is enabled or not.""" + return self.network_interface.enabled + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return ( + f"Cannot perform request on NetworkInterface " + f"'{self.network_interface.mac_address}' because it is not enabled." + ) + + class _DisabledValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the NetworkInterface is disabled. + + This is useful because some actions should be being resolved if the NetworkInterface is disabled. + """ + + network_interface: NetworkInterface + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the NetworkInterface is disabled or not.""" + return not self.network_interface.enabled + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return ( + f"Cannot perform request on NetworkInterface " + f"'{self.network_interface.mac_address}' because it is not disabled." + ) + class WiredNetworkInterface(NetworkInterface, ABC): """ @@ -440,14 +499,17 @@ class WiredNetworkInterface(NetworkInterface, ABC): :param frame: The network frame to be sent. :return: True if the frame is sent, False if the Network Interface is disabled or not connected to a link. """ + if not self.enabled: + return False + if not self._connected_link.can_transmit_frame(frame): + # Drop frame for now. Queuing will happen here (probably) if it's done in the future. + self._connected_node.sys_log.info(f"{self}: Frame dropped as Link is at capacity") + return False super().send_frame(frame) - if self.enabled: - frame.set_sent_timestamp() - self.pcap.capture_outbound(frame) - self._connected_link.transmit_frame(sender_nic=self, frame=frame) - return True - # Cannot send Frame as the NIC is not enabled - return False + frame.set_sent_timestamp() + self.pcap.capture_outbound(frame) + self._connected_link.transmit_frame(sender_nic=self, frame=frame) + return True @abstractmethod def receive_frame(self, frame: Frame) -> bool: @@ -678,12 +740,21 @@ class Link(SimComponent): """ return self.endpoint_a.enabled and self.endpoint_b.enabled - def _can_transmit(self, frame: Frame) -> bool: + def can_transmit_frame(self, frame: Frame) -> bool: + """ + Determines whether a frame can be transmitted considering the current Link load and the Link's bandwidth. + + This method assesses if the transmission of a given frame is possible without exceeding the Link's total + bandwidth capacity. It checks if the current load of the Link plus the size of the frame (expressed in Mbps) + would remain within the defined bandwidth limits. The transmission is only feasible if the Link is active + ('up') and the total load including the new frame does not surpass the bandwidth limit. + + :param frame: The frame intended for transmission, which contains its size in Mbps. + :return: True if the frame can be transmitted without exceeding the bandwidth limit, False otherwise. + """ if self.is_up: frame_size_Mbits = frame.size_Mbits # noqa - Leaving it as Mbits as this is how they're expressed - # return self.current_load + frame_size_Mbits <= self.bandwidth - # TODO: re add this check once packet size limiting and MTU checks are implemented - return True + return self.current_load + frame.size_Mbits <= self.bandwidth return False def transmit_frame(self, sender_nic: WiredNetworkInterface, frame: Frame) -> bool: @@ -694,11 +765,6 @@ class Link(SimComponent): :param frame: The network frame to be sent. :return: True if the Frame can be sent, otherwise False. """ - can_transmit = self._can_transmit(frame) - if not can_transmit: - _LOGGER.debug(f"Cannot transmit frame as {self} is at capacity") - return False - receiver = self.endpoint_a if receiver == sender_nic: receiver = self.endpoint_b @@ -878,6 +944,25 @@ class Node(SimComponent): """Message that is reported when a request is rejected by this validator.""" return f"Cannot perform request on node '{self.node.hostname}' because it is not turned on." + class _NodeIsOffValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the node is off. + + This is useful because some actions require the node to be in an off state. + """ + + node: Node + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the node is on or off.""" + return self.node.operating_state == NodeOperatingState.OFF + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return f"Cannot perform request on node '{self.node.hostname}' because it is not turned off." + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. @@ -940,6 +1025,7 @@ class Node(SimComponent): return RequestResponse.from_bool(False) _node_is_on = Node._NodeIsOnValidator(node=self) + _node_is_off = Node._NodeIsOffValidator(node=self) rm = super()._init_request_manager() # since there are potentially many services, create an request manager that can map service name @@ -969,7 +1055,12 @@ class Node(SimComponent): func=lambda request, context: RequestResponse.from_bool(self.power_off()), validator=_node_is_on ), ) - rm.add_request("startup", RequestType(func=lambda request, context: RequestResponse.from_bool(self.power_on()))) + rm.add_request( + "startup", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.power_on()), validator=_node_is_off + ), + ) rm.add_request( "reset", RequestType(func=lambda request, context: RequestResponse.from_bool(self.reset()), validator=_node_is_on), diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 6eee0d40..1a7da2e7 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -58,12 +58,16 @@ class SwitchPort(WiredNetworkInterface): :param frame: The network frame to be sent. :return: A boolean indicating whether the frame was successfully sent. """ - if self.enabled: - self.pcap.capture_outbound(frame) - self._connected_link.transmit_frame(sender_nic=self, frame=frame) - return True - # Cannot send Frame as the SwitchPort is not enabled - return False + if not self.enabled: + return False + if not self._connected_link.can_transmit_frame(frame): + # Drop frame for now. Queuing will happen here (probably) if it's done in the future. + self._connected_node.sys_log.info(f"{self}: Frame dropped as Link is at capacity") + return False + + self.pcap.capture_outbound(frame) + self._connected_link.transmit_frame(sender_nic=self, frame=frame) + return True def receive_frame(self, frame: Frame) -> bool: """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index e329f7a1..3cb4c515 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Any, Dict, Union +from typing import Any, Dict, Optional, Union from pydantic import validate_call @@ -153,7 +153,7 @@ class WirelessRouter(Router): self, ip_address: IPV4Address, subnet_mask: IPV4Address, - frequency: AirSpaceFrequency = AirSpaceFrequency.WIFI_2_4, + frequency: Optional[AirSpaceFrequency] = AirSpaceFrequency.WIFI_2_4, ): """ Configures a wireless access point (WAP). @@ -170,13 +170,20 @@ class WirelessRouter(Router): enum. This determines the frequency band (e.g., 2.4 GHz or 5 GHz) the access point will use for wireless communication. Default is AirSpaceFrequency.WIFI_2_4. """ + if not frequency: + frequency = AirSpaceFrequency.WIFI_2_4 + self.sys_log.info("Configuring wireless access point") + self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration + network_interface = self.network_interface[1] + network_interface.ip_address = ip_address network_interface.subnet_mask = subnet_mask - self.sys_log.info(f"Configured WAP {network_interface}") + self.wireless_access_point.frequency = frequency # Set operating frequency self.wireless_access_point.enable() # Re-enable the WAP with new settings + self.sys_log.info(f"Configured WAP {network_interface}") @property def router_interface(self) -> RouterInterface: diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index 776a5bfb..159eca7f 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -133,10 +133,11 @@ class Frame(BaseModel): def size(self) -> float: # noqa - Keep it as MBits as this is how they're expressed """The size of the Frame in Bytes.""" # get the payload size if it is a data packet + payload_size = 0.0 if isinstance(self.payload, DataPacket): - return self.payload.get_packet_size() + payload_size = self.payload.get_packet_size() - return float(len(self.model_dump_json().encode("utf-8"))) + return float(len(self.model_dump_json().encode("utf-8"))) + payload_size @property def size_Mbits(self) -> float: # noqa - Keep it as MBits as this is how they're expressed diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 848e1ef0..dc16a725 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -1,10 +1,12 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations + from abc import abstractmethod from enum import Enum from typing import Any, ClassVar, Dict, Optional, Set, Type -from primaite.interface.request import RequestResponse -from primaite.simulator.core import RequestManager, RequestType +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType from primaite.simulator.system.software import IOSoftware, SoftwareHealthState @@ -64,9 +66,27 @@ class Application(IOSoftware): More information in user guide and docstring for SimComponent._init_request_manager. """ - rm = super()._init_request_manager() + _is_application_running = Application._StateValidator(application=self, state=ApplicationOperatingState.RUNNING) - rm.add_request("close", RequestType(func=lambda request, context: RequestResponse.from_bool(self.close()))) + rm = super()._init_request_manager() + rm.add_request( + "scan", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.scan()), validator=_is_application_running + ), + ) + rm.add_request( + "close", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.close()), validator=_is_application_running + ), + ) + rm.add_request( + "fix", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.fix()), validator=_is_application_running + ), + ) return rm @abstractmethod @@ -169,3 +189,28 @@ class Application(IOSoftware): :return: True if successful, False otherwise. """ return super().receive(payload=payload, session_id=session_id, **kwargs) + + class _StateValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the application is in the correct state. + + This is useful because most actions require the application to be in a specific state. + """ + + application: Application + """Save a reference to the application instance.""" + + state: ApplicationOperatingState + """The state of the application to validate.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the application is in the state we are validating for.""" + return self.application.operating_state == self.state + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return ( + f"Cannot perform request on application '{self.application.name}' because it is not in the " + f"{self.state.name} state." + ) diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index e6ce2c87..5adea6e7 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,11 +1,13 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations + from abc import abstractmethod from enum import Enum from typing import Any, Dict, Optional from primaite import getLogger -from primaite.interface.request import RequestResponse -from primaite.simulator.core import RequestManager, RequestType +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType from primaite.simulator.system.software import IOSoftware, SoftwareHealthState _LOGGER = getLogger(__name__) @@ -40,6 +42,7 @@ class Service(IOSoftware): restart_duration: int = 5 "How many timesteps does it take to restart this service." + restart_countdown: Optional[int] = None "If currently restarting, how many timesteps remain until the restart is finished." @@ -86,15 +89,61 @@ class Service(IOSoftware): More information in user guide and docstring for SimComponent._init_request_manager. """ + _is_service_running = Service._StateValidator(service=self, state=ServiceOperatingState.RUNNING) + _is_service_stopped = Service._StateValidator(service=self, state=ServiceOperatingState.STOPPED) + _is_service_paused = Service._StateValidator(service=self, state=ServiceOperatingState.PAUSED) + _is_service_disabled = Service._StateValidator(service=self, state=ServiceOperatingState.DISABLED) + rm = super()._init_request_manager() - rm.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()))) - rm.add_request("stop", RequestType(func=lambda request, context: RequestResponse.from_bool(self.stop()))) - rm.add_request("start", RequestType(func=lambda request, context: RequestResponse.from_bool(self.start()))) - rm.add_request("pause", RequestType(func=lambda request, context: RequestResponse.from_bool(self.pause()))) - rm.add_request("resume", RequestType(func=lambda request, context: RequestResponse.from_bool(self.resume()))) - rm.add_request("restart", RequestType(func=lambda request, context: RequestResponse.from_bool(self.restart()))) + rm.add_request( + "scan", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.scan()), validator=_is_service_running + ), + ) + rm.add_request( + "stop", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.stop()), validator=_is_service_running + ), + ) + rm.add_request( + "start", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.start()), validator=_is_service_stopped + ), + ) + rm.add_request( + "pause", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.pause()), validator=_is_service_running + ), + ) + rm.add_request( + "resume", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.resume()), validator=_is_service_paused + ), + ) + rm.add_request( + "restart", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.restart()), validator=_is_service_running + ), + ) rm.add_request("disable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.disable()))) - rm.add_request("enable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.enable()))) + rm.add_request( + "enable", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.enable()), validator=_is_service_disabled + ), + ) + rm.add_request( + "fix", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.fix()), validator=_is_service_running + ), + ) return rm @abstractmethod @@ -191,3 +240,28 @@ class Service(IOSoftware): self.sys_log.debug(f"Restarting finished for service {self.name}") self.operating_state = ServiceOperatingState.RUNNING self.restart_countdown -= 1 + + class _StateValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the service is in the correct state. + + This is useful because most actions require the service to be in a specific state. + """ + + service: Service + """Save a reference to the service instance.""" + + state: ServiceOperatingState + """The state of the service to validate.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the service is in the state we are validating for.""" + return self.service.operating_state == self.state + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return ( + f"Cannot perform request on service '{self.service.name}' because it is not in the " + f"{self.state.name} state." + ) diff --git a/src/primaite/utils/cli/dev_cli.py b/src/primaite/utils/cli/dev_cli.py index 15adacb3..0dd9f9cc 100644 --- a/src/primaite/utils/cli/dev_cli.py +++ b/src/primaite/utils/cli/dev_cli.py @@ -82,12 +82,31 @@ def config_callback( show_default=False, ), ] = None, + agent_log_level: Annotated[ + LogLevel, + typer.Option( + "--agent-log-level", + "-level", + click_type=click.Choice(LogLevel._member_names_, case_sensitive=False), + help="The level of agent behaviour logs to output.", + show_default=False, + ), + ] = None, output_sys_logs: Annotated[ bool, typer.Option( "--output-sys-logs/--no-sys-logs", "-sys/-nsys", help="Output system logs to file.", show_default=False ), ] = None, + output_agent_logs: Annotated[ + bool, + typer.Option( + "--output-agent-logs/--no-agent-logs", + "-agent/-nagent", + help="Output agent logs to file.", + show_default=False, + ), + ] = None, output_pcap_logs: Annotated[ bool, typer.Option( @@ -109,10 +128,18 @@ def config_callback( PRIMAITE_CONFIG["developer_mode"]["sys_log_level"] = ctx.params.get("sys_log_level") print(f"PrimAITE dev-mode config updated sys_log_level={ctx.params.get('sys_log_level')}") + if ctx.params.get("agent_log_level") is not None: + PRIMAITE_CONFIG["developer_mode"]["agent_log_level"] = ctx.params.get("agent_log_level") + print(f"PrimAITE dev-mode config updated agent_log_level={ctx.params.get('agent_log_level')}") + if output_sys_logs is not None: PRIMAITE_CONFIG["developer_mode"]["output_sys_logs"] = output_sys_logs print(f"PrimAITE dev-mode config updated {output_sys_logs=}") + if output_agent_logs is not None: + PRIMAITE_CONFIG["developer_mode"]["output_agent_logs"] = output_agent_logs + print(f"PrimAITE dev-mode config updated {output_agent_logs=}") + if output_pcap_logs is not None: PRIMAITE_CONFIG["developer_mode"]["output_pcap_logs"] = output_pcap_logs print(f"PrimAITE dev-mode config updated {output_pcap_logs=}") diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 7d40075d..69187fa3 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -9,6 +9,9 @@ io_settings: save_pcap_logs: true save_sys_logs: true sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True game: diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 971f36f8..a2d64605 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1,3 +1,10 @@ +io_settings: + save_agent_actions: false + save_step_metadata: false + save_pcap_logs: false + save_sys_logs: false + + game: max_episode_length: 128 ports: @@ -13,31 +20,105 @@ game: agents: - ref: client_2_green_user team: GREEN - type: PeriodicAgent + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 observation_space: null action_space: action_list: - type: DONOTHING - type: NODE_APPLICATION_EXECUTE - options: nodes: - node_name: client_2 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient max_folders_per_node: 1 max_files_per_folder: 1 max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 reward_function: reward_components: - - type: DUMMY + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 + + - ref: client_1_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: null + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_1 + + + - agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 - ref: data_manipulation_attacker team: RED @@ -57,6 +138,9 @@ agents: - node_name: client_1 applications: - application_name: DataManipulationBot + - node_name: client_2 + applications: + - application_name: DataManipulationBot max_folders_per_node: 1 max_files_per_folder: 1 max_services_per_node: 1 @@ -71,7 +155,7 @@ agents: frequency: 20 variance: 5 - - ref: defender1 + - ref: defender_1 team: BLUE type: ProxyAgent @@ -194,318 +278,425 @@ agents: 3: action: "NODE_SERVICE_START" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 4: action: "NODE_SERVICE_PAUSE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 5: action: "NODE_SERVICE_RESUME" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 6: action: "NODE_SERVICE_RESTART" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 7: action: "NODE_SERVICE_DISABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 8: action: "NODE_SERVICE_ENABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 9: # check database.db file action: "NODE_FILE_SCAN" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 10: - action: "NODE_FILE_CHECKHASH" + action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 11: action: "NODE_FILE_DELETE" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 12: action: "NODE_FILE_REPAIR" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 13: action: "NODE_SERVICE_FIX" options: - node_id: 2 - service_id: 0 + node_id: 2 + service_id: 0 14: action: "NODE_FOLDER_SCAN" options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 15: - action: "NODE_FOLDER_CHECKHASH" + action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 18: action: "NODE_OS_SCAN" options: - node_id: 2 - 19: # shutdown client 1 + node_id: 0 + 19: action: "NODE_SHUTDOWN" options: - node_id: 5 + node_id: 0 20: - action: "NODE_STARTUP" + action: NODE_STARTUP options: - node_id: 5 + node_id: 0 21: - action: "NODE_RESET" + action: NODE_RESET options: - node_id: 5 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + node_id: 0 + 22: + action: "NODE_OS_SCAN" options: - target_router: router_1 - position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + node_id: 1 + 23: + action: "NODE_SHUTDOWN" options: - target_router: router_1 - position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 - 24: # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + node_id: 1 + 24: + action: NODE_STARTUP options: - target_router: router_1 - position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 - 25: # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + node_id: 1 + 25: + action: NODE_RESET options: - target_router: router_1 - position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 - 26: - action: "ROUTER_ACL_ADDRULE" + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" options: - target_router: router_1 - position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + node_id: 2 27: - action: "ROUTER_ACL_ADDRULE" + action: "NODE_SHUTDOWN" options: - target_router: router_1 - position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + node_id: 2 28: - action: "ROUTER_ACL_REMOVERULE" + action: NODE_STARTUP options: - target_router: router_1 - position: 0 + node_id: 2 29: - action: "ROUTER_ACL_REMOVERULE" + action: NODE_RESET options: - target_router: router_1 - position: 1 + node_id: 2 30: - action: "ROUTER_ACL_REMOVERULE" + action: "NODE_OS_SCAN" options: - target_router: router_1 - position: 2 + node_id: 3 31: - action: "ROUTER_ACL_REMOVERULE" + action: "NODE_SHUTDOWN" options: - target_router: router_1 - position: 3 + node_id: 3 32: - action: "ROUTER_ACL_REMOVERULE" + action: NODE_STARTUP options: - target_router: router_1 - position: 4 + node_id: 3 33: - action: "ROUTER_ACL_REMOVERULE" + action: NODE_RESET options: - target_router: router_1 - position: 5 + node_id: 3 34: - action: "ROUTER_ACL_REMOVERULE" + action: "NODE_OS_SCAN" options: - target_router: router_1 - position: 6 + node_id: 4 35: - action: "ROUTER_ACL_REMOVERULE" + action: "NODE_SHUTDOWN" options: - target_router: router_1 - position: 7 + node_id: 4 36: - action: "ROUTER_ACL_REMOVERULE" + action: NODE_STARTUP options: - target_router: router_1 - position: 8 + node_id: 4 37: - action: "ROUTER_ACL_REMOVERULE" + action: NODE_RESET options: - target_router: router_1 - position: 9 + node_id: 4 38: - action: "HOST_NIC_DISABLE" + action: "NODE_OS_SCAN" options: - node_id: 0 - nic_id: 0 - 39: - action: "HOST_NIC_ENABLE" + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" options: - node_id: 0 - nic_id: 0 - 40: - action: "HOST_NIC_DISABLE" + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP options: - node_id: 1 - nic_id: 0 - 41: - action: "HOST_NIC_ENABLE" + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET options: - node_id: 1 - nic_id: 0 + node_id: 5 42: - action: "HOST_NIC_DISABLE" + action: "NODE_OS_SCAN" options: - node_id: 2 - nic_id: 0 + node_id: 6 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 1 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 1 # ALL + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 2 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 1 # ALL + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 48: # old action num: 24 # block tcp traffic from client 1 to web app + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 3 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 3 # web server + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 49: # old action num: 25 # block tcp traffic from client 2 to web app + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 4 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 3 # web server + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 50: # old action num: 26 + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 5 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 4 # database + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 51: # old action num: 27 + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 6 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 4 # database + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 52: # old action num: 28 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 0 + 53: # old action num: 29 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 1 + 54: # old action num: 30 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 2 + 55: # old action num: 31 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 3 + 56: # old action num: 32 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 4 + 57: # old action num: 33 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 5 + 58: # old action num: 34 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 6 + 59: # old action num: 35 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 7 + 60: # old action num: 36 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 8 + 61: # old action num: 37 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 9 + 62: # old action num: 38 + action: "HOST_NIC_DISABLE" + options: + node_id: 0 + nic_id: 0 + 63: # old action num: 39 + action: "HOST_NIC_ENABLE" + options: + node_id: 0 + nic_id: 0 + 64: # old action num: 40 + action: "HOST_NIC_DISABLE" + options: + node_id: 1 + nic_id: 0 + 65: # old action num: 41 + action: "HOST_NIC_ENABLE" + options: + node_id: 1 + nic_id: 0 + 66: # old action num: 42 + action: "HOST_NIC_DISABLE" + options: + node_id: 2 + nic_id: 0 + 67: # old action num: 43 action: "HOST_NIC_ENABLE" options: node_id: 2 nic_id: 0 - 44: + 68: # old action num: 44 action: "HOST_NIC_DISABLE" options: node_id: 3 nic_id: 0 - 45: + 69: # old action num: 45 action: "HOST_NIC_ENABLE" options: node_id: 3 nic_id: 0 - 46: + 70: # old action num: 46 action: "HOST_NIC_DISABLE" options: node_id: 4 nic_id: 0 - 47: + 71: # old action num: 47 action: "HOST_NIC_ENABLE" options: node_id: 4 nic_id: 0 - 48: + 72: # old action num: 48 action: "HOST_NIC_DISABLE" options: node_id: 4 nic_id: 1 - 49: + 73: # old action num: 49 action: "HOST_NIC_ENABLE" options: node_id: 4 nic_id: 1 - 50: + 74: # old action num: 50 action: "HOST_NIC_DISABLE" options: node_id: 5 nic_id: 0 - 51: + 75: # old action num: 51 action: "HOST_NIC_ENABLE" options: node_id: 5 nic_id: 0 - 52: + 76: # old action num: 52 action: "HOST_NIC_DISABLE" options: node_id: 6 nic_id: 0 - 53: + 77: # old action num: 53 action: "HOST_NIC_ENABLE" options: node_id: 6 nic_id: 0 - options: nodes: - node_name: domain_controller - node_name: web_server + applications: + - application_name: DatabaseClient + services: + - service_name: WebServer - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db + services: + - service_name: DatabaseService - node_name: backup_server - node_name: security_suite - node_name: client_1 - node_name: client_2 + max_folders_per_node: 2 max_files_per_folder: 2 max_services_per_node: 2 @@ -521,27 +712,30 @@ agents: - 192.168.10.22 - 192.168.10.110 + reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.5 + weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - - - type: WEB_SERVER_404_PENALTY - weight: 0.5 + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: web_server - service_name: web_server_web_service + agent_name: client_1_green_user + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_2_green_user agent_settings: - # ... + flatten_obs: true + action_masking: true - - ref: defender2 + - ref: defender_2 team: BLUE type: ProxyAgent @@ -640,7 +834,11 @@ agents: - type: NODE_STARTUP - type: NODE_RESET - type: ROUTER_ACL_ADDRULE + options: + target_router: router_1 - type: ROUTER_ACL_REMOVERULE + options: + target_router: router_1 - type: HOST_NIC_ENABLE - type: HOST_NIC_DISABLE @@ -664,99 +862,196 @@ agents: 3: action: "NODE_SERVICE_START" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 4: action: "NODE_SERVICE_PAUSE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 5: action: "NODE_SERVICE_RESUME" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 6: action: "NODE_SERVICE_RESTART" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 7: action: "NODE_SERVICE_DISABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 8: action: "NODE_SERVICE_ENABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 9: # check database.db file action: "NODE_FILE_SCAN" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 10: - action: "NODE_FILE_CHECKHASH" + action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 11: action: "NODE_FILE_DELETE" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 12: action: "NODE_FILE_REPAIR" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 13: action: "NODE_SERVICE_FIX" options: - node_id: 2 - service_id: 0 + node_id: 2 + service_id: 0 14: action: "NODE_FOLDER_SCAN" options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 15: - action: "NODE_FOLDER_CHECKHASH" + action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: - node_id: 2 - folder_id: 1 + node_id: 2 + folder_id: 0 18: action: "NODE_OS_SCAN" options: - node_id: 2 - 19: # shutdown client 1 + node_id: 0 + 19: action: "NODE_SHUTDOWN" options: - node_id: 5 + node_id: 0 20: - action: "NODE_STARTUP" + action: NODE_STARTUP options: - node_id: 5 + node_id: 0 21: - action: "NODE_RESET" + action: NODE_RESET options: - node_id: 5 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" action: "ROUTER_ACL_ADDRULE" options: target_router: router_1 @@ -769,7 +1064,7 @@ agents: protocol_id: 1 source_wildcard_id: 0 dest_wildcard_id: 0 - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" action: "ROUTER_ACL_ADDRULE" options: target_router: router_1 @@ -782,7 +1077,7 @@ agents: protocol_id: 1 source_wildcard_id: 0 dest_wildcard_id: 0 - 24: # block tcp traffic from client 1 to web app + 48: # old action num: 24 # block tcp traffic from client 1 to web app action: "ROUTER_ACL_ADDRULE" options: target_router: router_1 @@ -795,7 +1090,7 @@ agents: protocol_id: 3 source_wildcard_id: 0 dest_wildcard_id: 0 - 25: # block tcp traffic from client 2 to web app + 49: # old action num: 25 # block tcp traffic from client 2 to web app action: "ROUTER_ACL_ADDRULE" options: target_router: router_1 @@ -808,7 +1103,7 @@ agents: protocol_id: 3 source_wildcard_id: 0 dest_wildcard_id: 0 - 26: + 50: # old action num: 26 action: "ROUTER_ACL_ADDRULE" options: target_router: router_1 @@ -821,7 +1116,7 @@ agents: protocol_id: 3 source_wildcard_id: 0 dest_wildcard_id: 0 - 27: + 51: # old action num: 27 action: "ROUTER_ACL_ADDRULE" options: target_router: router_1 @@ -834,67 +1129,159 @@ agents: protocol_id: 3 source_wildcard_id: 0 dest_wildcard_id: 0 - 28: + 52: # old action num: 28 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 0 - 29: + 53: # old action num: 29 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 1 - 30: + 54: # old action num: 30 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 2 - 31: + 55: # old action num: 31 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 3 - 32: + 56: # old action num: 32 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 4 - 33: + 57: # old action num: 33 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 5 - 34: + 58: # old action num: 34 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 6 - 35: + 59: # old action num: 35 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 7 - 36: + 60: # old action num: 36 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 8 - 37: + 61: # old action num: 37 action: "ROUTER_ACL_REMOVERULE" options: target_router: router_1 position: 9 + 62: # old action num: 38 + action: "HOST_NIC_DISABLE" + options: + node_id: 0 + nic_id: 0 + 63: # old action num: 39 + action: "HOST_NIC_ENABLE" + options: + node_id: 0 + nic_id: 0 + 64: # old action num: 40 + action: "HOST_NIC_DISABLE" + options: + node_id: 1 + nic_id: 0 + 65: # old action num: 41 + action: "HOST_NIC_ENABLE" + options: + node_id: 1 + nic_id: 0 + 66: # old action num: 42 + action: "HOST_NIC_DISABLE" + options: + node_id: 2 + nic_id: 0 + 67: # old action num: 43 + action: "HOST_NIC_ENABLE" + options: + node_id: 2 + nic_id: 0 + 68: # old action num: 44 + action: "HOST_NIC_DISABLE" + options: + node_id: 3 + nic_id: 0 + 69: # old action num: 45 + action: "HOST_NIC_ENABLE" + options: + node_id: 3 + nic_id: 0 + 70: # old action num: 46 + action: "HOST_NIC_DISABLE" + options: + node_id: 4 + nic_id: 0 + 71: # old action num: 47 + action: "HOST_NIC_ENABLE" + options: + node_id: 4 + nic_id: 0 + 72: # old action num: 48 + action: "HOST_NIC_DISABLE" + options: + node_id: 4 + nic_id: 1 + 73: # old action num: 49 + action: "HOST_NIC_ENABLE" + options: + node_id: 4 + nic_id: 1 + 74: # old action num: 50 + action: "HOST_NIC_DISABLE" + options: + node_id: 5 + nic_id: 0 + 75: # old action num: 51 + action: "HOST_NIC_ENABLE" + options: + node_id: 5 + nic_id: 0 + 76: # old action num: 52 + action: "HOST_NIC_DISABLE" + options: + node_id: 6 + nic_id: 0 + 77: # old action num: 53 + action: "HOST_NIC_ENABLE" + options: + node_id: 6 + nic_id: 0 + options: nodes: - node_name: domain_controller - node_name: web_server + applications: + - application_name: DatabaseClient + services: + - service_name: WebServer - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db + services: + - service_name: DatabaseService - node_name: backup_server - node_name: security_suite - node_name: client_1 - node_name: client_2 + max_folders_per_node: 2 max_files_per_folder: 2 max_services_per_node: 2 @@ -913,50 +1300,63 @@ agents: reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.5 + weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - - - type: WEB_SERVER_404_PENALTY - weight: 0.5 + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: web_server - service_name: web_server_web_service + agent_name: client_1_green_user + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_2_green_user agent_settings: - # ... - + flatten_obs: true + action_masking: true simulation: network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE nodes: - - type: router - hostname: router_1 + - hostname: router_1 + type: router num_ports: 5 ports: 1: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 2: - ip_address: 192.168.1.1 + ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: - 0: + 18: action: PERMIT src_port: POSTGRES_SERVER dst_port: POSTGRES_SERVER - 1: + 19: action: PERMIT src_port: DNS dst_port: DNS + 20: + action: PERMIT + src_port: FTP + dst_port: FTP + 21: + action: PERMIT + src_port: HTTP + dst_port: HTTP 22: action: PERMIT src_port: ARP @@ -965,16 +1365,16 @@ simulation: action: PERMIT protocol: ICMP - - type: switch - hostname: switch_1 + - hostname: switch_1 + type: switch num_ports: 8 - - type: switch - hostname: switch_2 + - hostname: switch_2 + type: switch num_ports: 8 - - type: server - hostname: domain_controller + - hostname: domain_controller + type: server ip_address: 192.168.1.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -984,8 +1384,8 @@ simulation: domain_mapping: arcd.com: 192.168.1.12 # web server - - type: server - hostname: web_server + - hostname: web_server + type: server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -997,17 +1397,21 @@ simulation: options: db_server_ip: 192.168.1.14 - - type: server - hostname: database_server + + - hostname: database_server + type: server ip_address: 192.168.1.14 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - type: FTPClient - - type: server - hostname: backup_server + - hostname: backup_server + type: server ip_address: 192.168.1.16 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1015,8 +1419,8 @@ simulation: services: - type: FTPServer - - type: server - hostname: security_suite + - hostname: security_suite + type: server ip_address: 192.168.1.110 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1026,8 +1430,8 @@ simulation: ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 - - type: computer - hostname: client_1 + - hostname: client_1 + type: computer ip_address: 192.168.10.21 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 @@ -1035,24 +1439,43 @@ simulation: applications: - type: DataManipulationBot options: - port_scan_p_of_success: 0.1 - data_manipulation_p_of_success: 0.1 + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 + - type: WebBrowser + options: + target_url: http://arcd.com/users/ + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - type: DNSClient - - type: computer - hostname: client_2 + - hostname: client_2 + type: computer ip_address: 192.168.10.22 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - type: WebBrowser + options: + target_url: http://arcd.com/users/ + - type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - type: DNSClient + + links: - endpoint_a_hostname: router_1 endpoint_a_port: 1 diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 08944ee5..8ed715c1 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -41,6 +41,12 @@ agents: options: source_node: client_1 target_ip_address: 192.168.10.0/24 + target_port: + - 21 + - 53 + - 80 + - 123 + - 219 reward_function: reward_components: diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index beb176d1..1acb05a9 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -177,6 +177,9 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: + - type: NMAP + options: + fix_duration: 1 - type: RansomwareScript options: fix_duration: 1 diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 54143af0..eb8103e8 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -243,25 +243,25 @@ agents: action: "NODE_FILE_SCAN" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 10: action: "NODE_FILE_CHECKHASH" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 11: action: "NODE_FILE_DELETE" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 12: action: "NODE_FILE_REPAIR" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 13: action: "NODE_SERVICE_FIX" @@ -272,22 +272,22 @@ agents: action: "NODE_FOLDER_SCAN" options: node_id: 2 - folder_id: 1 + folder_id: 0 15: action: "NODE_FOLDER_CHECKHASH" options: node_id: 2 - folder_id: 1 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: node_id: 2 - folder_id: 1 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: node_id: 2 - folder_id: 1 + folder_id: 0 18: action: "NODE_OS_SCAN" options: @@ -518,11 +518,22 @@ agents: nodes: - node_name: domain_controller - node_name: web_server + applications: + - application_name: DatabaseClient + services: + - service_name: WebServer - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db + services: + - service_name: DatabaseService - node_name: backup_server - node_name: security_suite - node_name: client_1 - node_name: client_2 + max_folders_per_node: 2 max_files_per_folder: 2 max_services_per_node: 2 @@ -557,6 +568,7 @@ agents: agent_settings: flatten_obs: true + action_masking: true @@ -634,6 +646,8 @@ simulation: dns_server: 192.168.1.10 services: - type: DatabaseService + options: + backup_server_ip: 192.168.1.16 - type: server hostname: backup_server diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml new file mode 100644 index 00000000..a327b0f5 --- /dev/null +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml @@ -0,0 +1,81 @@ +game: + max_episode_length: 256 + ports: + - ARP + protocols: + - ICMP + - TCP + - UDP + +simulation: + network: + airspace: + frequency_max_capacity_mbps: + WIFI_2_4: 123.45 + WIFI_5: 0.0 + nodes: + - type: computer + hostname: pc_a + ip_address: 192.168.0.2 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.0.1 + start_up_duration: 0 + + - type: computer + hostname: pc_b + ip_address: 192.168.2.2 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.2.1 + start_up_duration: 0 + + - type: wireless_router + hostname: router_1 + start_up_duration: 0 + + router_interface: + ip_address: 192.168.0.1 + subnet_mask: 255.255.255.0 + + wireless_access_point: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + frequency: WIFI_2_4 + acl: + 1: + action: PERMIT + routes: + - address: 192.168.2.0 # PC B subnet + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.1.2 + metric: 0 + + - type: wireless_router + hostname: router_2 + start_up_duration: 0 + + router_interface: + ip_address: 192.168.2.1 + subnet_mask: 255.255.255.0 + + wireless_access_point: + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + frequency: WIFI_2_4 + acl: + 1: + action: PERMIT + routes: + - address: 192.168.0.0 # PC A subnet + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.1.1 + metric: 0 + links: + - endpoint_a_hostname: pc_a + endpoint_a_port: 1 + endpoint_b_hostname: router_1 + endpoint_b_port: 2 + + - endpoint_a_hostname: pc_b + endpoint_a_port: 1 + endpoint_b_hostname: router_2 + endpoint_b_port: 2 diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml new file mode 100644 index 00000000..ff048c92 --- /dev/null +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml @@ -0,0 +1,81 @@ +game: + max_episode_length: 256 + ports: + - ARP + protocols: + - ICMP + - TCP + - UDP + +simulation: + network: + airspace: + frequency_max_capacity_mbps: + WIFI_2_4: 0.0 + WIFI_5: 0.0 + nodes: + - type: computer + hostname: pc_a + ip_address: 192.168.0.2 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.0.1 + start_up_duration: 0 + + - type: computer + hostname: pc_b + ip_address: 192.168.2.2 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.2.1 + start_up_duration: 0 + + - type: wireless_router + hostname: router_1 + start_up_duration: 0 + + router_interface: + ip_address: 192.168.0.1 + subnet_mask: 255.255.255.0 + + wireless_access_point: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + frequency: WIFI_2_4 + acl: + 1: + action: PERMIT + routes: + - address: 192.168.2.0 # PC B subnet + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.1.2 + metric: 0 + + - type: wireless_router + hostname: router_2 + start_up_duration: 0 + + router_interface: + ip_address: 192.168.2.1 + subnet_mask: 255.255.255.0 + + wireless_access_point: + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + frequency: WIFI_2_4 + acl: + 1: + action: PERMIT + routes: + - address: 192.168.0.0 # PC A subnet + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.1.1 + metric: 0 + links: + - endpoint_a_hostname: pc_a + endpoint_a_port: 1 + endpoint_b_hostname: router_1 + endpoint_b_port: 2 + + - endpoint_a_hostname: pc_b + endpoint_a_port: 1 + endpoint_b_hostname: router_2 + endpoint_b_port: 2 diff --git a/tests/conftest.py b/tests/conftest.py index 980e4aa9..54519e2b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Tuple import pytest import yaml +from ray import init as rayinit from primaite import getLogger, PRIMAITE_PATHS from primaite.game.agent.actions import ActionManager @@ -29,6 +30,7 @@ from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer from tests import TEST_ASSETS_ROOT +rayinit(local_mode=True) ACTION_SPACE_NODE_VALUES = 1 ACTION_SPACE_NODE_ACTION_VALUES = 1 @@ -87,7 +89,10 @@ def service_class(): @pytest.fixture(scope="function") def application(file_system) -> DummyApplication: return DummyApplication( - name="DummyApplication", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="dummy_application") + name="DummyApplication", + port=Port.ARP, + file_system=file_system, + sys_log=SysLog(hostname="dummy_application"), ) @@ -252,8 +257,7 @@ def example_network() -> Network: server_2.power_on() network.connect(endpoint_b=server_2.network_interface[1], endpoint_a=switch_1.network_interface[2]) - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, position=1) assert all(link.is_up for link in network.links.values()) diff --git a/tests/e2e_integration_tests/action_masking/__init__.py b/tests/e2e_integration_tests/action_masking/__init__.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/tests/e2e_integration_tests/action_masking/__init__.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py new file mode 100644 index 00000000..745e280b --- /dev/null +++ b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py @@ -0,0 +1,156 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Dict + +import yaml +from ray.rllib.algorithms.ppo import PPOConfig +from ray.rllib.core.rl_module.marl_module import MultiAgentRLModuleSpec +from ray.rllib.core.rl_module.rl_module import SingleAgentRLModuleSpec +from ray.rllib.examples.rl_modules.classes.action_masking_rlm import ActionMaskingTorchRLModule +from sb3_contrib import MaskablePPO + +from primaite.game.game import PrimaiteGame +from primaite.session.environment import PrimaiteGymEnv +from primaite.session.ray_envs import PrimaiteRayEnv, PrimaiteRayMARLEnv +from tests import TEST_ASSETS_ROOT + +CFG_PATH = TEST_ASSETS_ROOT / "configs/test_primaite_session.yaml" +MARL_PATH = TEST_ASSETS_ROOT / "configs/multi_agent_session.yaml" + + +def test_sb3_action_masking(monkeypatch): + # There's no simple way of capturing what the action mask was at every step, therefore we are mocking the action + # mask function here to save the output of the action mask method and pass through the result back to the agent. + old_action_mask_method = PrimaiteGame.action_mask + mask_history = [] + + def cache_action_mask(obj, agent_name): + mask = old_action_mask_method(obj, agent_name) + mask_history.append(mask) + return mask + + # Even though it's easy to know which CAOS action the agent took by looking at agent history, we don't know which + # action map action integer that was, therefore we cache it by using monkeypatch + action_num_history = [] + + def cache_step(env, action: int): + action_num_history.append(action) + return PrimaiteGymEnv.step(env, action) + + monkeypatch.setattr(PrimaiteGame, "action_mask", cache_action_mask) + env = PrimaiteGymEnv(CFG_PATH) + monkeypatch.setattr(env, "step", lambda action: cache_step(env, action)) + + model = MaskablePPO("MlpPolicy", env, gamma=0.4, seed=32, batch_size=32) + model.learn(256) + + assert len(action_num_history) == len(mask_history) > 0 + # Make sure the masks had at least some False entries, if it was all True then the mask was disabled + assert any([not all(x) for x in mask_history]) + # When the agent takes action N from its action map, we need to have a look at the action mask and make sure that + # the N-th entry was True, meaning that it was a valid action at that step. + # This plucks out the mask history at step i, and at action entry a and checks that it's set to True, and this + # happens for all steps i in the episode + assert all(mask_history[i][a] for i, a in enumerate(action_num_history)) + monkeypatch.undo() + + +def test_ray_single_agent_action_masking(monkeypatch): + """Check that a Ray agent uses the action mask and never chooses invalid actions.""" + with open(CFG_PATH, "r") as f: + cfg = yaml.safe_load(f) + for agent in cfg["agents"]: + if agent["ref"] == "defender": + agent["agent_settings"]["flatten_obs"] = True + + # There's no simple way of capturing what the action mask was at every step, therefore we are mocking the step + # function to save the action mask and the agent's chosen action to a local variable. + old_step_method = PrimaiteRayEnv.step + action_num_history = [] + mask_history = [] + + def cache_step(self, action: int): + action_num_history.append(action) + obs, *_ = old_step_method(self, action) + action_mask = obs["action_mask"] + mask_history.append(action_mask) + return obs, *_ + + monkeypatch.setattr(PrimaiteRayEnv, "step", lambda *args, **kwargs: cache_step(*args, **kwargs)) + + # Configure Ray PPO to use action masking by using the ActionMaskingTorchRLModule + config = ( + PPOConfig() + .api_stack(enable_rl_module_and_learner=True, enable_env_runner_and_connector_v2=True) + .environment(env=PrimaiteRayEnv, env_config=cfg, action_mask_key="action_mask") + .rl_module(rl_module_spec=SingleAgentRLModuleSpec(module_class=ActionMaskingTorchRLModule)) + .env_runners(num_env_runners=0) + .training(train_batch_size=128) + ) + algo = config.build() + algo.train() + + assert len(action_num_history) == len(mask_history) > 0 + # Make sure the masks had at least some False entries, if it was all True then the mask was disabled + assert any([not all(x) for x in mask_history]) + # When the agent takes action N from its action map, we need to have a look at the action mask and make sure that + # the N-th action was valid. + # The first step uses the action mask provided by the reset method, so we are only checking from the second step + # onward, that's why we need to use mask_history[:-1] and action_num_history[1:] + assert all(mask_history[:-1][i][a] for i, a in enumerate(action_num_history[1:])) + monkeypatch.undo() + + +def test_ray_multi_agent_action_masking(monkeypatch): + """Check that Ray agents never take invalid actions when using MARL.""" + with open(MARL_PATH, "r") as f: + cfg = yaml.safe_load(f) + + old_step_method = PrimaiteRayMARLEnv.step + action_num_history = {"defender_1": [], "defender_2": []} + mask_history = {"defender_1": [], "defender_2": []} + + def cache_step(self, actions: Dict[str, int]): + for agent_name, action in actions.items(): + action_num_history[agent_name].append(action) + obs, *_ = old_step_method(self, actions) + for ( + agent_name, + o, + ) in obs.items(): + mask_history[agent_name].append(o["action_mask"]) + return obs, *_ + + monkeypatch.setattr(PrimaiteRayMARLEnv, "step", lambda *args, **kwargs: cache_step(*args, **kwargs)) + + config = ( + PPOConfig() + .multi_agent( + policies={ + "defender_1", + "defender_2", + }, # These names are the same as the agents defined in the example config. + policy_mapping_fn=lambda agent_id, *args, **kwargs: agent_id, + ) + .api_stack(enable_rl_module_and_learner=True, enable_env_runner_and_connector_v2=True) + .environment(env=PrimaiteRayMARLEnv, env_config=cfg, action_mask_key="action_mask") + .rl_module( + rl_module_spec=MultiAgentRLModuleSpec( + module_specs={ + "defender_1": SingleAgentRLModuleSpec(module_class=ActionMaskingTorchRLModule), + "defender_2": SingleAgentRLModuleSpec(module_class=ActionMaskingTorchRLModule), + } + ) + ) + .env_runners(num_env_runners=0) + .training(train_batch_size=128) + ) + algo = config.build() + algo.train() + + for agent_name in ["defender_1", "defender_2"]: + act_hist = action_num_history[agent_name] + mask_hist = mask_history[agent_name] + assert len(act_hist) == len(mask_hist) > 0 + assert any([not all(x) for x in mask_hist]) + assert all(mask_hist[:-1][i][a] for i, a in enumerate(act_hist[1:])) + monkeypatch.undo() diff --git a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py index 96ec799c..26e690d0 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py @@ -1,7 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -import ray import yaml -from ray import air, tune from ray.rllib.algorithms.ppo import PPOConfig from primaite.session.ray_envs import PrimaiteRayMARLEnv @@ -12,12 +10,9 @@ MULTI_AGENT_PATH = TEST_ASSETS_ROOT / "configs/multi_agent_session.yaml" def test_rllib_multi_agent_compatibility(): """Test that the PrimaiteRayEnv class can be used with a multi agent RLLIB system.""" - with open(MULTI_AGENT_PATH, "r") as f: cfg = yaml.safe_load(f) - ray.init() - config = ( PPOConfig() .environment(env=PrimaiteRayMARLEnv, env_config=cfg) @@ -28,15 +23,5 @@ def test_rllib_multi_agent_compatibility(): ) .training(train_batch_size=128) ) - - tune.Tuner( - "PPO", - run_config=air.RunConfig( - stop={"training_iteration": 128}, - checkpoint_config=air.CheckpointConfig( - checkpoint_frequency=10, - ), - ), - param_space=config, - ).fit() - ray.shutdown() + algo = config.build() + algo.train() diff --git a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py index d6cacfd2..265257e4 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py @@ -3,7 +3,6 @@ import tempfile from pathlib import Path import pytest -import ray import yaml from ray.rllib.algorithms import ppo @@ -20,9 +19,6 @@ def test_rllib_single_agent_compatibility(): game = PrimaiteGame.from_config(cfg) - ray.shutdown() - ray.init() - env_config = {"game": game} config = { "env": PrimaiteRayEnv, @@ -41,4 +37,3 @@ def test_rllib_single_agent_compatibility(): assert save_file.exists() save_file.unlink() # clean up - ray.shutdown() diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index 27fb134b..a07d5d2e 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -20,7 +20,7 @@ def test_sb3_compatibility(): gym = PrimaiteGymEnv(env_config=cfg) model = PPO("MlpPolicy", gym) - model.learn(total_timesteps=1000) + model.learn(total_timesteps=256) save_path = Path(tempfile.gettempdir()) / "model.zip" model.save(save_path) diff --git a/tests/e2e_integration_tests/test_environment.py b/tests/e2e_integration_tests/test_environment.py index c8238aba..dcd51193 100644 --- a/tests/e2e_integration_tests/test_environment.py +++ b/tests/e2e_integration_tests/test_environment.py @@ -65,25 +65,25 @@ class TestPrimaiteEnvironment: cfg = yaml.safe_load(f) env = PrimaiteRayMARLEnv(env_config=cfg) - assert set(env._agent_ids) == {"defender1", "defender2"} + assert set(env._agent_ids) == {"defender_1", "defender_2"} assert len(env.agents) == 2 - defender1 = env.agents["defender1"] - defender2 = env.agents["defender2"] - assert (num_actions_1 := len(defender1.action_manager.action_map)) == 54 - assert (num_actions_2 := len(defender2.action_manager.action_map)) == 38 + defender_1 = env.agents["defender_1"] + defender_2 = env.agents["defender_2"] + assert (num_actions_1 := len(defender_1.action_manager.action_map)) == 78 + assert (num_actions_2 := len(defender_2.action_manager.action_map)) == 78 # ensure we can run all valid actions without error for act_1 in range(num_actions_1): - env.step({"defender1": act_1, "defender2": 0}) + env.step({"defender_1": act_1, "defender_2": 0}) for act_2 in range(num_actions_2): - env.step({"defender1": 0, "defender2": act_2}) + env.step({"defender_1": 0, "defender_2": act_2}) # ensure we get error when taking an invalid action with pytest.raises(KeyError): - env.step({"defender1": num_actions_1, "defender2": 0}) + env.step({"defender_1": num_actions_1, "defender_2": 0}) with pytest.raises(KeyError): - env.step({"defender1": 0, "defender2": num_actions_2}) + env.step({"defender_1": 0, "defender_2": num_actions_2}) def test_error_thrown_on_bad_configuration(self): """Make sure we throw an error when the config is bad.""" diff --git a/tests/integration_tests/cli/test_dev_cli.py b/tests/integration_tests/cli/test_dev_cli.py index 43f623a5..19559e7c 100644 --- a/tests/integration_tests/cli/test_dev_cli.py +++ b/tests/integration_tests/cli/test_dev_cli.py @@ -67,7 +67,7 @@ def test_dev_mode_config_sys_log_level(): # check defaults assert PRIMAITE_CONFIG["developer_mode"]["sys_log_level"] == "DEBUG" # DEBUG by default - result = cli(["dev-mode", "config", "-level", "WARNING"]) + result = cli(["dev-mode", "config", "--sys-log-level", "WARNING"]) assert "sys_log_level=WARNING" in result.output # should print correct value @@ -78,10 +78,30 @@ def test_dev_mode_config_sys_log_level(): assert "sys_log_level=INFO" in result.output # should print correct value - # config should reflect that log level is WARNING + # config should reflect that log level is INFO assert PRIMAITE_CONFIG["developer_mode"]["sys_log_level"] == "INFO" +def test_dev_mode_config_agent_log_level(): + """Check that the agent log level can be changed via CLI.""" + # check defaults + assert PRIMAITE_CONFIG["developer_mode"]["agent_log_level"] == "DEBUG" # DEBUG by default + + result = cli(["dev-mode", "config", "-level", "WARNING"]) + + assert "agent_log_level=WARNING" in result.output # should print correct value + + # config should reflect that log level is WARNING + assert PRIMAITE_CONFIG["developer_mode"]["agent_log_level"] == "WARNING" + + result = cli(["dev-mode", "config", "--agent-log-level", "INFO"]) + + assert "agent_log_level=INFO" in result.output # should print correct value + + # config should reflect that log level is INFO + assert PRIMAITE_CONFIG["developer_mode"]["agent_log_level"] == "INFO" + + def test_dev_mode_config_sys_logs_enable_disable(): """Test that the system logs output can be enabled or disabled.""" # check defaults @@ -112,6 +132,36 @@ def test_dev_mode_config_sys_logs_enable_disable(): assert PRIMAITE_CONFIG["developer_mode"]["output_sys_logs"] is False +def test_dev_mode_config_agent_logs_enable_disable(): + """Test that the agent logs output can be enabled or disabled.""" + # check defaults + assert PRIMAITE_CONFIG["developer_mode"]["output_agent_logs"] is False # False by default + + result = cli(["dev-mode", "config", "--output-agent-logs"]) + assert "output_agent_logs=True" in result.output # should print correct value + + # config should reflect that output_agent_logs is True + assert PRIMAITE_CONFIG["developer_mode"]["output_agent_logs"] + + result = cli(["dev-mode", "config", "--no-agent-logs"]) + assert "output_agent_logs=False" in result.output # should print correct value + + # config should reflect that output_agent_logs is True + assert PRIMAITE_CONFIG["developer_mode"]["output_agent_logs"] is False + + result = cli(["dev-mode", "config", "-agent"]) + assert "output_agent_logs=True" in result.output # should print correct value + + # config should reflect that output_agent_logs is True + assert PRIMAITE_CONFIG["developer_mode"]["output_agent_logs"] + + result = cli(["dev-mode", "config", "-nagent"]) + assert "output_agent_logs=False" in result.output # should print correct value + + # config should reflect that output_agent_logs is True + assert PRIMAITE_CONFIG["developer_mode"]["output_agent_logs"] is False + + def test_dev_mode_config_pcap_logs_enable_disable(): """Test that the pcap logs output can be enabled or disabled.""" # check defaults diff --git a/tests/integration_tests/configuration_file_parsing/test_io_settings.py b/tests/integration_tests/configuration_file_parsing/test_io_settings.py index ebaa4956..82977b82 100644 --- a/tests/integration_tests/configuration_file_parsing/test_io_settings.py +++ b/tests/integration_tests/configuration_file_parsing/test_io_settings.py @@ -35,3 +35,7 @@ def test_io_settings(): assert env.io.settings.save_step_metadata is False assert env.io.settings.write_sys_log_to_terminal is False # false by default + + assert env.io.settings.save_agent_logs is True + assert env.io.settings.agent_log_level is LogLevel.INFO + assert env.io.settings.write_agent_log_to_terminal is True # Set to True by the config file. diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py index bf325946..ae4825ff 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py @@ -1,35 +1,23 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import copy -from ipaddress import IPv4Address from pathlib import Path from typing import Union import yaml -from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.interface import ProxyAgent -from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent -from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent -from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING -from primaite.simulator.network.container import Network +from primaite.game.game import PrimaiteGame, SERVICE_TYPES_MAPPING from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot -from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot -from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.simulator.system.services.dns.dns_server import DNSServer -from primaite.simulator.system.services.ftp.ftp_client import FTPClient -from primaite.simulator.system.services.ftp.ftp_server import FTPServer -from primaite.simulator.system.services.ntp.ntp_client import NTPClient -from primaite.simulator.system.services.ntp.ntp_server import NTPServer -from primaite.simulator.system.services.web_server.web_server import WebServer from tests import TEST_ASSETS_ROOT TEST_CONFIG = TEST_ASSETS_ROOT / "configs/software_fix_duration.yaml" ONE_ITEM_CONFIG = TEST_ASSETS_ROOT / "configs/fix_duration_one_item.yaml" +TestApplications = ["DummyApplication", "BroadcastTestClient"] + def load_config(config_path: Union[str, Path]) -> PrimaiteGame: """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" @@ -62,9 +50,12 @@ def test_fix_duration_set_from_config(): assert client_1.software_manager.software.get(service).fixing_duration == 3 # in config - applications take 1 timestep to fix - for applications in APPLICATION_TYPES_MAPPING: - assert client_1.software_manager.software.get(applications) is not None - assert client_1.software_manager.software.get(applications).fixing_duration == 1 + # remove test applications from list + applications = set(Application._application_registry) - set(TestApplications) + + for application in applications: + assert client_1.software_manager.software.get(application) is not None + assert client_1.software_manager.software.get(application).fixing_duration == 1 def test_fix_duration_for_one_item(): @@ -80,8 +71,9 @@ def test_fix_duration_for_one_item(): assert client_1.software_manager.software.get(service).fixing_duration == 2 # in config - applications take 1 timestep to fix - applications = copy.copy(APPLICATION_TYPES_MAPPING) - applications.pop("DatabaseClient") + # remove test applications from list + applications = set(Application._application_registry) - set(TestApplications) + applications.remove("DatabaseClient") for applications in applications: assert client_1.software_manager.software.get(applications) is not None assert client_1.software_manager.software.get(applications).fixing_duration == 2 diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py new file mode 100644 index 00000000..36a7ae57 --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -0,0 +1,54 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.simulator.system.services.service import ServiceOperatingState + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_application_cannot_perform_actions_unless_running(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test the the request permissions prevent any actions unless application is running.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + + browser.close() + assert browser.operating_state == ApplicationOperatingState.CLOSED + + action = ("NODE_APPLICATION_SCAN", {"node_id": 0, "application_id": 0}) + agent.store_action(action) + game.step() + assert browser.operating_state == ApplicationOperatingState.CLOSED + + action = ("NODE_APPLICATION_CLOSE", {"node_id": 0, "application_id": 0}) + agent.store_action(action) + game.step() + assert browser.operating_state == ApplicationOperatingState.CLOSED + + action = ("NODE_APPLICATION_FIX", {"node_id": 0, "application_id": 0}) + agent.store_action(action) + game.step() + assert browser.operating_state == ApplicationOperatingState.CLOSED + + action = ("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}) + agent.store_action(action) + game.step() + assert browser.operating_state == ApplicationOperatingState.CLOSED diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index b7acc8a8..0c9ec6f0 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -99,7 +99,7 @@ class TestConfigureDatabaseAction: game.step() assert db_client.server_ip_address == old_ip - assert db_client.server_password is "admin123" + assert db_client.server_password == "admin123" class TestConfigureRansomwareScriptAction: diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py new file mode 100644 index 00000000..1c143aed --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -0,0 +1,159 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import uuid +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.network.hardware.nodes.host.computer import Computer + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_create_file(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a files to be created.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + random_folder = str(uuid.uuid4()) + random_file = str(uuid.uuid4()) + + assert client_1.file_system.get_file(folder_name=random_folder, file_name=random_file) is None + + action = ( + "NODE_FILE_CREATE", + {"node_id": 0, "folder_name": random_folder, "file_name": random_file}, + ) + agent.store_action(action) + game.step() + + assert client_1.file_system.get_file(folder_name=random_folder, file_name=random_file) is not None + + +def test_file_delete_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a file to be deleted.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + file = client_1.file_system.get_file(folder_name="downloads", file_name="cat.png") + assert file.deleted is False + + action = ( + "NODE_FILE_DELETE", + {"node_id": 0, "folder_id": 0, "file_id": 0}, + ) + agent.store_action(action) + game.step() + + assert file.deleted + + +def test_file_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a file to be scanned.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + file = client_1.file_system.get_file(folder_name="downloads", file_name="cat.png") + + file.corrupt() + assert file.health_status == FileSystemItemHealthStatus.CORRUPT + assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + + action = ( + "NODE_FILE_SCAN", + {"node_id": 0, "folder_id": 0, "file_id": 0}, + ) + agent.store_action(action) + game.step() + + assert file.health_status == FileSystemItemHealthStatus.CORRUPT + assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_file_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a folder to be created.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + file = client_1.file_system.get_file(folder_name="downloads", file_name="cat.png") + + file.corrupt() + assert file.health_status == FileSystemItemHealthStatus.CORRUPT + + action = ( + "NODE_FILE_REPAIR", + {"node_id": 0, "folder_id": 0, "file_id": 0}, + ) + agent.store_action(action) + game.step() + + assert file.health_status == FileSystemItemHealthStatus.GOOD + + +def test_file_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a file to be restored.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + file = client_1.file_system.get_file(folder_name="downloads", file_name="cat.png") + + file.corrupt() + assert file.health_status == FileSystemItemHealthStatus.CORRUPT + + action = ( + "NODE_FILE_RESTORE", + {"node_id": 0, "folder_id": 0, "file_id": 0}, + ) + agent.store_action(action) + game.step() + + assert file.health_status == FileSystemItemHealthStatus.GOOD + + +def test_file_corrupt_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a file to be corrupted.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + file = client_1.file_system.get_file(folder_name="downloads", file_name="cat.png") + + assert file.health_status == FileSystemItemHealthStatus.GOOD + + action = ( + "NODE_FILE_CORRUPT", + {"node_id": 0, "folder_id": 0, "file_id": 0}, + ) + agent.store_action(action) + game.step() + + assert file.health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_file_access_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a file to be accessed.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + file = client_1.file_system.get_file(folder_name="downloads", file_name="cat.png") + assert file.num_access == 0 + + action = ( + "NODE_FILE_ACCESS", + {"node_id": 0, "folder_name": file.folder_name, "file_name": file.name}, + ) + agent.store_action(action) + game.step() + + assert file.num_access == 1 diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py new file mode 100644 index 00000000..e5e0806a --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -0,0 +1,123 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import uuid +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.network.hardware.nodes.host.computer import Computer + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_create_folder(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator allows a folder to be created.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + random_folder = str(uuid.uuid4()) + + assert client_1.file_system.get_folder(folder_name=random_folder) is None + + action = ( + "NODE_FOLDER_CREATE", + { + "node_id": 0, + "folder_name": random_folder, + }, + ) + agent.store_action(action) + game.step() + + assert client_1.file_system.get_folder(folder_name=random_folder) is not None + + +def test_folder_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test to make sure that the validator checks if the folder exists before scanning.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + folder = client_1.file_system.get_folder(folder_name="downloads") + assert folder.health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + + folder.corrupt() + + assert folder.health_status == FileSystemItemHealthStatus.CORRUPT + assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + + action = ( + "NODE_FOLDER_SCAN", + { + "node_id": 0, # client_1, + "folder_id": 0, # downloads + }, + ) + agent.store_action(action) + game.step() + + for i in range(folder.scan_duration + 1): + game.step() + + assert folder.health_status == FileSystemItemHealthStatus.CORRUPT + assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_folder_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test to make sure that the validator checks if the folder exists before repairing.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + folder = client_1.file_system.get_folder(folder_name="downloads") + folder.corrupt() + assert folder.health_status == FileSystemItemHealthStatus.CORRUPT + + action = ( + "NODE_FOLDER_REPAIR", + { + "node_id": 0, # client_1, + "folder_id": 0, # downloads + }, + ) + agent.store_action(action) + game.step() + + assert folder.health_status == FileSystemItemHealthStatus.GOOD + + +def test_folder_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test to make sure that the validator checks if the folder exists before restoring.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + folder = client_1.file_system.get_folder(folder_name="downloads") + folder.corrupt() + + assert folder.health_status == FileSystemItemHealthStatus.CORRUPT + + action = ( + "NODE_FOLDER_RESTORE", + { + "node_id": 0, # client_1, + "folder_id": 0, # downloads + }, + ) + agent.store_action(action) + game.step() + + assert folder.health_status == FileSystemItemHealthStatus.RESTORING diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py new file mode 100644 index 00000000..d796b75e --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -0,0 +1,95 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.nodes.host.computer import Computer + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_nic_cannot_be_turned_off_if_not_on(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that a NIC cannot be disabled if it is not enabled.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + nic = client_1.network_interface[1] + nic.disable() + assert nic.enabled is False + + action = ( + "HOST_NIC_DISABLE", + { + "node_id": 0, # client_1 + "nic_id": 0, # the only nic (eth-1) + }, + ) + agent.store_action(action) + game.step() + + assert nic.enabled is False + + +def test_nic_cannot_be_turned_on_if_already_on(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that a NIC cannot be enabled if it is already enabled.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + nic = client_1.network_interface[1] + assert nic.enabled + + action = ( + "HOST_NIC_ENABLE", + { + "node_id": 0, # client_1 + "nic_id": 0, # the only nic (eth-1) + }, + ) + agent.store_action(action) + game.step() + + assert nic.enabled + + +def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a NIC can be enabled and disabled.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + nic = client_1.network_interface[1] + assert nic.enabled + + action = ( + "HOST_NIC_DISABLE", + { + "node_id": 0, # client_1 + "nic_id": 0, # the only nic (eth-1) + }, + ) + agent.store_action(action) + game.step() + + assert nic.enabled is False + + action = ( + "HOST_NIC_ENABLE", + { + "node_id": 0, # client_1 + "nic_id": 0, # the only nic (eth-1) + }, + ) + agent.store_action(action) + game.step() + + assert nic.enabled diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py new file mode 100644 index 00000000..fdf04ad5 --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -0,0 +1,94 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.computer import Computer + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the node can be shut down and started up.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + assert client_1.operating_state == NodeOperatingState.ON + + # turn it off + action = ("NODE_SHUTDOWN", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.SHUTTING_DOWN + + for i in range(client_1.shut_down_duration + 1): + action = ("DONOTHING", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.OFF + + # turn it on + action = ("NODE_STARTUP", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.BOOTING + + for i in range(client_1.start_up_duration + 1): + action = ("DONOTHING", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.ON + + +def test_node_cannot_be_started_up_if_node_is_already_on(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that a node cannot be started up if it is already on.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + assert client_1.operating_state == NodeOperatingState.ON + + # turn it on + action = ("NODE_STARTUP", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.ON + + +def test_node_cannot_be_shut_down_if_node_is_already_off(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that a node cannot be shut down if it is already off.""" + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + client_1.power_off() + + for i in range(client_1.shut_down_duration + 1): + action = ("DONOTHING", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.OFF + + # turn it ff + action = ("NODE_SHUTDOWN", {"node_id": 0}) + agent.store_action(action) + game.step() + + assert client_1.operating_state == NodeOperatingState.OFF diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py new file mode 100644 index 00000000..3054c73b --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -0,0 +1,106 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.system.services.service import ServiceOperatingState + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator makes sure that the service is stopped before starting the service.""" + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + dns_server = server_1.software_manager.software.get("DNSServer") + + dns_server.pause() + assert dns_server.operating_state == ServiceOperatingState.PAUSED + + action = ("NODE_SERVICE_START", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.PAUSED + + dns_server.stop() + + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + action = ("NODE_SERVICE_START", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + + assert dns_server.operating_state == ServiceOperatingState.RUNNING + + +def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test that the validator checks if the service is paused before resuming.""" + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + dns_server = server_1.software_manager.software.get("DNSServer") + + action = ("NODE_SERVICE_RESUME", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.RUNNING + + dns_server.pause() + + assert dns_server.operating_state == ServiceOperatingState.PAUSED + + action = ("NODE_SERVICE_RESUME", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + + assert dns_server.operating_state == ServiceOperatingState.RUNNING + + +def test_service_cannot_perform_actions_unless_running(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Test to make sure that the service cannot perform certain actions while not running.""" + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + dns_server = server_1.software_manager.software.get("DNSServer") + + dns_server.stop() + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + action = ("NODE_SERVICE_SCAN", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + action = ("NODE_SERVICE_PAUSE", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + action = ("NODE_SERVICE_RESUME", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + action = ("NODE_SERVICE_RESTART", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + action = ("NODE_SERVICE_FIX", {"node_id": 1, "service_id": 0}) + agent.store_action(action) + game.step() + assert dns_server.operating_state == ServiceOperatingState.STOPPED diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index f1a8ea92..88dd2bd5 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -155,7 +155,7 @@ def test_nic_monitored_traffic(simulation): assert traffic_obs["icmp"]["outbound"] == 0 # send a ping - pc.ping(target_ip_address=pc2.network_interface[1].ip_address) + assert pc.ping(target_ip_address=pc2.network_interface[1].ip_address) traffic_obs = nic_obs.observe(simulation.describe_state()).get("TRAFFIC") assert traffic_obs["icmp"]["inbound"] == 1 @@ -178,7 +178,7 @@ def test_nic_monitored_traffic(simulation): traffic_obs = nic_obs.observe(simulation.describe_state()).get("TRAFFIC") assert traffic_obs["icmp"]["inbound"] == 0 assert traffic_obs["icmp"]["outbound"] == 0 - assert traffic_obs["tcp"][53]["inbound"] == 0 + assert traffic_obs["tcp"][53]["inbound"] == 1 assert traffic_obs["tcp"][53]["outbound"] == 1 # getting a webpage sent a dns request out simulation.pre_timestep(2) # apply timestep to whole sim diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py new file mode 100644 index 00000000..64464724 --- /dev/null +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -0,0 +1,161 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.session.environment import PrimaiteGymEnv +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode +from primaite.simulator.system.services.service import ServiceOperatingState +from tests.conftest import TEST_ASSETS_ROOT + +CFG_PATH = TEST_ASSETS_ROOT / "configs/test_primaite_session.yaml" + + +def test_mask_contents_correct(): + env = PrimaiteGymEnv(CFG_PATH) + game = env.game + sim = game.simulation + net = sim.network + mask = game.action_mask("defender") + agent = env.agent + node_list = agent.action_manager.node_names + action_map = agent.action_manager.action_map + + # CHECK NIC ENABLE/DISABLE ACTIONS + for action_num, action in action_map.items(): + mask = game.action_mask("defender") + act_type, act_params = action + + if act_type == "NODE_NIC_ENABLE": + node_name = node_list[act_params["node_id"]] + node_obj = net.get_node_by_hostname(node_name) + nic_obj = node_obj.network_interface[act_params["nic_id"] + 1] + assert nic_obj.enabled + assert not mask[action_num] + nic_obj.disable() + mask = game.action_mask("defender") + assert mask[action_num] + nic_obj.enable() + + if act_type == "NODE_NIC_DISABLE": + node_name = node_list[act_params["node_id"]] + node_obj = net.get_node_by_hostname(node_name) + nic_obj = node_obj.network_interface[act_params["nic_id"] + 1] + assert nic_obj.enabled + assert mask[action_num] + nic_obj.disable() + mask = game.action_mask("defender") + assert not mask[action_num] + nic_obj.enable() + + if act_type == "ROUTER_ACL_ADDRULE": + assert mask[action_num] + + if act_type == "ROUTER_ACL_REMOVERULE": + assert mask[action_num] + + if act_type == "NODE_RESET": + node_name = node_list[act_params["node_id"]] + node_obj = net.get_node_by_hostname(node_name) + assert node_obj.operating_state is NodeOperatingState.ON + assert mask[action_num] + node_obj.operating_state = NodeOperatingState.OFF + mask = game.action_mask("defender") + assert not mask[action_num] + node_obj.operating_state = NodeOperatingState.ON + + if act_type == "NODE_SHUTDOWN": + node_name = node_list[act_params["node_id"]] + node_obj = net.get_node_by_hostname(node_name) + assert node_obj.operating_state is NodeOperatingState.ON + assert mask[action_num] + node_obj.operating_state = NodeOperatingState.OFF + mask = game.action_mask("defender") + assert not mask[action_num] + node_obj.operating_state = NodeOperatingState.ON + + if act_type == "NODE_OS_SCAN": + node_name = node_list[act_params["node_id"]] + node_obj = net.get_node_by_hostname(node_name) + assert node_obj.operating_state is NodeOperatingState.ON + assert mask[action_num] + node_obj.operating_state = NodeOperatingState.OFF + mask = game.action_mask("defender") + assert not mask[action_num] + node_obj.operating_state = NodeOperatingState.ON + + if act_type == "NODE_STARTUP": + node_name = node_list[act_params["node_id"]] + node_obj = net.get_node_by_hostname(node_name) + assert node_obj.operating_state is NodeOperatingState.ON + assert not mask[action_num] + node_obj.operating_state = NodeOperatingState.OFF + mask = game.action_mask("defender") + assert mask[action_num] + node_obj.operating_state = NodeOperatingState.ON + + if act_type == "DONOTHING": + assert mask[action_num] + + if act_type == "NODE_SERVICE_DISABLE": + assert mask[action_num] + + if act_type in ["NODE_SERVICE_SCAN", "NODE_SERVICE_STOP", "NODE_SERVICE_PAUSE"]: + node_name = node_list[act_params["node_id"]] + service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + node_obj = net.get_node_by_hostname(node_name) + service_obj = node_obj.software_manager.software.get(service_name) + assert service_obj.operating_state is ServiceOperatingState.RUNNING + assert mask[action_num] + service_obj.operating_state = ServiceOperatingState.DISABLED + mask = game.action_mask("defender") + assert not mask[action_num] + service_obj.operating_state = ServiceOperatingState.RUNNING + + if act_type == "NODE_SERVICE_RESUME": + node_name = node_list[act_params["node_id"]] + service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + node_obj = net.get_node_by_hostname(node_name) + service_obj = node_obj.software_manager.software.get(service_name) + assert service_obj.operating_state is ServiceOperatingState.RUNNING + assert not mask[action_num] + service_obj.operating_state = ServiceOperatingState.PAUSED + mask = game.action_mask("defender") + assert mask[action_num] + service_obj.operating_state = ServiceOperatingState.RUNNING + + if act_type == "NODE_SERVICE_START": + node_name = node_list[act_params["node_id"]] + service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + node_obj = net.get_node_by_hostname(node_name) + service_obj = node_obj.software_manager.software.get(service_name) + assert service_obj.operating_state is ServiceOperatingState.RUNNING + assert not mask[action_num] + service_obj.operating_state = ServiceOperatingState.STOPPED + mask = game.action_mask("defender") + assert mask[action_num] + service_obj.operating_state = ServiceOperatingState.RUNNING + + if act_type == "NODE_SERVICE_ENABLE": + node_name = node_list[act_params["node_id"]] + service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + node_obj = net.get_node_by_hostname(node_name) + service_obj = node_obj.software_manager.software.get(service_name) + assert service_obj.operating_state is ServiceOperatingState.RUNNING + assert not mask[action_num] + service_obj.operating_state = ServiceOperatingState.DISABLED + mask = game.action_mask("defender") + assert mask[action_num] + service_obj.operating_state = ServiceOperatingState.RUNNING + + if act_type in ["NODE_FILE_SCAN", "NODE_FILE_CHECKHASH", "NODE_FILE_DELETE"]: + node_name = node_list[act_params["node_id"]] + folder_name = agent.action_manager.get_folder_name_by_idx(act_params["node_id"], act_params["folder_id"]) + file_name = agent.action_manager.get_file_name_by_idx( + act_params["node_id"], act_params["folder_id"], act_params["file_id"] + ) + node_obj = net.get_node_by_hostname(node_name) + file_obj = node_obj.file_system.get_file(folder_name, file_name, include_deleted=True) + assert not file_obj.deleted + assert mask[action_num] + service_obj.operating_state = ServiceOperatingState.DISABLED + mask = game.action_mask("defender") + assert mask[action_num] + service_obj.operating_state = ServiceOperatingState.RUNNING diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py new file mode 100644 index 00000000..78d00b47 --- /dev/null +++ b/tests/integration_tests/network/test_airspace_config.py @@ -0,0 +1,44 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import yaml + +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.airspace import AirSpaceFrequency +from tests import TEST_ASSETS_ROOT + + +def test_override_freq_max_capacity_mbps(): + config_path = TEST_ASSETS_ROOT / "configs" / "wireless_wan_network_config_freq_max_override.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_2_4) == 123.45 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_5) == 0.0 + + pc_a = network.get_node_by_hostname("pc_a") + pc_b = network.get_node_by_hostname("pc_b") + + assert pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should be able to ping PC B" + assert pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should be able to ping PC A" + + network.airspace.show() + + +def test_override_freq_max_capacity_mbps_blocked(): + config_path = TEST_ASSETS_ROOT / "configs" / "wireless_wan_network_config_freq_max_override_blocked.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_2_4) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_5) == 0.0 + + pc_a = network.get_node_by_hostname("pc_a") + pc_b = network.get_node_by_hostname("pc_b") + + assert not pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should not be able to ping PC B" + assert not pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should not be able to ping PC A" + + network.airspace.show() diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py new file mode 100644 index 00000000..b7317c3d --- /dev/null +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -0,0 +1,114 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.simulator.file_system.file_type import FileType +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from tests.integration_tests.network.test_wireless_router import wireless_wan_network +from tests.integration_tests.system.test_ftp_client_server import ftp_client_and_ftp_server + + +def test_wireless_link_loading(wireless_wan_network): + client, server, router_1, router_2 = wireless_wan_network + + # Configure Router 1 ACLs + router_1.acl.add_rule(action=ACLAction.PERMIT, position=1) + + # Configure Router 2 ACLs + router_2.acl.add_rule(action=ACLAction.PERMIT, position=1) + + airspace = router_1.airspace + + client.software_manager.install(FTPClient) + ftp_client: FTPClient = client.software_manager.software.get("FTPClient") + ftp_client.start() + + server.software_manager.install(FTPServer) + ftp_server: FTPServer = server.software_manager.software.get("FTPServer") + ftp_server.start() + + client.file_system.create_file(file_name="mixtape", size=10 * 10**6, file_type=FileType.MP3, folder_name="music") + + assert ftp_client.send_file( + src_file_name="mixtape.mp3", + src_folder_name="music", + dest_ip_address=server.network_interface[1].ip_address, + dest_file_name="mixtape.mp3", + dest_folder_name="music", + ) + + # Reset the physical links between the host nodes and the routers + client.network_interface[1]._connected_link.pre_timestep(1) + server.network_interface[1]._connected_link.pre_timestep(1) + + assert not ftp_client.send_file( + src_file_name="mixtape.mp3", + src_folder_name="music", + dest_ip_address=server.network_interface[1].ip_address, + dest_file_name="mixtape3.mp3", + dest_folder_name="music", + ) + + # Reset the physical links between the host nodes and the routers + client.network_interface[1]._connected_link.pre_timestep(1) + server.network_interface[1]._connected_link.pre_timestep(1) + + airspace.reset_bandwidth_load() + + assert ftp_client.send_file( + src_file_name="mixtape.mp3", + src_folder_name="music", + dest_ip_address=server.network_interface[1].ip_address, + dest_file_name="mixtape3.mp3", + dest_folder_name="music", + ) + + +def test_wired_link_loading(ftp_client_and_ftp_server): + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server + + link = computer.network_interface[1]._connected_link # noqa + + assert link.is_up + + link.pre_timestep(1) + + computer.file_system.create_file( + file_name="mixtape", size=10 * 10**6, file_type=FileType.MP3, folder_name="music" + ) + link_load = link.current_load + assert link_load == 0.0 + + assert ftp_client.send_file( + src_file_name="mixtape.mp3", + src_folder_name="music", + dest_ip_address=server.network_interface[1].ip_address, + dest_file_name="mixtape.mp3", + dest_folder_name="music", + ) + + new_link_load = link.current_load + assert new_link_load > link_load + + assert not ftp_client.send_file( + src_file_name="mixtape.mp3", + src_folder_name="music", + dest_ip_address=server.network_interface[1].ip_address, + dest_file_name="mixtape1.mp3", + dest_folder_name="music", + ) + + link.pre_timestep(2) + + link_load = link.current_load + assert link_load == 0.0 + + assert ftp_client.send_file( + src_file_name="mixtape.mp3", + src_folder_name="music", + dest_ip_address=server.network_interface[1].ip_address, + dest_file_name="mixtape1.mp3", + dest_folder_name="music", + ) + + new_link_load = link.current_load + assert new_link_load > link_load diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index b89d6db6..80007c46 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -14,7 +14,7 @@ from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.service import Service -class BroadcastService(Service): +class BroadcastTestService(Service): """A service for sending broadcast and unicast messages over a network.""" def __init__(self, **kwargs): @@ -41,14 +41,14 @@ class BroadcastService(Service): super().send(payload="broadcast", dest_ip_address=ip_network, dest_port=Port.HTTP, ip_protocol=self.protocol) -class BroadcastClient(Application, identifier="BroadcastClient"): +class BroadcastTestClient(Application, identifier="BroadcastTestClient"): """A client application to receive broadcast and unicast messages.""" payloads_received: List = [] def __init__(self, **kwargs): # Set default client properties - kwargs["name"] = "BroadcastClient" + kwargs["name"] = "BroadcastTestClient" kwargs["port"] = Port.HTTP kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) @@ -75,8 +75,8 @@ def broadcast_network() -> Network: start_up_duration=0, ) client_1.power_on() - client_1.software_manager.install(BroadcastClient) - application_1 = client_1.software_manager.software["BroadcastClient"] + client_1.software_manager.install(BroadcastTestClient) + application_1 = client_1.software_manager.software["BroadcastTestClient"] application_1.run() client_2 = Computer( @@ -87,8 +87,8 @@ def broadcast_network() -> Network: start_up_duration=0, ) client_2.power_on() - client_2.software_manager.install(BroadcastClient) - application_2 = client_2.software_manager.software["BroadcastClient"] + client_2.software_manager.install(BroadcastTestClient) + application_2 = client_2.software_manager.software["BroadcastTestClient"] application_2.run() server_1 = Server( @@ -100,8 +100,8 @@ def broadcast_network() -> Network: ) server_1.power_on() - server_1.software_manager.install(BroadcastService) - service: BroadcastService = server_1.software_manager.software["BroadcastService"] + server_1.software_manager.install(BroadcastTestService) + service: BroadcastTestService = server_1.software_manager.software["BroadcastService"] service.start() switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) @@ -115,14 +115,16 @@ def broadcast_network() -> Network: @pytest.fixture(scope="function") -def broadcast_service_and_clients(broadcast_network) -> Tuple[BroadcastService, BroadcastClient, BroadcastClient]: - client_1: BroadcastClient = broadcast_network.get_node_by_hostname("client_1").software_manager.software[ - "BroadcastClient" +def broadcast_service_and_clients( + broadcast_network, +) -> Tuple[BroadcastTestService, BroadcastTestClient, BroadcastTestClient]: + client_1: BroadcastTestClient = broadcast_network.get_node_by_hostname("client_1").software_manager.software[ + "BroadcastTestClient" ] - client_2: BroadcastClient = broadcast_network.get_node_by_hostname("client_2").software_manager.software[ - "BroadcastClient" + client_2: BroadcastTestClient = broadcast_network.get_node_by_hostname("client_2").software_manager.software[ + "BroadcastTestClient" ] - service: BroadcastService = broadcast_network.get_node_by_hostname("server_1").software_manager.software[ + service: BroadcastTestService = broadcast_network.get_node_by_hostname("server_1").software_manager.software[ "BroadcastService" ] diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index a261f272..08251d71 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -101,6 +101,7 @@ def test_port_scan_full_subnet_all_ports_and_protocols(example_network): actual_result = client_1_nmap.port_scan( target_ip_address=IPv4Network("192.168.10.0/24"), + target_port=[Port.ARP, Port.HTTP, Port.FTP, Port.DNS, Port.NTP], ) expected_result = { diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py new file mode 100644 index 00000000..d61e1a23 --- /dev/null +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py @@ -0,0 +1,137 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from uuid import uuid4 + +import pytest + +from primaite import PRIMAITE_CONFIG +from primaite.game.agent.agent_log import AgentLog +from primaite.simulator import LogLevel, SIM_OUTPUT + + +@pytest.fixture(autouse=True) +def override_dev_mode_temporarily(): + """Temporarily turn off dev mode for this test.""" + primaite_dev_mode = PRIMAITE_CONFIG["developer_mode"]["enabled"] + PRIMAITE_CONFIG["developer_mode"]["enabled"] = False + yield # run tests + PRIMAITE_CONFIG["developer_mode"]["enabled"] = primaite_dev_mode + + +@pytest.fixture(scope="function") +def agentlog() -> AgentLog: + return AgentLog(agent_name="test_agent") + + +def test_debug_agent_log_level(agentlog, capsys): + """Test that the debug log level logs debug agent logs and above.""" + SIM_OUTPUT.agent_log_level = LogLevel.DEBUG + SIM_OUTPUT.write_agent_log_to_terminal = True + + test_string = str(uuid4()) + + agentlog.debug(msg=test_string) + agentlog.info(msg=test_string) + agentlog.warning(msg=test_string) + agentlog.error(msg=test_string) + agentlog.critical(msg=test_string) + + captured = "".join(capsys.readouterr()) + + assert test_string in captured + assert "DEBUG" in captured + assert "INFO" in captured + assert "WARNING" in captured + assert "ERROR" in captured + assert "CRITICAL" in captured + + +def test_info_agent_log_level(agentlog, capsys): + """Test that the debug log level logs debug agent logs and above.""" + SIM_OUTPUT.agent_log_level = LogLevel.INFO + SIM_OUTPUT.write_agent_log_to_terminal = True + + test_string = str(uuid4()) + + agentlog.debug(msg=test_string) + agentlog.info(msg=test_string) + agentlog.warning(msg=test_string) + agentlog.error(msg=test_string) + agentlog.critical(msg=test_string) + + captured = "".join(capsys.readouterr()) + + assert test_string in captured + assert "DEBUG" not in captured + assert "INFO" in captured + assert "WARNING" in captured + assert "ERROR" in captured + assert "CRITICAL" in captured + + +def test_warning_agent_log_level(agentlog, capsys): + """Test that the debug log level logs debug agent logs and above.""" + SIM_OUTPUT.agent_log_level = LogLevel.WARNING + SIM_OUTPUT.write_agent_log_to_terminal = True + + test_string = str(uuid4()) + + agentlog.debug(msg=test_string) + agentlog.info(msg=test_string) + agentlog.warning(msg=test_string) + agentlog.error(msg=test_string) + agentlog.critical(msg=test_string) + + captured = "".join(capsys.readouterr()) + + assert test_string in captured + assert "DEBUG" not in captured + assert "INFO" not in captured + assert "WARNING" in captured + assert "ERROR" in captured + assert "CRITICAL" in captured + + +def test_error_agent_log_level(agentlog, capsys): + """Test that the debug log level logs debug agent logs and above.""" + SIM_OUTPUT.agent_log_level = LogLevel.ERROR + SIM_OUTPUT.write_agent_log_to_terminal = True + + test_string = str(uuid4()) + + agentlog.debug(msg=test_string) + agentlog.info(msg=test_string) + agentlog.warning(msg=test_string) + agentlog.error(msg=test_string) + agentlog.critical(msg=test_string) + + captured = "".join(capsys.readouterr()) + + assert test_string in captured + assert "DEBUG" not in captured + assert "INFO" not in captured + assert "WARNING" not in captured + assert "ERROR" in captured + assert "CRITICAL" in captured + + +def test_critical_agent_log_level(agentlog, capsys): + """Test that the debug log level logs debug agent logs and above.""" + SIM_OUTPUT.agent_log_level = LogLevel.CRITICAL + SIM_OUTPUT.write_agent_log_to_terminal = True + + test_string = str(uuid4()) + + agentlog.debug(msg=test_string) + agentlog.info(msg=test_string) + agentlog.warning(msg=test_string) + agentlog.error(msg=test_string) + agentlog.critical(msg=test_string) + + captured = "".join(capsys.readouterr()) + + assert test_string in captured + assert "DEBUG" not in captured + assert "INFO" not in captured + assert "WARNING" not in captured + assert "ERROR" not in captured + assert "CRITICAL" in captured diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 295bca08..594c7afe 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -26,7 +26,7 @@ def test_file_scan_request(populated_file_system): assert file.health_status == FileSystemItemHealthStatus.CORRUPT assert file.visible_health_status == FileSystemItemHealthStatus.GOOD - fs.apply_request(request=["file", file.name, "scan"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "scan"]) assert file.health_status == FileSystemItemHealthStatus.CORRUPT assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT @@ -37,12 +37,12 @@ def test_file_checkhash_request(populated_file_system): """Test that an agent can request a file hash check.""" fs, folder, file = populated_file_system - fs.apply_request(request=["file", file.name, "checkhash"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "checkhash"]) assert file.health_status == FileSystemItemHealthStatus.GOOD file.sim_size = 0 - fs.apply_request(request=["file", file.name, "checkhash"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "checkhash"]) assert file.health_status == FileSystemItemHealthStatus.CORRUPT @@ -54,7 +54,7 @@ def test_file_repair_request(populated_file_system): file.corrupt() assert file.health_status == FileSystemItemHealthStatus.CORRUPT - fs.apply_request(request=["file", file.name, "repair"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "repair"]) assert file.health_status == FileSystemItemHealthStatus.GOOD @@ -71,7 +71,7 @@ def test_file_restore_request(populated_file_system): assert fs.get_file(folder_name=folder.name, file_name=file.name) is not None assert fs.get_file(folder_name=folder.name, file_name=file.name).deleted is False - fs.apply_request(request=["file", file.name, "corrupt"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "corrupt"]) assert fs.get_file(folder_name=folder.name, file_name=file.name).health_status == FileSystemItemHealthStatus.CORRUPT fs.apply_request(request=["restore", "file", folder.name, file.name]) @@ -81,7 +81,7 @@ def test_file_restore_request(populated_file_system): def test_file_corrupt_request(populated_file_system): """Test that an agent can request a file corruption.""" fs, folder, file = populated_file_system - fs.apply_request(request=["file", file.name, "corrupt"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "corrupt"]) assert file.health_status == FileSystemItemHealthStatus.CORRUPT @@ -90,7 +90,7 @@ def test_deleted_file_cannot_be_interacted_with(populated_file_system): fs, folder, file = populated_file_system assert fs.get_file(folder_name=folder.name, file_name=file.name) is not None - fs.apply_request(request=["file", file.name, "corrupt"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "corrupt"]) assert fs.get_file(folder_name=folder.name, file_name=file.name).health_status == FileSystemItemHealthStatus.CORRUPT assert ( fs.get_file(folder_name=folder.name, file_name=file.name).visible_health_status diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py index 209668c4..7d022ea4 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py @@ -39,3 +39,39 @@ def test_folder_delete_request(populated_file_system): assert fs.get_file_by_id(folder_uuid=folder.uuid, file_uuid=file.uuid) is None fs.show(full=True) + + +def test_folder_exists_request_validator(populated_file_system): + """Tests that the _FolderExistsValidator works as intended.""" + fs, folder, file = populated_file_system + validator = FileSystem._FolderExistsValidator(file_system=fs) + + assert validator(request=["test_folder"], context={}) # test_folder exists + assert validator(request=["fake_folder"], context={}) is False # fake_folder does not exist + + assert validator.fail_message == "Cannot perform request on folder because it does not exist." + + +def test_file_exists_request_validator(populated_file_system): + """Tests that the _FolderExistsValidator works as intended.""" + fs, folder, file = populated_file_system + validator = FileSystem._FileExistsValidator(file_system=fs) + + assert validator(request=["test_folder", "test_file.txt"], context={}) # test_file.txt exists + assert validator(request=["test_folder", "fake_file.txt"], context={}) is False # fake_file.txt does not exist + + assert validator.fail_message == "Cannot perform request on a file that does not exist." + + +def test_folder_not_deleted_request_validator(populated_file_system): + """Tests that the _FolderExistsValidator works as intended.""" + fs, folder, file = populated_file_system + validator = FileSystem._FolderNotDeletedValidator(file_system=fs) + + assert validator(request=["test_folder"], context={}) # test_folder is not deleted + + fs.delete_folder(folder_name="test_folder") + + assert validator(request=["test_folder"], context={}) is False # test_folder is deleted + + assert validator.fail_message == "Cannot perform request on folder because it is deleted." diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 40f1e78b..4a561b97 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -166,15 +166,40 @@ def test_deleted_folder_and_its_files_cannot_be_interacted_with(populated_file_s fs, folder, file = populated_file_system assert fs.get_file(folder_name=folder.name, file_name=file.name) is not None - fs.apply_request(request=["file", file.name, "corrupt"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "corrupt"]) assert fs.get_file(folder_name=folder.name, file_name=file.name).health_status == FileSystemItemHealthStatus.CORRUPT fs.apply_request(request=["delete", "folder", folder.name]) assert fs.get_file(folder_name=folder.name, file_name=file.name) is None - fs.apply_request(request=["file", file.name, "repair"]) + fs.apply_request(request=["folder", folder.name, "file", file.name, "repair"]) deleted_folder = fs.deleted_folders.get(folder.uuid) deleted_file = deleted_folder.deleted_files.get(file.uuid) assert deleted_file.health_status is not FileSystemItemHealthStatus.GOOD + + +def test_file_exists_request_validator(populated_file_system): + """Tests that the _FolderExistsValidator works as intended.""" + fs, folder, file = populated_file_system + validator = Folder._FileExistsValidator(folder=folder) + + assert validator(request=["test_file.txt"], context={}) # test_file.txt exists + assert validator(request=["fake_file.txt"], context={}) is False # fake_file.txt does not exist + + assert validator.fail_message == "Cannot perform request on a file that does not exist." + + +def test_file_not_deleted_request_validator(populated_file_system): + """Tests that the _FolderExistsValidator works as intended.""" + fs, folder, file = populated_file_system + validator = Folder._FileNotDeletedValidator(folder=folder) + + assert validator(request=["test_file.txt"], context={}) # test_file.txt is not deleted + + fs.delete_file(folder_name="test_folder", file_name="test_file.txt") + + assert validator(request=["fake_file.txt"], context={}) is False # test_file.txt is deleted + + assert validator.fail_message == "Cannot perform request on a file that is deleted." diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py new file mode 100644 index 00000000..f35cf171 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py @@ -0,0 +1,34 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.simulator.network.hardware.base import NetworkInterface, Node +from primaite.simulator.network.hardware.nodes.host.computer import Computer + + +@pytest.fixture +def node() -> Node: + return Computer(hostname="test", ip_address="192.168.1.2", subnet_mask="255.255.255.0") + + +def test_nic_enabled_validator(node): + """Test the NetworkInterface enabled validator.""" + network_interface = node.network_interface[1] + validator = NetworkInterface._EnabledValidator(network_interface=network_interface) + + assert validator(request=[], context={}) is False # not enabled + + network_interface.enabled = True + + assert validator(request=[], context={}) # enabled + + +def test_nic_disabled_validator(node): + """Test the NetworkInterface enabled validator.""" + network_interface = node.network_interface[1] + validator = NetworkInterface._DisabledValidator(network_interface=network_interface) + + assert validator(request=[], context={}) # not enabled + + network_interface.enabled = True + + assert validator(request=[], context={}) is False # enabled diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 57d6cecb..9b37ac80 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -155,3 +155,39 @@ def test_reset_node(node): assert node.operating_state == NodeOperatingState.BOOTING assert node.operating_state == NodeOperatingState.ON + + +def test_node_is_on_validator(node): + """Test that the node is on validator.""" + node.power_on() + + for i in range(node.start_up_duration + 1): + node.apply_timestep(i) + + validator = Node._NodeIsOnValidator(node=node) + + assert validator(request=[], context={}) + + node.power_off() + for i in range(node.shut_down_duration + 1): + node.apply_timestep(i) + + assert validator(request=[], context={}) is False + + +def test_node_is_off_validator(node): + """Test that the node is on validator.""" + node.power_on() + + for i in range(node.start_up_duration + 1): + node.apply_timestep(i) + + validator = Node._NodeIsOffValidator(node=node) + + assert validator(request=[], context={}) is False + + node.power_off() + for i in range(node.shut_down_duration + 1): + node.apply_timestep(i) + + assert validator(request=[], context={}) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py index be6c00e7..0e9c536c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py @@ -1 +1,15 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.simulator.system.applications.application import Application, ApplicationOperatingState + + +def test_application_state_validator(application): + """Test the application state validator.""" + validator = Application._StateValidator(application=application, state=ApplicationOperatingState.CLOSED) + assert validator(request=[], context={}) # application is closed + application.run() + assert validator(request=[], context={}) is False # application is running - expecting closed + + validator = Application._StateValidator(application=application, state=ApplicationOperatingState.RUNNING) + assert validator(request=[], context={}) # application is running + application.close() + assert validator(request=[], context={}) is False # application is closed - expecting running diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py index 2d9a6c52..537beb8b 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState @@ -92,3 +92,21 @@ def test_service_fix(service): assert service.health_state_actual == SoftwareHealthState.FIXING service.apply_timestep(2) assert service.health_state_actual == SoftwareHealthState.GOOD + + +def test_service_state_validator(service): + """Test the service state validator.""" + validator = Service._StateValidator(service=service, state=ServiceOperatingState.STOPPED) + assert validator(request=[], context={}) # service is stopped + service.start() + assert validator(request=[], context={}) is False # service is running - expecting stopped + + validator = Service._StateValidator(service=service, state=ServiceOperatingState.RUNNING) + assert validator(request=[], context={}) # service is running + service.pause() + assert validator(request=[], context={}) is False # service is paused - expecting running + + validator = Service._StateValidator(service=service, state=ServiceOperatingState.PAUSED) + assert validator(request=[], context={}) # service is paused + service.resume() + assert validator(request=[], context={}) is False # service is running - expecting paused