diff --git a/.clang-tidy b/.clang-tidy index fc8d13fe0c2..f3f76f550c3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -81,6 +81,7 @@ Checks: " cppcoreguidelines-pro-type-cstyle-cast, -cppcoreguidelines-pro-type-member-init, cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-rvalue-reference-param-not-moved, -cppcoreguidelines-special-member-functions, diff --git a/.docker/Dockerfile b/.docker/Dockerfile index bd200abb730..cbd4b4da322 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -47,19 +47,23 @@ RUN vcpkg/vcpkg install # build and test FROM vcpkg-installed AS firestarr-build WORKDIR /appl/firestarr +# HACK: copy .env here since cmake needs to write to it in github workflow +COPY .env ./.env # mount instead of copy to minimize layers RUN --mount=type=bind,source=./cmake,target=./cmake \ --mount=type=bind,source=./CMakeLists.txt,target=./CMakeLists.txt \ --mount=type=bind,source=./CMakePresets.json,target=./CMakePresets.json \ --mount=type=bind,source=./.clang-format,target=./.clang-format \ --mount=type=bind,source=./.clang-tidy,target=./.clang-tidy \ - --mount=type=bind,source=./.env,target=./.env \ --mount=type=bind,source=./.git,target=./.git \ --mount=type=bind,source=./src/cpp,target=./src/cpp \ --mount=type=bind,source=./test/data,target=./test/data \ --mount=type=bind,source=./test/input,target=./test/input \ --mount=type=bind,source=./fuel.lut,target=./fuel.lut \ --mount=type=bind,source=./settings.ini,target=./settings.ini \ + --mount=type=bind,source=./README.md,target=./README.md \ + --mount=type=bind,source=./ORIGIN.md,target=./ORIGIN.md \ + --mount=type=bind,source=./LICENSE,target=./LICENSE \ cmake --preset Release \ && cmake --build --parallel --preset Release \ && ctest --preset Release diff --git a/.dockerignore b/.dockerignore index 6aeb7072139..908b24e508d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,6 @@ !./settings.ini !./test !./vcpkg*.json +!./README.md +!./ORIGIN.md +!./LICENSE diff --git a/.env b/.env index 737fc6a63f6..3f2724f2283 100644 --- a/.env +++ b/.env @@ -1,3 +1 @@ -VERSION=0.9.11 -USERNAME=user -USER_ID=1000 +VERSION=0.9.12 diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 3475d030a28..91aae9c0d85 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -39,7 +39,11 @@ jobs: # arm64 == arm64/v8 platform: [amd64, arm64, ppc64le, riscv64, s390x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + submodules: true - name: Prepare run: | @@ -48,26 +52,26 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ inputs.registry-image }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and push by digest id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: .docker/Dockerfile @@ -87,7 +91,7 @@ jobs: touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: digests-${{ inputs.image }}-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* @@ -101,28 +105,32 @@ jobs: packages: write needs: [ build-docker-image ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + submodules: true - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: ${{ runner.temp }}/digests pattern: digests-${{ inputs.image }}-* merge-multiple: true - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ inputs.registry-image }} flavor: | diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 9d6596d3ce6..30b3a3f93a3 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -2,6 +2,14 @@ name: build-docker on: push: + branches: + - main + - dev + - unstable + paths-ignore: + - '.github/workflows/cmake-test.yml' + - '.github/workflows/docker-cleanup.yml' + workflow_dispatch: jobs: diff --git a/.github/workflows/cmake-test.yml b/.github/workflows/cmake-test.yml index 59072399df5..74c45ff394b 100644 --- a/.github/workflows/cmake-test.yml +++ b/.github/workflows/cmake-test.yml @@ -33,7 +33,7 @@ jobs: # target-os: windows - os: windows target-cpu: x64 - os-version: latest + os-version: 2025-vs2026 target-os: windows - compiler: clang++ target-compiler: clang @@ -46,8 +46,6 @@ jobs: target-cpu: x64 - os: macos compiler: cl - - os: macos - compiler: g++ - os: ubuntu compiler: cl - os: windows @@ -59,18 +57,54 @@ jobs: target-cpu: arm64 env: CXX: ${{ matrix.compiler }} + BUILD_SETUP: "" # HACK: -static makes windows work right now VCPKG_TARGET_TRIPLET: ${{ matrix.target-cpu }}-${{ matrix.target-os }}${{ case ( matrix.os == 'windows', '-static', '' ) }} VCPKG_DISABLE_METRICS: true VCPKG_ROOT: ${{ github.workspace }}/vcpkg - OUTPUT_BINARY: firestarr-${{ matrix.os }}-${{ matrix.target-cpu}}-${{ matrix.target-compiler }}-${{ matrix.variant }} + ARCHIVE_FORMAT: ${{ case( matrix.os == 'windows', '.zip', '.tar.gz' ) }} + RELEASE_ARCHIVE: firestarr-${{ matrix.os }}-${{ matrix.target-cpu}}-${{ matrix.target-compiler }}-${{ matrix.variant }} + BINARY: firestarr${{ case( matrix.os == 'windows', '.exe', '' ) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: + fetch-depth: 0 + fetch-tags: true submodules: true - - uses: ilammy/msvc-dev-cmd@v1 + - name: Install dependencies (macos gcc) + if: ${{ matrix.os == 'macos' && matrix.target-compiler == 'gcc' }} + run: | + brew install -q gcc proj libgeotiff libtiff curl tbb + GCC_VERSION=$(brew list --versions gcc | grep -oE '[0-9]+' | head -1) + echo "GCC_VERSION=$GCC_VERSION" >> $GITHUB_ENV + echo "CC=$(brew --prefix)/bin/gcc-${GCC_VERSION}" >> $GITHUB_ENV + echo "CXX=$(brew --prefix)/bin/g++-${GCC_VERSION}" >> $GITHUB_ENV + echo "BREW_PREFIX=$(brew --prefix)" >> $GITHUB_ENV + echo "TBB_ROOT=$(brew --prefix tbb)" >> $GITHUB_ENV + + + - name: Find build tools (windows) + if: ${{ matrix.os == 'windows' }} + run: | + $VSWHERE = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe" + If(Test-Path "${VSWHERE}") + { + # detect shell to get msvcs + $vsPath = &("${VSWHERE}") -property installationpath + } + If(${vsPath}) + { + echo "Found path ${vsPath}" + } else { + $vsPath = "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools" + echo "Assuming path ${vsPath}" + } + $vars=(cmd /c """${vsPath}\\VC\\Auxiliary\\Build\\vcvarsall.bat"" amd64 > NUL && set") + foreach ($v in $vars) { + Add-Content -Path ${env:GITHUB_ENV} -Value "$v" + } - uses: friendlyanon/setup-vcpkg@v1 with: @@ -82,14 +116,16 @@ jobs: run: | echo "VCPKG_DEFAULT_BINARY_CACHE = $VCPKG_DEFAULT_BINARY_CACHE" - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: # vcpkg always builds release, so share cache between debug and release key: vcpkg-${{ matrix.os }}-${{ matrix.target-cpu}}-${{ matrix.target-compiler }}-${{ hashFiles('vcpkg.json', '.github/vcpkg_overlays/**') }} path: | ${{ env.VCPKG_DEFAULT_BINARY_CACHE }} - - run: cmake --preset ${{ matrix.variant }} + - name: Configure + run: | + cmake --preset ${{ matrix.variant }} - name: Run cmake --build --parallel --preset ${{ matrix.variant }} run: | @@ -98,125 +134,22 @@ jobs: - run: ./firestarr -h - - run: ctest --output-on-failure --preset ${{ matrix.variant }} - - - run: ./firestarr . 2024-06-03 58.81228184403946 -122.9117103995713 01:00 --ffmc 89.9 --dmc 59.5 --dc 450.9 --apcp_prev 0 -v --wx ./test/input/10N_50651/firestarr_10N_50651_wx.csv --output_date_offsets [1] --tz -5 --raster-root ./test/input/10N_50651/ --perim ./test/input/10N_50651/10N_50651.tif - - - name: Locate binary (non-windows) - if: ${{ matrix.variant == 'Release' && matrix.os != 'windows' }} - id: locate + - name: Test run: | - # Find the built executable - BINARY=$(find build -type f -name "firestarr" ${{ case ( matrix.os == 'macos', '-perm +111', '-executable' ) }} | head -1) - if [ -z "$BINARY" ]; then - echo "ERROR: Could not find firestarr binary" - find build -type f ${{ case ( matrix.os == 'macos', '-perm +111', '-executable' ) }} - exit 1 - fi - echo "binary=$BINARY" >> $GITHUB_OUTPUT - echo "Found binary at: $BINARY" + ctest --output-on-failure --preset ${{ matrix.variant }} - - name: Locate binary (windows) - if: ${{ matrix.variant == 'Release' && matrix.os == 'windows' }} - id: locate-windows - run: | - $BINARY = Get-ChildItem -Path build -Recurse -Filter "firestarr.exe" | Select-Object -First 1 -ExpandProperty FullName - if (-not $BINARY) { - echo "ERROR: Could not find firestarr.exe" - Get-ChildItem -Path build -Recurse -Filter "*.exe" | Select-Object FullName - exit 1 - } - echo "binary=$BINARY" >> $env:GITHUB_OUTPUT - echo "Found binary at: $BINARY" - shell: pwsh + - run: ./firestarr dir_test 2024-06-03 58.81228184403946 -122.9117103995713 01:00 --ffmc 89.9 --dmc 59.5 --dc 450.9 --apcp_prev 0 -v --wx ./test/input/10N_50651/firestarr_10N_50651_wx.csv --output_date_offsets [1] --tz -5 --raster-root ./test/input/10N_50651/ --perim ./test/input/10N_50651/10N_50651.tif - - name: Check binary dependencies (non-windows) - if: ${{ matrix.variant == 'Release' && matrix.os != 'windows' }} - run: | - echo "=== Binary info ===" - file ${{ steps.locate.outputs.binary }} - echo "" - echo "=== Dynamic dependencies ===" - ${{ case ( matrix.os == 'macos', 'otool -L', 'ldd' ) }} ${{ steps.locate.outputs.binary }} || true - - - name: Check binary dependencies (windows) - if: ${{ matrix.variant == 'Release' && matrix.os == 'windows' }} - run: | - echo "=== Binary info ===" - Get-Item "${{ steps.locate-windows.outputs.binary }}" | Format-List Name, Length, LastWriteTime - echo "" - echo "=== Binary size ===" - (Get-Item "${{ steps.locate-windows.outputs.binary }}").Length / 1MB | ForEach-Object { "{0:N2} MB" -f $_ } - shell: pwsh + # rename so we can upload without archiving again + - run: | + mv build/firestarr${{ env.ARCHIVE_FORMAT }} build/${{ env.RELEASE_ARCHIVE }}${{ env.ARCHIVE_FORMAT }} - - name: Package artifact (non-windows) - if: ${{ matrix.variant == 'Release' && matrix.os != 'windows' }} - run: | - mkdir -p dist - cp README.md LICENSE ORIGIN.md dist/ - cp ${{ steps.locate.outputs.binary }} dist/firestarr - - # Bundle data files if they exist - [ -f fuel.lut ] && cp fuel.lut dist/ - [ -f proj.db ] && cp proj.db dist/ - [ -f settings.ini ] && cp settings.ini dist/ - [ -f data/fuel.lut ] && cp data/fuel.lut dist/ - [ -f data/settings.ini ] && cp data/settings.ini dist/ - - # Create version info - ./firestarr -h | head -n1 > dist/VERSION - - # List contents - echo "=== Package contents ===" - ls -la dist/ - - # Create tarball - tar -czvf ${{ env.OUTPUT_BINARY }}.tar.gz -C dist . - - - name: Package artifact (windows) - if: ${{ matrix.variant == 'Release' && matrix.os == 'windows' }} - run: | - New-Item -ItemType Directory -Force -Path dist - - # Including these is a requirement - Copy-Item @("README.md", "LICENSE", "ORIGIN.md") dist\ - Copy-Item "${{ steps.locate-windows.outputs.binary }}" dist\firestarr.exe - - # static build so don't need dlls - # copy @((Get-ChildItem -Recurse vcpkg\packages\*.dll).FullName | Select-String ${{ case( matrix.variant == 'Debug', '', '-notMatch' ) }} debug | Select-String bin ) dist\ - - # Bundle data files if they exist - if (Test-Path fuel.lut) { Copy-Item fuel.lut dist\ } - if (Test-Path proj.db) { Copy-Item proj.db dist\ } - if (Test-Path settings.ini) { Copy-Item settings.ini dist\ } - if (Test-Path data\fuel.lut) { Copy-Item data\fuel.lut dist\ } - if (Test-Path data\settings.ini) { Copy-Item data\settings.ini dist\ } - - # Create version info - .\firestarr.exe | select -first 1 | Out-File -FilePath dist/VERSION -Encoding utf8 - - # List contents - echo "=== Package contents ===" - Get-ChildItem dist - - # Create zip - Compress-Archive -Path dist\* -DestinationPath ${{ env.OUTPUT_BINARY }}.zip - shell: pwsh - - - name: Upload artifact (non-windows) - if: ${{ matrix.variant == 'Release' && matrix.os != 'windows' }} - uses: actions/upload-artifact@v4 + - name: Upload artifact + if: ${{ matrix.variant == 'Release' }} + uses: actions/upload-artifact@v7 with: - name: ${{ env.OUTPUT_BINARY }} - path: ${{ env.OUTPUT_BINARY }}.tar.gz - retention-days: 30 - - - name: Upload artifact (windows) - if: ${{ matrix.variant == 'Release' && matrix.os == 'windows' }} - uses: actions/upload-artifact@v4 - with: - name: ${{ env.OUTPUT_BINARY }} - path: ${{ env.OUTPUT_BINARY }}.zip + path: build/${{ env.RELEASE_ARCHIVE }}${{ env.ARCHIVE_FORMAT }} + archive: false retention-days: 30 build-release: @@ -234,8 +167,10 @@ jobs: steps: # need checkout to delete release - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: + fetch-depth: 0 + fetch-tags: true submodules: true - name: Delete existing tag @@ -245,7 +180,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 + with: + skip-decompress: true - name: List artifacts run: | @@ -253,7 +190,7 @@ jobs: find . -type f \( -name "*.tar.gz" -o -name "*.zip" \) - name: Publish Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: name: "FireSTARR ${{ github.ref_name }}" tag_name: ${{ env.TAG }} diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index 6452060183b..561022fe3a3 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -20,8 +20,10 @@ jobs: DEFAULT_PACKAGES: 'firestarr-cpp/cache,firestarr-cpp/firestarr' DEFAULT_DRY_RUN: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: + fetch-depth: 0 + fetch-tags: true submodules: true - uses: dataaxiom/ghcr-cleanup-action@v1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 70093255b77..beb34bab234 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,11 @@ set(FILE_UNSTABLE_CPP ${DIR_SRC_COMMON}/unstable.cpp) set_source_files_properties(${FILE_UNSTABLE_CPP} PROPERTIES COMPILE_FLAGS "${CMAKE_CXX_FLAGS_RELEASE}") if(NOT ASAN_ARGS) + file(GLOB FILES_CPP ${DIR_SRC}/*.cpp) + file(GLOB FILES_H ${DIR_SRC}/*.h) + file(GLOB FILES_CPP_COMMON ${DIR_SRC_COMMON}/*.cpp) + file(GLOB FILES_H_COMMON ${DIR_SRC_COMMON}/*.h) + set(FILES_FORMAT ${FILES_CPP} ${FILES_H} ${FILES_CPP_COMMON} ${FILES_H_COMMON}) include(cmake/TidyFormat.cmake) endif() @@ -189,4 +194,55 @@ message("proj.db path is ${PROJ_DB}") add_custom_command( TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy "${PROJ_DB}" "$/../") + COMMAND ${CMAKE_COMMAND} -E copy "${PROJ_DB}" "${CMAKE_CURRENT_SOURCE_DIR}") + +set(DIR_DIST "${CMAKE_BINARY_DIR}/dist/") + +set(FILES_DIST + "${CMAKE_CURRENT_SOURCE_DIR}/README.md" + "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" + "${CMAKE_CURRENT_SOURCE_DIR}/ORIGIN.md" + "${FILE_VERSION}" + "${CMAKE_CURRENT_SOURCE_DIR}/fuel.lut" + "${PROJ_DB}" + "${CMAKE_CURRENT_SOURCE_DIR}/settings.ini" + "$" +) +make_directory("${DIR_DIST}") +add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy -t "${DIR_DIST}" ${FILES_DIST}) + +set(DIR_LICENSE "${DIR_DIST}/copyright") +make_directory("${DIR_LICENSE}") + +file(GLOB_RECURSE PKG_DIRS "${VCPKG_ROOT}/packages/**/copyright") +foreach(FILE_COPYRIGHT ${PKG_DIRS}) + get_filename_component(LICENSE_LIB_DIR ${FILE_COPYRIGHT} DIRECTORY) + get_filename_component(LICENSE_LIB_NAME ${LICENSE_LIB_DIR} NAME) + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy "${FILE_COPYRIGHT}" "${DIR_LICENSE}/${LICENSE_LIB_NAME}") +endforeach() + +if (WIN32) + set(ARCHIVE_SUFFIX "zip") + set(OUTPUT_ARCHIVE "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.${ARCHIVE_SUFFIX}") + + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + WORKING_DIRECTORY "${DIR_DIST}" + COMMAND ${CMAKE_COMMAND} -E tar -cvf "${OUTPUT_ARCHIVE}" --format=zip .) +else() + set(ARCHIVE_SUFFIX "tar.gz") + set(OUTPUT_ARCHIVE "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.${ARCHIVE_SUFFIX}") + + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + WORKING_DIRECTORY "${DIR_DIST}" + COMMAND ${CMAKE_COMMAND} -E tar -cvfz "${OUTPUT_ARCHIVE}" .) +endif() diff --git a/cmake/TidyFormat.cmake b/cmake/TidyFormat.cmake index 1209fb21e00..02c4f812fec 100644 --- a/cmake/TidyFormat.cmake +++ b/cmake/TidyFormat.cmake @@ -34,11 +34,15 @@ if (LINUX) DEPENDS ${PROJECT_NAME} ) - # HACK: clang-tidy not applying format? - add_custom_target(clang_format ALL - COMMAND ${CLANG_FORMAT} --style=file -i ${FILES_USED} - DEPENDS ${PROJECT_NAME} - ) + if(FILES_FORMAT) + # HACK: clang-tidy not applying format? + add_custom_target(clang_format ALL + COMMAND ${CLANG_FORMAT} --style=file -i ${FILES_FORMAT} + DEPENDS apply_clang_tidy_fixes + ) + else() + message("ERROR: FILES_FORMAT undefined so not calling ${CLANG_FORMAT}") + endif() endif() endif() endif() diff --git a/cmake/TuneArchCPU.cmake b/cmake/TuneArchCPU.cmake index a6ab02dfc61..aaeeb3ab81d 100644 --- a/cmake/TuneArchCPU.cmake +++ b/cmake/TuneArchCPU.cmake @@ -48,8 +48,11 @@ if(HAVE_NO_SVE) set(COMPILER_TUNING -mno-sve ${COMPILER_TUNING}) endif() -CHECK_FLAG(-mno-avx512f HAVE_NO_AVX512F) -if(HAVE_NO_AVX512F) - message("Disabling AVX512 because performance is worse and results are wildly different with it on") - set(COMPILER_TUNING -mno-avx512f ${COMPILER_TUNING}) +# apple clang sets this but doesn't use it +if (NOT APPLE) + CHECK_FLAG(-mno-avx512f HAVE_NO_AVX512F) + if(HAVE_NO_AVX512F) + message("Disabling AVX512 because performance is worse and results are wildly different with it on") + set(COMPILER_TUNING -mno-avx512f ${COMPILER_TUNING}) + endif() endif() diff --git a/cmake/Version.cmake b/cmake/Version.cmake index 511eb24c735..0c063a43e94 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -1,9 +1,21 @@ cmake_minimum_required(VERSION 3.31.6) set(FILE_ENV ${CMAKE_CURRENT_SOURCE_DIR}/.env) +set(FILE_VERSION "${CMAKE_CURRENT_SOURCE_DIR}/VERSION") +set(VERSION_TAG "") -# HACK: look for version in parent folder .env +# always create .env from latest git tag +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") + execute_process(COMMAND git describe --tags --abbrev=0 --match "v*" OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE VERSION_TAG) + set(VERSION ${VERSION_TAG}) + list(TRANSFORM VERSION REPLACE "v(.*)" "\\1") + message("VERSION_TAG=${VERSION}") + # always generate from git tag initially + file(WRITE ${FILE_ENV} "VERSION=${VERSION}\n") +endif() + +# read from config if(EXISTS "${FILE_ENV}") file(STRINGS "${FILE_ENV}" CONFIG REGEX "^[ ]*[A-Za-z0-9_]+[ ]*=") list(TRANSFORM CONFIG STRIP) @@ -11,12 +23,21 @@ if(EXISTS "${FILE_ENV}") message(${CONFIG}) cmake_language(EVAL CODE ${CONFIG}) message("Parsed config") -else() +endif() + +if(NOT VERSION) message(WARNING "VERSION IS NOT SET") # no version set set(VERSION "?.?") +else() + project(${PROJECT_NAME} VERSION ${VERSION}) endif() +message("CMAKE_PROJECT_VERSION=${CMAKE_PROJECT_VERSION}") +message("CMAKE_PROJECT_VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR}") +message("CMAKE_PROJECT_VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR}") +message("CMAKE_PROJECT_VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH}") + set(MODIFIED_TIME "") file(GLOB_RECURSE FILES_USED ${DIR_SRC}/*) @@ -29,18 +50,33 @@ set(FILE_VERSION_CPP ${CMAKE_BINARY_DIR}/version.cpp) set(HASH_PREFIX "") set(HASH_SUFFIX "") set(MODIFIED_TIME "") +set(VERSION_SUFFIX "") if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") if(EXISTS "${FILE_ENV}") - # check when .env last changed - if not this commit then this is version+ - execute_process(COMMAND git log -n1 --pretty=%H ${FILE_ENV} OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE VERSION_HASH) + # check when tag last changed - if not this commit then this is version+ + execute_process(COMMAND git log -n1 --pretty=%H ${VERSION_TAG} OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE VERSION_HASH) endif() execute_process(COMMAND git rev-parse --verify HEAD OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE FULL_HASH) message("VERSION_HASH = ${VERSION_HASH}") message("FULL_HASH = ${FULL_HASH}") if (NOT "${VERSION_HASH}" STREQUAL "${FULL_HASH}") - # add + to version if .env isn't from current commit - set(VERSION "${VERSION}+") + # add +1 to version if .env isn't from current commit + math(EXPR PATCH_INCR "${CMAKE_PROJECT_VERSION_PATCH}+1") + set(VERSION "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${PATCH_INCR}") + message("VERSION=${VERSION}") + project(${PROJECT_NAME} VERSION ${VERSION}) + execute_process(COMMAND git log -n1 --pretty=%H ${VERSION_TAG} OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE VERSION_HASH) + # HACK: # of commits isn't always going to be unique but it's something to start with + execute_process(COMMAND git rev-list ${VERSION_TAG}..HEAD --count OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NUM_SINCE_TAG) + set(VERSION_SUFFIX "-alpha-${NUM_SINCE_TAG}") + # NOTE: docker and cmake don't seem to like suffixes on version endif() + + message("CMAKE_PROJECT_VERSION=${CMAKE_PROJECT_VERSION}") + message("CMAKE_PROJECT_VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR}") + message("CMAKE_PROJECT_VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR}") + message("CMAKE_PROJECT_VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH}") + # is anything in git changed? list(JOIN FILES_USED " " FILES_USED_STRING) execute_process(COMMAND git diff-index HEAD -- ${FILES_USED_STRING} RESULT_VARIABLE GIT_CHANGED) @@ -82,9 +118,12 @@ string(TIMESTAMP COMPILE_TIME "${FMT_TIME}" UTC) set(COMPILED_ON "${CMAKE_SYSTEM_PROCESSOR}-${CMAKE_SYSTEM}-${CMAKE_CXX_COMPILER_ID}") +string(REGEX REPLACE "[^0-9]*([0-9]+)[^0-9]*" "\\1" MODIFIED_NUMERICAL "${MODIFIED_TIME}") + message("FULL_HASH = ${FULL_HASH}") message("HASH = ${HASH}") message("MODIFIED_TIME = ${MODIFIED_TIME}") +message("MODIFIED_NUMERICAL = ${MODIFIED_NUMERICAL}") message("VERSION = ${VERSION}") message("COMPILE_TIME = ${COMPILE_TIME}") message("COMPILED_ON = ${COMPILED_ON}") @@ -93,7 +132,8 @@ if (NOT "" STREQUAL "${HASH}") set(HASH "[${HASH}]") endif() -set (SPECIFIC_REVISION "v${VERSION} ${HASH} <${MODIFIED_TIME}>") +# don't use VERSION_SUFFIX in .env +set (SPECIFIC_REVISION "v${VERSION}${VERSION_SUFFIX} ${HASH} <${MODIFIED_TIME}>") message("${SPECIFIC_REVISION}") set(VERSION_CODE "extern \"C\" const char* const SPECIFIC_REVISION{\"${SPECIFIC_REVISION}\"}\;") list(APPEND VERSION_CODE "extern \"C\" const char* const FULL_HASH{\"${FULL_HASH}\"}\;") @@ -107,4 +147,5 @@ endif() # if matched exiting file don't write if (NOT 0 EQUAL SAME_CODE) file(WRITE ${FILE_VERSION_CPP} "${VERSION_CODE}") + file(WRITE ${FILE_VERSION} "FireSTARR ${SPECIFIC_REVISION}\n") endif() diff --git a/docker-compose.yml b/docker-compose.yml index afa81a420ca..60e2bf7edd1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,6 @@ services: - ./test:/appl/firestarr/test:z - ./grid:/appl/firestarr/grid:z - ./data:/appl/data:z - - ./sims:/appl/data/sims:z env_file: - .env @@ -37,7 +36,6 @@ services: - ./test:/appl/firestarr/test:z - ./grid:/appl/firestarr/grid:z - ./data:/appl/data:z - - ./sims:/appl/data/sims:z env_file: - .env @@ -49,8 +47,6 @@ services: dockerfile: .docker/Dockerfile.dev args: VERSION: ${VERSION} - USERNAME: ${USERNAME} - USER_ID: ${USER_ID} tags: - firestarr-dev:${VERSION} entrypoint: tail -f /dev/null @@ -64,7 +60,6 @@ services: source: ./ target: /appl/firestarr - ./data:/appl/data:z - - ./sims:/appl/data/sims:z restart: always cap_add: - PERFMON @@ -72,3 +67,23 @@ services: - SYS_ADMIN env_file: - .env + + # HACK: so we can call cmake to update version + firestarr-build: + image: firestarr-build + build: + context: . + target: firestarr-build + dockerfile: .docker/Dockerfile + entrypoint: /appl/firestarr/scripts/build.sh + security_opt: + - seccomp:unconfined + volumes: + - /etc/ssl/certs:/etc/ssl/certs + - ~/.ssh:/home/user/.ssh + - ./repo/.bash_aliases:/home/user/.bash_aliases + - type: bind + source: ./ + target: /appl/firestarr + - ./data:/appl/data:z + restart: never diff --git a/src/cpp/firestarr.cpp b/src/cpp/firestarr.cpp index 3e3868bbe9c..36727a51a01 100644 --- a/src/cpp/firestarr.cpp +++ b/src/cpp/firestarr.cpp @@ -108,7 +108,7 @@ int main(const int argc, const char* const argv[]) const auto seconds = difftime(mktime(&year_end), start_t); // start day counts too, so +1 // HACK: but we don't want to go to Jan 1 so don't add 1 - size_t num_days = static_cast(seconds / fs::DAY_SECONDS); + auto num_days = static_cast(seconds / fs::DAY_SECONDS); logging::debug("Calculated number of days until end of year: {:d}", num_days); // +1 because day 1 counts too // +2 so that results don't change when we change number of days diff --git a/src/cpp/fs/ArgumentParser.cpp b/src/cpp/fs/ArgumentParser.cpp index 2e1297f2a7a..0897595929b 100644 --- a/src/cpp/fs/ArgumentParser.cpp +++ b/src/cpp/fs/ArgumentParser.cpp @@ -267,12 +267,13 @@ ArgumentParser::ArgumentParser( ArgumentParser::ArgumentParser( const vector usages, const vector arguments, - const pair binary, + const string binary_directory, + const string binary_name, const PositionalArgumentsRequired require_positional ) : require_positional_{require_positional}, cur_arg_{1}, arguments_{arguments}, // HACK: already parsed binary from arg 0 - binary_directory_{std::get<0>(binary)}, binary_name_{std::get<1>(binary)} + binary_directory_{binary_directory}, binary_name_{binary_name} { // HACK: need output directory so find first thing without a - auto output_directory = [&]() -> string { diff --git a/src/cpp/fs/ArgumentParser.h b/src/cpp/fs/ArgumentParser.h index 79c010dd7ac..3ad1084307c 100644 --- a/src/cpp/fs/ArgumentParser.h +++ b/src/cpp/fs/ArgumentParser.h @@ -33,7 +33,8 @@ class ArgumentParser ArgumentParser( const vector usages, const vector arguments, - const pair binary, + const string binary_directory, + const string binary_name, const PositionalArgumentsRequired require_positional ); ArgumentParser( @@ -60,6 +61,14 @@ class ArgumentParser virtual Settings& parse_args(); protected: + ArgumentParser( + const vector usages, + const vector arguments, + const pair binary, + const PositionalArgumentsRequired require_positional + ) + : ArgumentParser(usages, arguments, binary.first, binary.second, require_positional) + { } /** * \brief If any more positional arguments are available */ diff --git a/src/cpp/fs/BurnedData.cpp b/src/cpp/fs/BurnedData.cpp index d1383bac4e6..c4d100bfe18 100644 --- a/src/cpp/fs/BurnedData.cpp +++ b/src/cpp/fs/BurnedData.cpp @@ -4,13 +4,16 @@ namespace fs { BurnedData::BurnedData(const CellGrid& cells) noexcept - : data_(from_grid(cells, [](const auto& v) { return fuel_by_code(v.fuelCode()); })) + : data_{from_grid(cells, [](const auto& v) { return fuel_by_code(v.fuelCode()); })}, + cell_size_{cells.cellSize()}, unburnable_(data_->count()), height_{cells.height()}, + width_{cells.width()} { } BurnedData::BurnedData(const FuelGrid& fuel) noexcept - : data_{from_grid(fuel, [](const auto& v) { return v; })} + : data_{from_grid(fuel, [](const auto& v) { return v; })}, cell_size_{fuel.cellSize()}, + unburnable_(data_->count()), height_{fuel.height()}, width_{fuel.width()} { } -bool BurnedData::at(const HashSize h) const noexcept { return (*data_)[h]; } -void BurnedData::set(const HashSize h) noexcept { data_->set(h); } +bool BurnedData::at(const XYIdx& xy) const noexcept { return (*data_)[to_index(xy)]; } +void BurnedData::set(const XYIdx& xy) noexcept { data_->set(to_index(xy)); } void BurnedData::clear() noexcept { if (nullptr == data_) @@ -21,17 +24,28 @@ void BurnedData::clear() noexcept { data_->reset(); } + unburnable_ = data_->count(); } BurnedData::BurnedData(const BurnedData& rhs) noexcept - : data_{nullptr == rhs.data_ ? nullptr : make_unique(*rhs.data_)} + : data_{nullptr == rhs.data_ ? nullptr : make_unique(*rhs.data_)}, + cell_size_{rhs.cell_size_}, unburnable_{rhs.unburnable_}, height_{rhs.height_}, + width_{rhs.width_} { } BurnedData::BurnedData(BurnedData&& rhs) noexcept - : data_(nullptr == rhs.data_ ? nullptr : std::move(rhs.data_)) + : data_(nullptr == rhs.data_ ? nullptr : std::move(rhs.data_)), cell_size_{rhs.cell_size_}, + unburnable_{rhs.unburnable_}, height_{rhs.height_}, width_{rhs.width_} { rhs.data_ = nullptr; + rhs.unburnable_ = 0; + rhs.height_ = 0; + rhs.width_ = 0; } BurnedData& BurnedData::operator=(const BurnedData& rhs) noexcept { + cell_size_ = rhs.cell_size_; + unburnable_ = rhs.unburnable_; + height_ = rhs.height_; + width_ = rhs.width_; if (nullptr == rhs.data_) { data_ = nullptr; @@ -46,6 +60,10 @@ BurnedData& BurnedData::operator=(const BurnedData& rhs) noexcept } BurnedData& BurnedData::operator=(BurnedData&& rhs) noexcept { + cell_size_ = rhs.cell_size_; + unburnable_ = rhs.unburnable_; + height_ = rhs.height_; + width_ = rhs.width_; if (nullptr == rhs.data_) { data_ = nullptr; @@ -53,6 +71,9 @@ BurnedData& BurnedData::operator=(BurnedData&& rhs) noexcept } data_ = std::move(rhs.data_); rhs.data_ = nullptr; + rhs.unburnable_ = 0; + rhs.height_ = 0; + rhs.width_ = 0; return *this; } uptr BurnedData::from_grid(auto& grid, auto fct) @@ -60,14 +81,14 @@ uptr BurnedData::from_grid(auto& grid, auto fct) // FIX: change to use grid iterator auto result = make_unique(false); // make a template we can copy to reset things - for (Idx r = 0; r < grid.rows(); ++r) + for (Idx y = 0; y < grid.height(); ++y) { - for (Idx c = 0; c < grid.columns(); ++c) + for (Idx x = 0; x < grid.width(); ++x) { - const Location location(r, c); + const XYIdx xy{x, y}; // HACK: just mark outside edge as unburnable so we never need to check - bool is_outer = 0 == r || 0 == c || (grid.rows() - 1) == r || (grid.columns() - 1) == c; - (*result)[location.hash()] = is_outer || (nullptr == fct(grid.at(location))); + bool is_outer = 0 == y || 0 == x || (grid.height() - 1) == y || (grid.width() - 1) == x; + (*result)[to_index(xy)] = is_outer || (nullptr == fct(grid.at(xy))); } } return result; diff --git a/src/cpp/fs/BurnedData.h b/src/cpp/fs/BurnedData.h index a70e98ef09f..25ecdb5c1b3 100644 --- a/src/cpp/fs/BurnedData.h +++ b/src/cpp/fs/BurnedData.h @@ -3,6 +3,7 @@ #define FS_BURNEDDATA_H #include "stdafx.h" #include "ConstantGrid.h" +#include "Location.h" namespace fs { class BurnedData @@ -10,20 +11,37 @@ class BurnedData public: BurnedData(const CellGrid& cells) noexcept; BurnedData(const FuelGrid& fuel) noexcept; - bool at(const HashSize h) const noexcept; - void set(const HashSize h) noexcept; + bool at(const XYIdx& h) const noexcept; + void set(const XYIdx& h) noexcept; void clear() noexcept; BurnedData() noexcept = default; BurnedData(const BurnedData& rhs) noexcept; BurnedData(BurnedData&& rhs) noexcept; BurnedData& operator=(const BurnedData& rhs) noexcept; BurnedData& operator=(BurnedData&& rhs) noexcept; + [[nodiscard]] constexpr Idx height() const noexcept { return height_; } + [[nodiscard]] constexpr Idx width() const noexcept { return width_; } + /** + * \brief Calculate area for cells that have a value (ha) + * \return Area for cells that have a value (ha) + */ + [[nodiscard]] MathSize fireSize() const noexcept + { + // we know that every cell is a key, so we convert that to hectares + const MathSize per_width = (cell_size_ / 100.0); + // size of fire is number of bits set * cell size + return static_cast(data_->count() - unburnable_) * per_width * per_width; + } private: // std::bitset doesn't heap allocate - using dtype = std::bitset(MAX_ROWS) * MAX_COLUMNS>; + using dtype = std::bitset(MAX_HEIGHT) * MAX_WIDTH>; uptr data_{nullptr}; static uptr from_grid(auto& grid, auto fct); + MathSize cell_size_{-1}; + size_t unburnable_{0}; + Idx height_{}; + Idx width_{}; }; } #endif diff --git a/src/cpp/fs/CMakeLists.txt b/src/cpp/fs/CMakeLists.txt index df98270841e..0dec612e196 100644 --- a/src/cpp/fs/CMakeLists.txt +++ b/src/cpp/fs/CMakeLists.txt @@ -3,9 +3,10 @@ cmake_minimum_required(VERSION 3.13) set(DIR_SRC .) +set(LIB_NAME fs) file(GLOB FILES_CPP ${DIR_SRC}/*.cpp) set(FILE_UNSTABLE_CPP ${DIR_SRC}/unstable.cpp) -add_library(fs ${FILES_CPP}) +add_library(${LIB_NAME} ${FILES_CPP}) if(NOT WIN32) set_source_files_properties(${FILE_UNSTABLE_CPP} PROPERTIES COMPILE_FLAGS "${CMAKE_CXX_FLAGS_RELEASE}") @@ -17,9 +18,9 @@ find_package(PROJ CONFIG REQUIRED) include_directories(${DIR_SRC}) include_directories(${GEOTIFF_INCLUDE_DIR}) -target_link_libraries(fs PRIVATE ${GEOTIFF_LIBRARIES}) -target_link_libraries(fs PRIVATE TIFF::TIFF) -target_link_libraries(fs PRIVATE PROJ::proj) +target_link_libraries(${LIB_NAME} PRIVATE ${GEOTIFF_LIBRARIES}) +target_link_libraries(${LIB_NAME} PRIVATE TIFF::TIFF) +target_link_libraries(${LIB_NAME} PRIVATE PROJ::proj) find_package(GeoTIFF CONFIG REQUIRED) find_package(TIFF REQUIRED) @@ -32,5 +33,9 @@ endif() find_package(PROJ CONFIG REQUIRED) # link_libraries(geotiff tiff PROJ::proj) if(LINUX) - target_link_libraries(fs PRIVATE SQLite::SQLite3) + target_link_libraries(${LIB_NAME} PRIVATE SQLite::SQLite3) +endif() +if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + find_library(TBB_LIB tbb REQUIRED) + target_link_libraries(${LIB_NAME} PRIVATE ${TBB_LIB}) endif() diff --git a/src/cpp/fs/Cell.h b/src/cpp/fs/Cell.h index b28baeac66d..1147add0ff8 100644 --- a/src/cpp/fs/Cell.h +++ b/src/cpp/fs/Cell.h @@ -2,7 +2,6 @@ #ifndef FS_CELL_H #define FS_CELL_H #include "stdafx.h" -#include "Location.h" #include "Util.h" namespace fs { @@ -10,32 +9,33 @@ using SpreadKey = uint32_t; /** * \brief A Position with a Slope, Aspect, and Fuel. */ -class Cell : public Position +class Cell { +protected: + /** + * \brief Stored hash that contains x and y data + */ + SpreadKey data_; + public: - auto operator<=>(const Cell& rhs) const { return hash() <=> rhs.hash(); } - constexpr Cell() noexcept - : Cell( - -1, - -1, - numeric_limits::min(), - numeric_limits::min(), - numeric_limits::min() - ) - { } /** * \brief Full stored hash that may contain data from subclasses * \return Full stored hash that may contain data from subclasses */ - [[nodiscard]] constexpr Topo fullHash() const { return topo_data_; } + [[nodiscard]] constexpr SpreadKey fullHash() const { return data_; } /** - * \brief Hash attributes into a Topo value + * \brief Construct from hash value + * \param hash Hash defining all attributes + */ + explicit constexpr Cell(const SpreadKey hash) noexcept : data_{hash} { } + /** + * \brief Hash attributes into a SpreadKey value * \param slope Slope * \param aspect Aspect * \param fuel Fuel * \return Hash */ - [[nodiscard]] static constexpr Topo hashCell( + [[nodiscard]] static constexpr SpreadKey hashCell( const SlopeSize slope, const AspectSize aspect, const FuelCodeSize& fuel @@ -44,8 +44,8 @@ class Cell : public Position // HACK: so we can call and set it all invalid if anything is // if any are invalid then they all should be const auto do_hash_cell = [](const SlopeSize s, const AspectSize a, const FuelCodeSize f) { - return static_cast(f) << FuelShift | static_cast(s) << SlopeShift - | static_cast(a) << AspectShift; + return static_cast(f) << FuelShift | static_cast(s) << SlopeShift + | static_cast(a) << AspectShift; }; if (INVALID_SLOPE == slope || INVALID_ASPECT == aspect || INVALID_FUEL_CODE == fuel) { @@ -54,49 +54,29 @@ class Cell : public Position // if slope is 0 make aspect north so less unique keys return do_hash_cell(slope, 0 == slope ? 0 : aspect, fuel); } - /** - * \brief Construct from hash value - * \param hash Hash defining all attributes - */ - explicit constexpr Cell(const Topo hash) noexcept : Position(hash) { } - /** - * \brief Construct based on given attributes - * \param hash Hash of row and column - * \param slope Slope - * \param aspect Aspect - * \param fuel Fuel - */ - constexpr Cell( - const HashSize hash_value, - const SlopeSize slope, - const AspectSize aspect, - const FuelCodeSize& fuel - ) noexcept - : Position((hash_value & HashMask) | hashCell(slope, aspect, fuel)) + constexpr Cell() noexcept + : Cell( + numeric_limits::min(), + numeric_limits::min(), + numeric_limits::min() + ) { } /** - * \brief Constructor - * \param row Row - * \param column Column + * \brief Construct based on given attributes + * \param hash Hash of x and y * \param slope Slope * \param aspect Aspect * \param fuel Fuel */ - constexpr Cell( - const Idx row, - const Idx column, - const SlopeSize slope, - const AspectSize aspect, - const FuelCodeSize& fuel - ) noexcept - : Position(static_cast(doHash(row, column)) | hashCell(slope, aspect, fuel)) + constexpr Cell(const SlopeSize slope, const AspectSize aspect, const FuelCodeSize& fuel) noexcept + : Cell(hashCell(slope, aspect, fuel)) { } /** * \brief A key defining Slope, Aspect, and Fuel, used for determining Cells that spread the same - * \param value Topo to extract from + * \param value SpreadKey to extract from * \return A key defining Slope, Aspect, and Fuel */ - [[nodiscard]] static constexpr SpreadKey key(const Topo value) noexcept + [[nodiscard]] static constexpr SpreadKey key(const SpreadKey value) noexcept { // can just shift since these are the only bits left after return static_cast(value >> FuelShift); @@ -108,9 +88,7 @@ class Cell : public Position */ [[nodiscard]] static constexpr AspectSize aspect(const SpreadKey value) noexcept { - return static_cast( - (value & (AspectMask >> FuelShift)) >> (AspectShift - FuelShift) - ); + return static_cast((value & AspectMask) >> AspectShift); } /** * \brief Fuel @@ -119,7 +97,7 @@ class Cell : public Position */ [[nodiscard]] static constexpr FuelCodeSize fuelCode(const SpreadKey value) noexcept { - return static_cast((value & (FuelMask >> FuelShift)) >> (FuelShift - FuelShift)); + return static_cast((value & FuelMask) >> FuelShift); } /** * \brief Slope (degrees) @@ -127,86 +105,42 @@ class Cell : public Position * \return Slope (degrees) */ [[nodiscard]] static constexpr SlopeSize slope(const SpreadKey value) noexcept - { - return static_cast((value & (SlopeMask >> FuelShift)) >> (SlopeShift - FuelShift)); - } - /** - * \brief Aspect (degrees) - * \param value Topo to extract from - * \return Aspect (degrees) - */ - [[nodiscard]] static constexpr AspectSize aspect(const Topo value) noexcept - { - return static_cast((value & AspectMask) >> AspectShift); - } - /** - * \brief Fuel - * \param value Topo to extract from - * \return Fuel - */ - [[nodiscard]] static constexpr FuelCodeSize fuelCode(const Topo value) noexcept - { - return static_cast((value & FuelMask) >> FuelShift); - } - /** - * \brief Slope (degrees) - * \param value Topo to extract from - * \return Slope (degrees) - */ - [[nodiscard]] static constexpr SlopeSize slope(const Topo value) noexcept { return static_cast((value & SlopeMask) >> SlopeShift); } - /** - * \brief Topo that contains Cell data - * \param value Topo to extract from - * \return Topo that contains Cell data - */ - [[nodiscard]] static constexpr Topo topoHash(const Topo value) noexcept - { - return static_cast(value) & CellMask; - } /** * \brief A key defining Slope, Aspect, and Fuel, used for determining Cells that spread the same * \return A key defining Slope, Aspect, and Fuel */ - [[nodiscard]] constexpr SpreadKey key() const noexcept { return Cell::key(topo_data_); } + [[nodiscard]] constexpr SpreadKey key() const noexcept { return Cell::key(data_); } /** * \brief Aspect (degrees) * \return Aspect (degrees) */ - [[nodiscard]] constexpr AspectSize aspect() const noexcept { return Cell::aspect(topo_data_); } + [[nodiscard]] constexpr AspectSize aspect() const noexcept { return Cell::aspect(data_); } /** * \brief Fuel * \return Fuel */ - [[nodiscard]] constexpr FuelCodeSize fuelCode() const noexcept - { - return Cell::fuelCode(topo_data_); - } + [[nodiscard]] constexpr FuelCodeSize fuelCode() const noexcept { return Cell::fuelCode(data_); } /** * \brief Slope (degrees) * \return Slope (degrees) */ - [[nodiscard]] constexpr SlopeSize slope() const noexcept { return Cell::slope(topo_data_); } - /** - * \brief Topo that contains Cell data - * \return Topo that contains Cell data - */ - [[nodiscard]] constexpr Topo topoHash() const noexcept { return Cell::topoHash(topo_data_); } + [[nodiscard]] constexpr SlopeSize slope() const noexcept { return Cell::slope(data_); } protected: /* * Field Natural Range Used Range Bits Bit Range - * Row 0 - 4095 0 - 4095 12 0 - 4095 - * Column 0 - 4095 0 - 4095 12 0 - 4095 + * Y 0 - 4095 0 - 4095 12 0 - 4095 + * X 0 - 4095 0 - 4095 12 0 - 4095 * PADDING 10 * Fuel 0 - 140 0 - 140 8 0 - 255 * Aspect 0 - 359 0 - 359 9 0 - 511 * Slope 0 - infinity 0 - 511 9 0 - 511 * Extra 8 * - * Rows and Columns are restricted to 4096 since that's what gets clipped out of + * X and Y are restricted to 4096 since that's what gets clipped out of * the GIS outputs. * * Fuel is tied to how many variations of percent conifer/dead fir we want to use, and @@ -223,23 +157,23 @@ class Cell : public Position /** * \brief Shift for fuel bitmask */ - static constexpr uint32_t FuelShift = 32; - // Need to make sure that fuel, slope & aspect aren't in first 32 bits - static_assert(32 <= FuelShift); + static constexpr uint32_t FuelShift = 0; + // // Need to make sure that fuel, slope & aspect aren't in first 32 bits + // static_assert(32 <= FuelShift); /** * \brief Number of bits in fuel bitmask */ static constexpr uint32_t FuelBits = std::bit_width(NUMBER_OF_FUELS); /** - * \brief Bitmask for fuel information in Topo before shift + * \brief Bitmask for fuel information in SpreadKey before shift */ - static constexpr Topo FuelBitMask = bit_mask(); + static constexpr SpreadKey FuelBitMask = bit_mask(); static_assert(FuelBitMask == 0xFF); static_assert(FuelBitMask >= NUMBER_OF_FUELS); /** - * \brief Bitmask for fuel information in Topo + * \brief Bitmask for fuel information in SpreadKey */ - static constexpr Topo FuelMask = FuelBitMask << FuelShift; + static constexpr SpreadKey FuelMask = FuelBitMask << FuelShift; /** * \brief Shift for aspect bitmask */ @@ -249,15 +183,15 @@ class Cell : public Position */ static constexpr uint32_t AspectBits = std::bit_width(MAX_ASPECT); /** - * \brief Bitmask for aspect in Topo before shift + * \brief Bitmask for aspect in SpreadKey before shift */ - static constexpr Topo AspectBitMask = bit_mask(); + static constexpr SpreadKey AspectBitMask = bit_mask(); static_assert(AspectBitMask == 0x1FF); static_assert(AspectBitMask >= INVALID_ASPECT); /** - * \brief Bitmask for aspect in Topo + * \brief Bitmask for aspect in SpreadKey */ - static constexpr Topo AspectMask = AspectBitMask << AspectShift; + static constexpr SpreadKey AspectMask = AspectBitMask << AspectShift; /** * \brief Shift for slope bitmask */ @@ -268,48 +202,19 @@ class Cell : public Position static constexpr uint32_t SlopeBits = std::bit_width(MAX_SLOPE_FOR_DISTANCE); static_assert(SlopeBits == 9); /** - * \brief Bitmask for slope in Topo before shift + * \brief Bitmask for slope in SpreadKey before shift */ - static constexpr Topo SlopeBitMask = bit_mask(); + static constexpr SpreadKey SlopeBitMask = bit_mask(); static_assert(SlopeBitMask == 0x1FF); static_assert(SlopeBitMask >= INVALID_SLOPE); /** - * \brief Bitmask for slope in Topo - */ - static constexpr Topo SlopeMask = SlopeBitMask << SlopeShift; - /** - * \brief Bitmask for Cell information in Topo + * \brief Bitmask for slope in SpreadKey */ - static constexpr Topo CellMask = HashMask | FuelMask | AspectMask | SlopeMask; + static constexpr SpreadKey SlopeMask = SlopeBitMask << SlopeShift; static_assert( - static_cast(std::bit_width(std::numeric_limits::max())) >= SlopeBits + SlopeShift + static_cast(std::bit_width(std::numeric_limits::max())) + >= SlopeBits + SlopeShift ); }; -/** - * \brief Less than operator - * \param lhs First Cell - * \param rhs Second Cell - * \return Whether or not first is less than second - */ -constexpr bool operator<(const Cell& lhs, const Cell& rhs) -{ - return lhs.topoHash() < rhs.topoHash(); -} -} -namespace std -{ -/** - * \brief Provides hashing of Cell objects - */ -template <> -struct hash -{ - /** - * \brief Provides hash for Cell objects - * \param k Cell to get hash for - * \return Hash value - */ - std::size_t operator()(const fs::Cell& k) const noexcept { return k.fullHash(); } -}; } #endif diff --git a/src/cpp/fs/CellPoints.cpp b/src/cpp/fs/CellPoints.cpp index fa91ffed881..0472605b4f6 100644 --- a/src/cpp/fs/CellPoints.cpp +++ b/src/cpp/fs/CellPoints.cpp @@ -1,385 +1,33 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ #include "CellPoints.h" -#include "Location.h" -#include "Log.h" namespace fs { -constexpr InnerSize DIST_22_5 = - static_cast(0.2071067811865475244008443621048490392848359376884740365883398689); -constexpr InnerSize P_0_5 = static_cast(0.5) + DIST_22_5; -constexpr InnerSize M_0_5 = static_cast(0.5) - DIST_22_5; -// not sure what's going on with this and wondering if it doesn't keep number exactly -// shouldn't be any way to be further than twice the entire width of the area -static const auto INVALID_DISTANCE = static_cast(MAX_ROWS * MAX_ROWS); -static const XYPos INVALID_XY_POSITION{}; -static const pair INVALID_XY_PAIR{INVALID_DISTANCE, {}}; -static const XYSize INVALID_XY_LOCATION = INVALID_XY_PAIR.second.first; -static const InnerPos INVALID_INNER_POSITION{}; -// static const pair INVALID_INNER_PAIR{INVALID_DISTANCE, {}}; -// static const InnerSize INVALID_INNER_LOCATION = INVALID_INNER_PAIR.second.first; -static const SpreadData INVALID_SPREAD_DATA{ - INVALID_TIME, - static_cast(0), - INVALID_ROS, - Direction::Invalid(), - Direction::Invalid() -}; -set CellPoints::unique() const noexcept -{ - // // if any point is invalid then they all have to be - if (INVALID_DISTANCE == pts_.distances()[0]) - { - return {}; - } - else - { - const auto& pts_all = std::views::transform(pts_.points(), [this](const auto& p) { - return XYPos(p.first + cell_x_y_.first, p.second + cell_x_y_.second); - }); - return {pts_all.begin(), pts_all.end()}; - } -} -#ifdef DEBUG_CELLPOINTS -size_t CellPoints::size() const noexcept { return unique().size(); } -#endif -CellPoints::CellPoints(const Idx cell_x, const Idx cell_y) noexcept - : spread_arrival_(INVALID_SPREAD_DATA), spread_internal_(INVALID_SPREAD_DATA), - spread_exit_(INVALID_SPREAD_DATA), pts_({}), cell_x_y_(cell_x, cell_y), src_(DIRECTION_NONE) -{ - std::fill(pts_.distances().begin(), pts_.distances().end(), INVALID_DISTANCE); - std::fill(pts_.points().begin(), pts_.points().end(), INVALID_INNER_POSITION); - std::fill(pts_.directions().begin(), pts_.directions().end(), INVALID_DIRECTION.value); -#ifdef DEBUG_CELLPOINTS - logging::note("CellPoints is size {:d} after creation and should be empty", size()); -#endif -} -CellPoints::CellPoints() noexcept - : CellPoints(static_cast(INVALID_XY_LOCATION), static_cast(INVALID_XY_LOCATION)) -{ } -CellPoints::CellPoints(const CellPoints* rhs) noexcept : CellPoints() -{ - logging::check_fatal(nullptr == rhs, "Initializing CellPoints from nullptr"); - *this = *rhs; -} -CellPoints::CellPoints( - const XYPos& src, - const SpreadData& spread_current, - const XYSize x, - const XYSize y -) noexcept - : CellPoints(static_cast(x), static_cast(y)) -{ - insert(src, spread_current, x, y); -} -using DISTANCE_PAIR = pair; -#define D_PTS(x, y) (DISTANCE_PAIR{static_cast(x), static_cast(y)}) -constexpr std::array POINTS_OUTER{ - D_PTS(0.5, 1.0), - // north-northeast is closest to point (0.5 + 0.207, 1.0) - D_PTS(P_0_5, 1.0), - // northeast is closest to point (1.0, 1.0) - D_PTS(1.0, 1.0), - // east-northeast is closest to point (1.0, 0.5 + 0.207) - D_PTS(1.0, P_0_5), - // east is closest to point (1.0, 0.5) - D_PTS(1.0, 0.5), - // east-southeast is closest to point (1.0, 0.5 - 0.207) - D_PTS(1.0, M_0_5), - // southeast is closest to point (1.0, 0.0) - D_PTS(1.0, 0.0), - // south-southeast is closest to point (0.5 + 0.207, 0.0) - D_PTS(P_0_5, 0.0), - // south is closest to point (0.5, 0.0) - D_PTS(0.5, 0.0), - // south-southwest is closest to point (0.5 - 0.207, 0.0) - D_PTS(M_0_5, 0.0), - // southwest is closest to point (0.0, 0.0) - D_PTS(0.0, 0.0), - // west-southwest is closest to point (0.0, 0.5 - 0.207) - D_PTS(0.0, M_0_5), - // west is closest to point (0.0, 0.5) - D_PTS(0.0, 0.5), - // west-northwest is closest to point (0.0, 0.5 + 0.207) - D_PTS(0.0, P_0_5), - // northwest is closest to point (0.0, 1.0) - D_PTS(0.0, 1.0), - // north-northwest is closest to point (0.5 - 0.207, 1.0) - D_PTS(M_0_5, 1.0) -}; -// TODO: add angle -CellPoints& CellPoints::insert( - const XYPos& src, +void CellPoints::insert_basic( + CellPoints& cell_pts, + const XYPos&, const SpreadData& spread_current, - const XYSize x, - const XYSize y + const XYPos& xy ) noexcept { -#ifdef DEBUG_CELLPOINTS - logging::note( - "Insert ({:f}, {:f}) at time {:f} with ROS {:f}, Intensity {:d}, RAZ {:f}", - x, - y, - arrival_time, - ros, - intensity, - raz.asDegrees() - ); -#endif - // count things as the same time if within a tolerance - constexpr auto TIME_EPSILON_SECONDS = 1.0 * MINUTE_SECONDS; - constexpr auto TIME_EPSILON = TIME_EPSILON_SECONDS / DAY_SECONDS; - if (0 < spread_current.time() && 0 > spread_arrival_.time()) + auto& spread_arrival = cell_pts.spread_arrival_; + if (0 < spread_current.time && 0 > spread_arrival.time) { - logging::extensive( - "No time so setting ros to {:f} at time {:f}", spread_current.ros(), spread_current.time() - ); // record ros and time if nothing yet - spread_arrival_ = spread_current; + spread_arrival = spread_current; } - else - { - // initial burn will have an invalid direction, so needs to burn everywhere - const auto is_initial = Direction::Invalid() == spread_current.direction_previous(); - // only spread in a direction that's in front of the normal to the angle it came from - // i.e. the 90 degrees on either side of the raz - const auto dir_diff = - abs(spread_current.direction().asDegrees() - spread_current.direction_previous().asDegrees()); - const auto MAX_DEGREES = 90.0; - // NOTE: there should be no change in the extent of the fire if we exclude things behind the - // normal to the direction it came from - // - but if we exclude too much then it can change how things spread, even if it is a more - // representative angle for the grids - if (is_initial || MAX_DEGREES >= dir_diff) - { - if (abs(spread_current.time() - spread_arrival_.time()) <= TIME_EPSILON) - // else if (arrival_time == arrival_time_) - { - logging::verbose( - "Same time so setting ros to max({:f}, {:f}) at time {:f}", - spread_current.ros(), - spread_arrival_.ros(), - spread_current.time() - ); - // the same time so pick higher ros - if ( - (spread_arrival_.ros() < spread_current.ros()) - || (spread_arrival_.ros() == spread_current.ros() - && spread_current.intensity() > spread_arrival_.intensity())) - { - // NOTE: keep track of original time so this doesn't just always happen - spread_arrival_ = SpreadData( - spread_arrival_.time(), - spread_current.intensity(), - spread_current.ros(), - spread_current.direction(), - spread_current.direction_previous() - ); - } - } - } - } - // NOTE: use location inside cell so smaller types can be more precise - // since digits aren't wasted on cell - const auto p0 = InnerPos( - static_cast(x - cell_x_y_.first), static_cast(y - cell_x_y_.second) - ); - const auto x0 = static_cast(p0.first); - const auto y0 = static_cast(p0.second); + const auto& cell_x_y = cell_pts.cell_x_y_; + const DistanceSize x0{static_cast(xy.x.value - cell_x_y.x.value)}; + const DistanceSize y0{static_cast(xy.y.value - cell_x_y.y.value)}; // CHECK: FIX: is this initializing everything to false or just one element? - std::array closer{}; - std::fill_n(closer.begin(), NUM_DIRECTIONS, false); - for (size_t i = 0; i < NUM_DIRECTIONS; ++i) + for (size_t i = 0; i < cell_pts.distances.size(); ++i) { - const auto& p1 = POINTS_OUTER[i]; - const auto& x1 = p1.first; - const auto& y1 = p1.second; - const auto d = ((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1)); - auto& p_d = pts_.distances()[i]; - auto& p_p = pts_.points()[i]; - auto& p_a = pts_.directions()[i]; - closer[i] = (d < p_d); - p_p = (d < p_d) ? p0 : p_p; + // calculate with inner but keep XYPos + const auto d = distance(x0, y0, POINTS_OUTER[i].first, POINTS_OUTER[i].second); + auto& p_d = cell_pts.distances[i]; + auto& p_p = cell_pts.points[i]; + p_p = (d < p_d) ? xy : p_p; p_d = (d < p_d) ? d : p_d; - p_a = (d < p_d) ? spread_current.direction().asDegrees() : p_a; - } -#ifdef DEBUG_CELLPOINTS - logging::note("now have {:d} points", size()); -#endif - const Location& dst = location(); - // adds 0 if the same so try without checking - { - // we inserted a pair of (src, dst), which means we've never - // calculated the relativeIndex for this so add it to main map - add_source(relativeIndex(src.location(), dst)); - } - if (src.location() == dst) - { - // if we spread from this cell to this cell again then ros could be considered for max - // need to make sure we're not spreading back towards where we came from because that doesn't - // matter HACK: for now look at source for this cell and exclude points in those directions - const auto srcs = sources(); - for (size_t i = 0; i < NUM_DIRECTIONS; ++i) - { - const auto mask = DIRECTION_MASKS[i]; - if (mask != (srcs & mask)) - { - // at least one of the cells in this direction is not a source, so consider them - if (closer[i]) - { - // point was closer to edge than what was there - if (spread_current.ros() >= spread_internal_.ros()) - { - // since we spread within cell then set internal spread - spread_internal_ = spread_current; - } - } - } - } - } - // FIX: do something with spread on exit - return *this; -} -#undef D_PTS -CellPoints::CellPoints(const XYPos& p) noexcept - : CellPoints(static_cast(p.first), static_cast(p.second)) -{ } -CellPoints& CellPoints::insert(const InnerPos& p) noexcept -{ - // HACK: FIX: just do something for now - insert(INVALID_XY_POSITION, INVALID_SPREAD_DATA, p.first, p.second); - return *this; -} -void CellPoints::add_source(const CellIndex src) { src_ |= src; } -CellPoints& CellPoints::merge(const CellPoints& rhs) -{ -#ifdef DEBUG_CELLPOINTS - const auto n0 = size(); - const auto n1 = rhs.size(); -#endif - // either both invalid or lower one is valid - cell_x_y_ = min(cell_x_y_, rhs.cell_x_y_); - auto& d0 = pts_.distances(); - auto& d1 = rhs.pts_.distances(); - auto& p0 = pts_.points(); - auto& p1 = rhs.pts_.points(); - // we know distances in each direction so just pick closer - for (size_t i = 0; i < d0.size(); ++i) - { - if (d1[i] < d0[i]) - { - d0[i] = d1[i]; - p0[i] = p1[i]; - } - } - add_source(rhs.src_); -#ifdef DEBUG_CELLPOINTS - logging::note("Merging {:d} with {:d} gives {:d} pts", n0, n1, size()); -#endif - return *this; -} -bool CellPoints::operator<(const CellPoints& rhs) const noexcept -{ - if (cell_x_y_ == rhs.cell_x_y_) - { - return pts_.points() < rhs.pts_.points(); - } - return cell_x_y_ < rhs.cell_x_y_; -} -bool CellPoints::operator==(const CellPoints& rhs) const noexcept -{ - return (cell_x_y_ == rhs.cell_x_y_ && pts_.points() == rhs.pts_.points()); -} -bool CellPoints::empty() const -{ - // NOTE: if anything is invalid then everything must be - return (INVALID_DISTANCE == pts_.distances()[0]); -} -[[nodiscard]] Location CellPoints::location() const noexcept -{ - return Location{cell_x_y_.second, cell_x_y_.first}; -} -CellPointsMap::CellPointsMap() : map_({}) { } -CellPoints& CellPointsMap::insert( - const XYPos& src, - const SpreadData& spread_current, - const XYSize x, - const XYSize y -) noexcept -{ -#ifdef DEBUG_CELLPOINTS - const auto n0 = size(); -#endif - const Location location{static_cast(y), static_cast(x)}; - auto e = map_.try_emplace(location, src, spread_current, x, y); - CellPoints& cell_pts = e.first->second; - if (!e.second) - { - // FIX: should use max of whatever ROS has entered during this time and not just first ros - // tried to add new CellPoints but already there - cell_pts.insert(src, spread_current, x, y); - } -#ifdef DEBUG_CELLPOINTS - logging::note( - "insert with size {:d} of ({:f}, {:f}) at time {:f} with ROS {:f} gives size {:d}", - n0, - x, - y, - arrival_time, - ros, - size() - ); -#endif - return cell_pts; -} -CellPointsMap& CellPointsMap::merge(const BurnedData& unburnable, const CellPointsMap& rhs) noexcept -{ - // FIX: if we iterate through both they should be sorted - for (const auto& kv : rhs.map_) - { - const auto h = kv.first.hash(); - if (!unburnable.at(h)) - { - const CellPoints& pts = kv.second; - const Location location = pts.location(); - auto e = map_.try_emplace(location, pts); - CellPoints& cell_pts = e.first->second; - if (!e.second) - { - // couldn't insert - cell_pts.merge(pts); - } - } - } - return *this; -} -void CellPointsMap::remove_if(std::function&)> F) noexcept -{ - auto it = map_.begin(); - while (map_.end() != it) - { - if (F(*it)) - { - it = map_.erase(it); - } - else - { - ++it; - } - } -} -set CellPointsMap::unique() const noexcept -{ - set r{}; - for (auto& lp : map_) - { - for (auto& p : lp.second.unique()) - { - r.insert(p); - } + // don't calculate FI/ROS/RAZ } - return r; } -#ifdef DEBUG_CELLPOINTS -size_t CellPointsMap::size() const noexcept { return unique().size(); } -#endif } diff --git a/src/cpp/fs/CellPoints.h b/src/cpp/fs/CellPoints.h index 54da6d4b8bb..6812a01e9ac 100644 --- a/src/cpp/fs/CellPoints.h +++ b/src/cpp/fs/CellPoints.h @@ -2,22 +2,26 @@ #ifndef FS_CELLPOINTS_H #define FS_CELLPOINTS_H #include "stdafx.h" +#include +#include #include "BurnedData.h" #include "Cell.h" -#include "InnerPos.h" namespace fs { using fs::Direction; -class SpreadData : std::tuple +// Type used for storing distances within cells +using DistanceSize = double; +struct SpreadData { -public: - using std::tuple::tuple; - DurationSize time() const { return std::get<0>(*this); } - IntensitySize intensity() const { return std::get<1>(*this); } - ROSSize ros() const { return std::get<2>(*this); } - Direction direction() const { return std::get<3>(*this); } - Direction direction_previous() const { return std::get<4>(*this); } + DurationSize time{INVALID_TIME}; + IntensitySize intensity{static_cast(0)}; + ROSSize ros{INVALID_ROS}; + Direction direction{Direction::Invalid()}; + Direction direction_previous{Direction::Invalid()}; }; +// not sure what's going on with this and wondering if it doesn't keep number exactly +// shouldn't be any way to be further than twice the entire width of the area +static const auto INVALID_DISTANCE = static_cast(MAX_HEIGHT * MAX_HEIGHT); static constexpr size_t FURTHEST_N = 0; static constexpr size_t FURTHEST_NNE = 1; static constexpr size_t FURTHEST_NE = 2; @@ -58,98 +62,477 @@ static constexpr std::array DIRECTION_MASKS{ MASK_NW, MASK_NW }; -class CellPointsMap; using array_dists = std::array; -using array_pts = std::array; +using array_pts = std::array; using array_dirs = std::array; -using array_cellpts = std::tuple; -class CellPointArrays : public array_cellpts -{ -public: - using array_cellpts::array_cellpts; - inline const array_dists& distances() const { return std::get<0>(*this); } - inline const array_pts& points() const { return std::get<1>(*this); } - inline const array_dirs& directions() const { return std::get<2>(*this); } - inline array_dists& distances() { return std::get<0>(*this); } - inline array_pts& points() { return std::get<1>(*this); } - inline array_dirs& directions() { return std::get<2>(*this); } -}; /** * Points in a cell furthest in each direction */ class CellPoints { -public: - using spreading_points = map>>; - CellPoints() noexcept; - // HACK: so we can emplace with nullptr - CellPoints(const CellPoints* rhs) noexcept; - CellPoints( +private: + static bool should_save() noexcept + { + // HACK: resolve once and fail if not set already + static auto& settings = fs::settings::instance(); + static auto r = settings.save_individual || settings.save_intensity; + return r; + } + static inline constexpr DistanceSize distance( + const DistanceSize x0, + const DistanceSize y0, + const DistanceSize x1, + const DistanceSize y1 + ) noexcept + { + const auto x2 = x0 - x1; + const auto y2 = y0 - y1; + return x2 * x2 + y2 * y2; + } + static inline constexpr DistanceSize distance( + const XYIdx& cell_x_y, + const XYPos& xy, + const DistanceSize x1, + const DistanceSize y1 + ) noexcept + { + const DistanceSize x0{static_cast(xy.x.value - cell_x_y.x.value)}; + const DistanceSize y0{static_cast(xy.y.value - cell_x_y.y.value)}; + const auto x2 = x0 - x1; + const auto y2 = y0 - y1; + return x2 * x2 + y2 * y2; + } + static inline constexpr DistanceSize distance( + const XYIdx& cell_x_y, + const XYPos& xy, + const size_t i + ) noexcept + { + const DistanceSize x0{static_cast(xy.x.value - cell_x_y.x.value)}; + const DistanceSize y0{static_cast(xy.y.value - cell_x_y.y.value)}; + const auto x2 = x0 - POINTS_OUTER[i].first; + const auto y2 = y0 - POINTS_OUTER[i].second; + return x2 * x2 + y2 * y2; + } + static void insert_calc( + CellPoints& cell_pts, const XYPos& src, const SpreadData& spread_current, - const XYSize x, - const XYSize y + const XYPos& xy + ) noexcept + { +#ifdef DEBUG_CELLPOINTS + logging::note( + "Insert ({:f}, {:f}) at time {:f} with ROS {:f}, Intensity {:d}, RAZ {:f}", + x, + y, + arrival_time, + ros, + intensity, + raz.asDegrees() + ); +#endif + auto& spread_arrival = cell_pts.spread_arrival_; + auto& spread_internal = cell_pts.spread_internal_; + // count things as the same time if within a tolerance + constexpr auto TIME_EPSILON_SECONDS = 1.0 * MINUTE_SECONDS; + constexpr auto TIME_EPSILON = TIME_EPSILON_SECONDS / DAY_SECONDS; + if (0 < spread_current.time && 0 > spread_arrival.time) + { +#ifdef DEBUG_CELLPOINTS + logging::extensive( + "No time so setting ros to {:f} at time {:f}", spread_current.ros, spread_current.time + ); +#endif + // record ros and time if nothing yet + spread_arrival = spread_current; + } + // no point in any of this if not outputting individual or intensity + else + { + // initial burn will have an invalid direction, so needs to burn everywhere + const auto is_initial = Direction::Invalid() == spread_current.direction_previous; + // only spread in a direction that's in front of the normal to the angle it came from + // i.e. the 90 degrees on either side of the raz + const auto dir_diff = + abs(spread_current.direction.asDegrees() - spread_current.direction_previous.asDegrees()); + const auto MAX_DEGREES = 90.0; + // NOTE: there should be no change in the extent of the fire if we exclude things behind the + // normal to the direction it came from + // - but if we exclude too much then it can change how things spread, even if it is a + // more representative angle for the grids + if (is_initial || MAX_DEGREES >= dir_diff) + { + if (abs(spread_current.time - spread_arrival.time) <= TIME_EPSILON) + // else if (arrival_time == arrival_time_) + { +#ifdef DEBUG_CELLPOINTS + logging::verbose( + "Same time so setting ros to max({:f}, {:f}) at time {:f}", + spread_current.ros, + spread_arrival.ros, + spread_current.time + ); +#endif + // the same time so pick higher ros + if ( + (spread_arrival.ros < spread_current.ros) + || (spread_arrival.ros == spread_current.ros + && spread_current.intensity > spread_arrival.intensity)) + { + // NOTE: keep track of original time so this doesn't just always happen + spread_arrival = { + spread_arrival.time, + spread_current.intensity, + spread_current.ros, + spread_current.direction, + spread_current.direction_previous + }; + } + } + } + } + // NOTE: use location inside cell so smaller types can be more precise + // since digits aren't wasted on cell + const auto& cell_x_y = cell_pts.cell_x_y_; + auto& directions = cell_pts.directions; + const DistanceSize x0{static_cast(xy.x.value - cell_x_y.x_value())}; + const DistanceSize y0{static_cast(xy.y.value - cell_x_y.y_value())}; + // CHECK: FIX: is this initializing everything to false or just one element? + std::array closer{}; + std::fill_n(closer.begin(), NUM_DIRECTIONS, false); + for (size_t i = 0; i < NUM_DIRECTIONS; ++i) + { + const auto d = distance(x0, y0, POINTS_OUTER[i].first, POINTS_OUTER[i].second); + auto& p_d = cell_pts.distances[i]; + auto& p_p = cell_pts.points[i]; + auto& p_a = directions[i]; + closer[i] = (d < p_d); + p_p = (d < p_d) ? xy : p_p; + p_d = (d < p_d) ? d : p_d; + // FIX: this is going to be comparing the new p_d value so it is wrong but old behaviour + p_a = (d < p_d) ? spread_current.direction.asDegrees() : p_a; + } +#ifdef DEBUG_CELLPOINTS + logging::note("now have {:d} points", size()); +#endif + const auto dst_xy = cell_pts.pos(); + const XYIdx src_xy{src}; + // adds 0 if the same so try without checking + { + // we inserted a pair of (src, dst), which means we've never + // calculated the relativeIndex for this so add it to main map + cell_pts.add_source(src_xy.relativeIndex(dst_xy)); + } + // if (src.hash() == dst.hash()) + if (src_xy == dst_xy) + { + // if we spread from this cell to this cell again then ros could be considered for max + // need to make sure we're not spreading back towards where we came from because that doesn't + // matter HACK: for now look at source for this cell and exclude points in those directions + const auto srcs = cell_pts.sources(); + for (size_t i = 0; i < NUM_DIRECTIONS; ++i) + { + const auto mask = DIRECTION_MASKS[i]; + if (mask != (srcs & mask)) + { + // at least one of the cells in this direction is not a source, so consider them + if (closer[i]) + { + // point was closer to edge than what was there + if (spread_current.ros >= spread_internal.ros) + { + // since we spread within cell then set internal spread + spread_internal = spread_current; + } + } + } + } + } + } + static void insert_basic( + CellPoints& cell_pts, + const XYPos&, + const SpreadData& spread_current, + const XYPos& xy ) noexcept; + static CellPoints& insert( + CellPoints& cell_pts, + const XYPos& src, + const SpreadData& spread_current, + const XYPos& xy + ) noexcept + { + static const auto fct = [&]() { return (should_save()) ? &insert_calc : &insert_basic; }(); + fct(cell_pts, src, spread_current, xy); + return cell_pts; + } + +private: + // HACK: repeat for now + CellPoints(const XYIdx& cell_xy) noexcept : cell_x_y_{cell_xy} + { + std::ranges::fill(distances, INVALID_DISTANCE); + // FIX: thought invalid would work, but does this need to be 0? + std::ranges::fill(points, XYPos::Invalid()); + if (should_save()) + { + std::ranges::fill(directions, INVALID_DIRECTION.value); + } +#ifdef DEBUG_CELLPOINTS + logging::note("CellPoints is size {:d} after creation and should be empty", size()); +#endif + } + CellPoints(const XYPos& p) noexcept : CellPoints(XYIdx{p}) { } + +private: + // HACK: define constants so we don't have to cast + static constexpr auto I_0_0 = static_cast(0.0); + static constexpr auto I_0_5 = static_cast(0.5); + static constexpr auto I_1_0 = static_cast(1.0); + static constexpr auto DIST_22_5 = + static_cast(0.2071067811865475244008443621048490392848359376884740365883398689); + static constexpr auto P_0_5 = static_cast(0.5) + DIST_22_5; + static constexpr auto M_0_5 = static_cast(0.5) - DIST_22_5; + using d = std::pair; + static constexpr std::array POINTS_OUTER{ + d{I_0_5, I_1_0}, + // north-northeast is closest to point (0.5 + 0.207, 1.0) + d{P_0_5, I_1_0}, + // northeast is closest to point (1.0, 1.0) + d{I_1_0, I_1_0}, + // east-northeast is closest to point (1.0, 0.5 + 0.207) + d{I_1_0, P_0_5}, + // east is closest to point (1.0, 0.5) + d{I_1_0, I_0_5}, + // east-southeast is closest to point (1.0, 0.5 - 0.207) + d{I_1_0, M_0_5}, + // southeast is closest to point (1.0, 0.0) + d{I_1_0, I_0_0}, + // south-southeast is closest to point (0.5 + 0.207, 0.0) + d{P_0_5, I_0_0}, + // south is closest to point (0.5, 0.0) + d{I_0_5, I_0_0}, + // south-southwest is closest to point (0.5 - 0.207, 0.0) + d{M_0_5, I_0_0}, + // southwest is closest to point (0.0, 0.0) + d{I_0_0, I_0_0}, + // west-southwest is closest to point (0.0, 0.5 - 0.207) + d{I_0_0, M_0_5}, + // west is closest to point (0.0, 0.5) + d{I_0_0, I_0_5}, + // west-northwest is closest to point (0.0, 0.5 + 0.207) + d{I_0_0, P_0_5}, + // northwest is closest to point (0.0, 1.0) + d{I_0_0, I_1_0}, + // north-northwest is closest to point (0.5 - 0.207, 1.0) + d{M_0_5, I_1_0} + }; + +public: + using spreading_points = map>>; + constexpr CellPoints() noexcept = default; + // HACK: so we can emplace with nullptr + CellPoints(const CellPoints* rhs) noexcept + { + logging::check_fatal(nullptr == rhs, "Initializing CellPoints from nullptr"); + *this = *rhs; + } + CellPoints& insert(const XYPos& src, const SpreadData& spread_current, const XYPos& xy) noexcept + { + return CellPoints::insert(*this, src, spread_current, xy); + } + CellPoints(const XYPos& src, const SpreadData& spread_current, const XYPos& xy) noexcept + : CellPoints(XYIdx{xy}) + { + insert(src, spread_current, xy); + } CellPoints(CellPoints&& rhs) noexcept = default; CellPoints(const CellPoints& rhs) noexcept = default; CellPoints& operator=(CellPoints&& rhs) noexcept = default; CellPoints& operator=(const CellPoints& rhs) noexcept = default; - CellPoints& insert( - const XYPos& src, - const SpreadData& spread_current, - const XYSize x, - const XYSize y - ) noexcept; - CellPoints& insert(const InnerPos& p) noexcept; - void add_source(const CellIndex src); + void add_source(const CellIndex src) { src_ |= src; } CellIndex sources() const { return src_; } - CellPoints& merge(const CellPoints& rhs); - set unique() const noexcept; + CellPoints& merge(const CellPoints& rhs) + { +#ifdef DEBUG_CELLPOINTS + const auto n0 = size(); + const auto n1 = rhs.size(); +#endif + // either both invalid or lower one is valid + cell_x_y_ = min(cell_x_y_, rhs.cell_x_y_); + auto& d0 = distances; + auto& d1 = rhs.distances; + auto& p0 = points; + auto& p1 = rhs.points; + auto& a0 = directions; + auto& a1 = rhs.directions; + // we know distances in each direction so just pick closer + for (size_t i = 0; i < d0.size(); ++i) + { + if (d1[i] < d0[i]) + { + d0[i] = d1[i]; + p0[i] = p1[i]; + a0[i] = a1[i]; + } + } + add_source(rhs.src_); + // if valid time and earlier then that would be the arrival time + if (INVALID_TIME != rhs.spread_arrival_.time + && (INVALID_TIME == spread_arrival_.time || rhs.spread_arrival_.time < spread_arrival_.time)) + { + spread_arrival_ = rhs.spread_arrival_; + } + // INVALID_ROS is -1 so just check > + if (rhs.spread_internal_.ros > spread_internal_.ros) + { + spread_internal_ = rhs.spread_internal_; + } #ifdef DEBUG_CELLPOINTS - size_t size() const noexcept; + logging::note("Merging {:d} with {:d} gives {:d} pts", n0, n1, size()); #endif - bool operator<(const CellPoints& rhs) const noexcept; - bool operator==(const CellPoints& rhs) const noexcept; - [[nodiscard]] Location location() const noexcept; + return *this; + } + set unique() const noexcept + { + // if any point is invalid then they all have to be + if (XPos::Invalid().value == points[0].x.value) + { + return {}; + } + return {points.begin(), points.end()}; + } + auto operator==(const CellPoints& rhs) const noexcept + { + return cell_x_y_ == rhs.cell_x_y_ && points == rhs.points; + } + std::partial_ordering operator<=>(const CellPoints& rhs) const noexcept + { + if (auto cmp = cell_x_y_ <=> rhs.cell_x_y_; 0 != cmp) + { + return cmp; + } + return points <=> rhs.points; + } + [[nodiscard]] constexpr const XYIdx& pos() const noexcept { return cell_x_y_; } void clear(); - bool empty() const; - SpreadData spread_arrival_; - SpreadData spread_internal_; - SpreadData spread_exit_; - // FIX: just access directly for now -public: - CellPointArrays pts_; - // use Idx instead of Location so it can be negative (invalid) - CellPos cell_x_y_; - CellIndex src_; + bool empty() const + { + // NOTE: if anything is invalid then everything must be + return (XPos::Invalid().value == points[0].x.value); + } + std::array, NUM_DIRECTIONS> point_directions() const noexcept + { + std::array, NUM_DIRECTIONS> pt_dirs{}; + auto& pts = points; + auto& dirs = directions; + for (size_t i = 0; i < pts.size(); ++i) + { + pt_dirs[i] = {pts[i], dirs[i]}; + } + return pt_dirs; + } + const SpreadData& spread_arrival() const noexcept { return spread_arrival_; } private: - CellPoints(const Idx cell_x, const Idx cell_y) noexcept; - CellPoints(const XYPos& p) noexcept; + SpreadData spread_arrival_{}; + SpreadData spread_internal_{}; + array_dists distances{}; + array_pts points{}; + array_dirs directions{}; + // any way to get rid of this since we're using it as the map key? + XYIdx cell_x_y_{}; + CellIndex src_{DIRECTION_NONE}; }; using spreading_points = CellPoints::spreading_points; -class Scenario; // map that merges items when try_emplace doesn't insert class CellPointsMap { public: - CellPointsMap(); - CellPoints& insert( - const XYPos& src, - const SpreadData& spread_current, - const XYSize x, - const XYSize y - ) noexcept; - CellPointsMap& merge(const BurnedData& unburnable, const CellPointsMap& rhs) noexcept; - set unique() const noexcept; + CellPointsMap() noexcept = default; + CellPointsMap& merge(const BurnedData& unburnable, const CellPointsMap& rhs) noexcept + { + // FIX: if we iterate through both they should be sorted + for (const auto& [location, pts] : rhs.cells_) + { + if (!unburnable.at(location)) + { + auto e = cells_.try_emplace(location, pts); + CellPoints& cell_pts = e.first->second; + if (!e.second) + { + // couldn't insert + cell_pts.merge(pts); + } #ifdef DEBUG_CELLPOINTS - size_t size() const noexcept; + logging::note( + "insert with size {:d} of ({:f}, {:f}) at time {:f} with ROS {:f} gives size {:d}", + n0, + x, + y, + arrival_time, + ros, + size() + ); #endif + } + } + return *this; + } + set unique() const noexcept + { + set r{}; + for (auto& [loc, pts] : cells_) + { + for (auto& p : pts.unique()) + { + r.insert(p); + } + } + return r; + } + using map_type = std::map; + using map_value = map_type::value_type; // apply function to each CellPoints within and remove matches - void remove_if(std::function&)> F) noexcept; + void remove_if(std::function F) noexcept { std::erase_if(cells_, F); } // FIX: public for debugging right now // private: - map map_; + // CellPoints contains XYIdx so no need for pair + map_type cells_{}; }; +static inline CellPoints& insert( + CellPointsMap& cell_pts_map, + const XYPos& src, + const SpreadData& spread_current, + const XYPos& xy +) noexcept +{ +#ifdef DEBUG_CELLPOINTS + const auto n0 = size(); +#endif + const XYIdx location{xy}; + auto& lhs = cell_pts_map.cells_; + auto e = lhs.try_emplace(location, src, spread_current, xy); + CellPoints& cell_pts = e.first->second; + if (!e.second) + { + // FIX: should use max of whatever ROS has entered during this time and not just first ros + // tried to add new CellPoints but already there + cell_pts.insert(src, spread_current, xy); +#ifdef DEBUG_CELLPOINTS + logging::note( + "insert with size {:d} of ({:f}, {:f}) at time {:f} with ROS {:f} gives size {:d}", + n0, + x, + y, + arrival_time, + ros, + size() + ); +#endif + } + return cell_pts; +} } #endif diff --git a/src/cpp/fs/ConstantGrid.cpp b/src/cpp/fs/ConstantGrid.cpp index b109b50524c..64aac3e10d3 100644 --- a/src/cpp/fs/ConstantGrid.cpp +++ b/src/cpp/fs/ConstantGrid.cpp @@ -48,61 +48,59 @@ int value_at_int(void* const buf, const FullIdx offset) logging::debug("NODATA value is originally parsed as {:d}", nodata_orig); const auto nodata_input = static_cast(stoi(string(static_cast(data)))); logging::debug("NODATA value is parsed as {:d}", nodata_input); - auto actual_rows = grid_info.calculateRows(); - auto actual_columns = grid_info.calculateColumns(); + auto actual_height = grid_info.calculateHeight(); + auto actual_width = grid_info.calculateWidth(); const auto coordinates = grid_info.findFullCoordinates(point, true); logging::note( "Coordinates before reading are ({:d}, {:d} => {:f}, {:f})", - std::get<0>(*coordinates), - std::get<1>(*coordinates), - std::get<0>(*coordinates) + std::get<2>(*coordinates) / 1000.0, - std::get<1>(*coordinates) + std::get<3>(*coordinates) / 1000.0 + coordinates->x, + coordinates->y, + coordinates->x + coordinates->x_sub / 1000.0, + coordinates->y + coordinates->y_sub / 1000.0 ); - auto min_column = max( + auto min_x = max( static_cast(0), - static_cast( - std::get<1>(*coordinates) - static_cast(MAX_COLUMNS) / static_cast(2) - ) + static_cast(coordinates->x - static_cast(MAX_WIDTH) / static_cast(2)) ); - if (min_column + MAX_COLUMNS >= actual_columns) + if (min_x + MAX_WIDTH >= actual_width) { - min_column = max(static_cast(0), actual_columns - MAX_COLUMNS); + min_x = max(static_cast(0), actual_width - MAX_WIDTH); } // make sure we're at the start of a tile - const auto tile_column = tile_width * static_cast(min_column / tile_width); - const auto max_column = static_cast(min(min_column + MAX_COLUMNS - 1, actual_columns)); + const auto tile_x = tile_width * static_cast(min_x / tile_width); + const auto max_x = static_cast(min(min_x + MAX_WIDTH - 1, actual_width)); #ifdef DEBUG_GRIDS - logging::check_fatal(min_column < 0, "Column can't be less than 0"); - logging::check_fatal( - max_column - min_column > MAX_COLUMNS, "Can't have more than {:d} columns", MAX_COLUMNS - ); + logging::check_fatal(min_x < 0, "X can't be less than 0"); + logging::check_fatal(max_x - min_x > MAX_WIDTH, "Can't have width more than {:d}", MAX_WIDTH); logging::check_fatal( - max_column > actual_columns, "Can't have more than actual {:d} columns", actual_columns + max_x > actual_width, "Can't have more than actual width {:d}", actual_width ); #endif - auto min_row = max( + auto min_y = max( static_cast(0), static_cast( - std::get<0>(*coordinates) - static_cast(MAX_ROWS) / static_cast(2) + coordinates->y - static_cast(MAX_HEIGHT) / static_cast(2) ) ); - if (min_row + MAX_COLUMNS >= actual_rows) + if (min_y + MAX_WIDTH >= actual_height) { - min_row = max(static_cast(0), actual_rows - MAX_ROWS); + min_y = max(static_cast(0), actual_height - MAX_HEIGHT); } - const auto tile_row = tile_width * static_cast(min_row / tile_width); - const auto max_row = static_cast(min(min_row + MAX_ROWS - 1, actual_rows)); + const auto tile_y = tile_width * static_cast(min_y / tile_width); + const auto max_y = static_cast(min(min_y + MAX_HEIGHT - 1, actual_height)); #ifdef DEBUG_GRIDS - logging::check_fatal(min_row < 0, "Row can't be less than 0 but is {:d}", min_row); + logging::check_fatal(min_y < 0, "Y can't be less than 0 but is {:d}", min_y); + logging::check_fatal( + max_y - min_y > MAX_HEIGHT, + "Can't have height more than {:d} but have {:d}", + MAX_HEIGHT, + max_y - min_y + ); logging::check_fatal( - max_row - min_row > MAX_ROWS, - "Can't have more than {:d} rows but have {:d}", - MAX_ROWS, - max_row - min_row + max_y > actual_height, "Can't have more than actual height {:d}", actual_height ); - logging::check_fatal(max_row > actual_rows, "Can't have more than actual {:d} rows", actual_rows); #endif - vector values(static_cast(MAX_ROWS) * MAX_COLUMNS, nodata_input); + vector values(static_cast(MAX_HEIGHT) * MAX_WIDTH, nodata_input); logging::verbose("{:s}: malloc start", geotiff.filename()); int bps = std::numeric_limits::digits + (1 * std::numeric_limits::is_signed); uint16_t sample_format; @@ -140,12 +138,12 @@ int value_at_int(void* const buf, const FullIdx offset) const tsample_t smp{}; logging::debug( "Want to clip grid to ({:d}, {:d}) => ({:d}, {:d}) for a {:d}{:d} raster", - min_row, - min_column, - max_row, - max_column, - actual_rows, - actual_columns + min_x, + min_y, + max_x, + max_y, + actual_width, + actual_height ); // HACK: it really feels like there should be a better way to do this switch (sample_format) @@ -203,26 +201,26 @@ int value_at_int(void* const buf, const FullIdx offset) "SAMPLEFORMAT {:d} has invalid TIFFTAG_BITSPERSAMPLE {:d}", sample_format, bps_file )); }(); - for (auto h = tile_row; h <= max_row; h += tile_length) + for (auto h = tile_y; h <= max_y; h += tile_length) { - for (auto w = tile_column; w <= max_column; w += tile_width) + for (auto w = tile_x; w <= max_x; w += tile_width) { std::ignore = TIFFReadTile(tif, buf, static_cast(w), static_cast(h), 0, smp); - for (FullIdx y = 0; (y < static_cast(tile_length)) && (y + h <= max_row); ++y) + for (FullIdx y = 0; (y < static_cast(tile_length)) && (y + h <= max_y); ++y) { // read in so that (0, 0) has a hash of 0 - const auto y_row = static_cast((h - min_row) + y); - const auto actual_row = (max_row - min_row) - y_row; - if (actual_row >= 0 && actual_row < MAX_ROWS) + const auto y0 = static_cast((h - min_y) + y); + const auto actual_y = (max_y - min_y) - y0; + if (actual_y >= 0 && actual_y < MAX_HEIGHT) { - for (auto x = 0; (x < static_cast(tile_width)) && (x + w <= max_column); ++x) + for (auto x = 0; (x < static_cast(tile_width)) && (x + w <= max_x); ++x) { const auto offset = y * tile_width + x; - const auto actual_column = ((w - min_column) + x); - if (actual_column >= 0 && actual_column < MAX_ROWS) + const auto actual_x = ((w - min_x) + x); + if (actual_x >= 0 && actual_x < MAX_HEIGHT) { - const auto cur_hash = actual_row * MAX_COLUMNS + actual_column; + const auto cur_hash = actual_y * MAX_WIDTH + actual_x; // auto cur = *(static_cast(buf) + offset); auto cur = value_at(buf, offset); #ifdef DEBUG_GRIDS @@ -240,10 +238,10 @@ int value_at_int(void* const buf, const FullIdx offset) _TIFFfree(buf); logging::verbose("{:s}: free end", geotiff.filename()); const auto new_xll = - grid_info.xllcorner() + (static_cast(min_column) * grid_info.cellSize()); + grid_info.xllcorner() + (static_cast(min_x) * grid_info.cellSize()); const auto new_yll = grid_info.yllcorner() - + (static_cast(actual_rows) - static_cast(max_row)) * grid_info.cellSize(); + + (static_cast(actual_height) - static_cast(max_y)) * grid_info.cellSize(); #ifdef DEBUG_GRIDS logging::check_fatal(new_yll < grid_info.yllcorner(), "New yllcorner is outside original grid"); #endif @@ -254,31 +252,31 @@ int value_at_int(void* const buf, const FullIdx offset) grid_info.xllcorner(), grid_info.yllcorner() ); - const auto num_rows = max_row - min_row + 1; - const auto num_columns = max_column - min_column + 1; + const auto height_calc = max_y - min_y + 1; + const auto width_calc = max_x - min_x + 1; ConstantGrid result{ grid_info.cellSize(), - static_cast(num_rows), - static_cast(num_columns), + static_cast(width_calc), + static_cast(height_calc), nodata_input, nodata_input, new_xll, new_yll, - new_xll + (static_cast(num_columns) + 1) * grid_info.cellSize(), - new_yll + (static_cast(num_rows) + 1) * grid_info.cellSize(), + new_xll + (static_cast(width_calc) + 1) * grid_info.cellSize(), + new_yll + (static_cast(height_calc) + 1) * grid_info.cellSize(), string(grid_info.proj4()), std::move(values) }; auto new_location = result.findCoordinates(point, false); #ifdef DEBUG_GRIDS - logging::check_fatal(nullptr == new_location, "Invalid location after reading"); + logging::check_fatal(!new_location.has_value(), "Invalid location after reading"); #endif logging::note( "Coordinates are ({:d}, {:d} => {:f}, {:f})", - std::get<0>(*new_location), - std::get<1>(*new_location), - std::get<0>(*new_location) + std::get<2>(*new_location) / 1000.0, - std::get<1>(*new_location) + std::get<3>(*new_location) / 1000.0 + new_location->x, + new_location->y, + new_location->x + new_location->x_sub / 1000.0, + new_location->y + new_location->y_sub / 1000.0 ); #ifdef DEBUG_GRIDS logging::note("Values for {:s} range from {:d} to {:d}", filename, min_value, max_value); diff --git a/src/cpp/fs/ConstantGrid.h b/src/cpp/fs/ConstantGrid.h index 0af374e7d6e..e73a4081bea 100644 --- a/src/cpp/fs/ConstantGrid.h +++ b/src/cpp/fs/ConstantGrid.h @@ -3,6 +3,7 @@ #define FS_CONSTANTGRID_H #include "stdafx.h" #include "Grid.h" +#include "Location.h" #include "Util.h" namespace fs { @@ -20,42 +21,16 @@ class ConstantGrid : public GridData> { public: ConstantGrid() = default; - /** - * \brief Value for grid at given Location. - * \param location Location to get value for. - * \return Value at grid Location. - */ - [[nodiscard]] constexpr T at(const Location& location) const noexcept override - { -#ifdef DEBUG_GRIDS - logging::check_fatal( - location.row() >= this->rows() || location.column() >= this->columns(), - "Out of bounds ({:d}, {:d})", - location.row(), - location.column() - ); -#endif -#ifdef DEBUG_POINTS - { - const Location loc{location.row(), location.column()}; - logging::check_equal(loc.column(), location.column(), "column"); - logging::check_equal(loc.row(), location.row(), "row"); - // if we're going to use the hash then we need to make sure it actually matches - logging::check_equal(loc.hash(), location.hash(), "hash"); - } -#endif - return this->data.at(location.hash()); - } - template - [[nodiscard]] constexpr T at(const Position

& position) const noexcept + [[nodiscard]] constexpr T at(const XYIdx& location) const noexcept override { - return at(Location{position.hash()}); + // constant grids would use index not hash + return this->data.at(to_index(location)); } /** * \brief Throw an error because ConstantGrid can't change values. */ // ! @cond Doxygen_Suppress - void set(const Location&, const T) override + void set(const XYIdx&, const T) override // ! @endcond { throw runtime_error("Cannot change ConstantGrid"); @@ -65,24 +40,10 @@ class ConstantGrid : public GridData> ConstantGrid(ConstantGrid&& rhs) noexcept = default; ConstantGrid& operator=(const ConstantGrid& rhs) noexcept = default; ConstantGrid& operator=(ConstantGrid&& rhs) noexcept = default; - /** - * \brief Constructor - * \param cell_size Cell width and height (m) - * \param rows Number of rows - * \param columns Number of columns - * \param nodata_input Value that represents no data for type V - * \param nodata_value Value that represents no data for type T - * \param xllcorner Lower left corner X coordinate (m) - * \param yllcorner Lower left corner Y coordinate (m) - * \param xurcorner Upper right corner X coordinate (m) - * \param yurcorner Upper right corner Y coordinate (m) - * \param proj4 Proj4 projection definition - * \param data Data to set as grid data - */ ConstantGrid( const MathSize cell_size, - const Idx rows, - const Idx columns, + const Idx width, + const Idx height, const V nodata_input, const T nodata_value, const MathSize xllcorner, @@ -94,8 +55,8 @@ class ConstantGrid : public GridData> ) : GridData>( cell_size, - rows, - columns, + width, + height, nodata_input, nodata_value, xllcorner, @@ -106,22 +67,10 @@ class ConstantGrid : public GridData> std::move(data) ) { } - /** - * \brief Constructor - * \param cell_size Cell width and height (m) - * \param rows Number of rows - * \param columns Number of columns - * \param nodata_input Value that represents no data for type V - * \param nodata_value Value that represents no data for type T - * \param xllcorner Lower left corner X coordinate (m) - * \param yllcorner Lower left corner Y coordinate (m) - * \param proj4 Proj4 projection definition - * \param initialization_value Value to initialize entire grid with - */ ConstantGrid( const MathSize cell_size, - const Idx rows, - const Idx columns, + const Idx width, + const Idx height, const V nodata_input, const T nodata_value, const MathSize xllcorner, @@ -131,14 +80,14 @@ class ConstantGrid : public GridData> ) noexcept : ConstantGrid( cell_size, - rows, - columns, + width, + height, nodata_input, nodata_value, xllcorner, yllcorner, proj4, - std::move(vector(static_cast(MAX_ROWS) * MAX_COLUMNS, initialization_value)) + std::move(vector(static_cast(MAX_HEIGHT) * MAX_WIDTH, initialization_value)) ) { } /** @@ -162,14 +111,14 @@ class ConstantGrid : public GridData> convert(nodata_input, nodata_input) != nodata_value, "Expected nodata value to be returned from convert()" ); - vector values(static_cast(MAX_ROWS) * MAX_COLUMNS, nodata_value); + vector values(static_cast(MAX_HEIGHT) * MAX_WIDTH, nodata_value); std::transform(grid.data.begin(), grid.data.end(), values.begin(), [&](const int v) { return convert(static_cast(v), nodata_input); }); ConstantGrid result{ grid.cellSize(), - grid.rows(), - grid.columns(), + grid.width(), + grid.height(), static_cast(grid.nodataInput()), nodata_value, grid.xllcorner(), @@ -195,12 +144,11 @@ class ConstantGrid : public GridData> protected: tuple dataBounds() const override { - Idx min_row = 0; - Idx max_row = this->rows(); - Idx min_column = 0; - Idx max_column = this->columns(); - // // #endif - return tuple{min_column, min_row, max_column, max_row}; + const Idx min_y = 0; + const Idx max_y = this->height(); + const Idx min_x = 0; + const Idx max_x = this->width(); + return {min_x, min_y, max_x, max_y}; } private: @@ -231,7 +179,7 @@ class ConstantGrid : public GridData> { #ifdef DEBUG_GRIDS logging::check_fatal( - this->data.size() != static_cast(MAX_ROWS) * MAX_COLUMNS, "Invalid grid size" + this->data.size() != static_cast(MAX_HEIGHT) * MAX_WIDTH, "Invalid grid size" ); #endif } @@ -240,6 +188,6 @@ class FuelType; class Cell; using FuelGrid = ConstantGrid; using ElevationGrid = ConstantGrid; -using CellGrid = ConstantGrid; +using CellGrid = ConstantGrid; } #endif diff --git a/src/cpp/fs/Environment.cpp b/src/cpp/fs/Environment.cpp index 1f01e8fa27d..159e787417f 100644 --- a/src/cpp/fs/Environment.cpp +++ b/src/cpp/fs/Environment.cpp @@ -4,6 +4,7 @@ #include "EnvironmentInfo.h" #include "FuelLookup.h" #include "Grid.h" +#include "Location.h" #include "Log.h" #include "Point.h" #include "ProbabilityMap.h" @@ -97,24 +98,24 @@ Environment Environment::loadEnvironment( // bounds of perimeter flipped because we're reading from a raster so change (left, top) to // (left, bottom) const auto coordinates = cur_info->findFullCoordinates(point, true); - if (nullptr != coordinates) + if (coordinates.has_value()) { - auto actual_rows = cur_info->calculateRows(); - auto actual_columns = cur_info->calculateColumns(); - const auto x = std::get<0>(*coordinates); - const auto y = std::get<1>(*coordinates); + auto actual_height = cur_info->calculateHeight(); + auto actual_width = cur_info->calculateWidth(); + const auto x = coordinates->x; + const auto y = coordinates->y; logging::note( "Coordinates before reading are ({:d}, {:d} => {:f}, {:f})", x, y, - x + std::get<2>(*coordinates) / 1000.0, - y + std::get<3>(*coordinates) / 1000.0 + x + coordinates->x_sub / 1000.0, + y + coordinates->y_sub / 1000.0 ); // if it's not in the raster then this is not an option // FIX: are these +/-1 because of counting the cell itself and starting from 0? const auto dist_W = x; - const auto dist_E = actual_columns - x; - const auto dist_N = actual_rows - y; + const auto dist_E = actual_width - x; + const auto dist_N = actual_height - y; const auto dist_S = y; // FIX: should take size of cells into account too? But is largest areas or highest resolution // the priority? @@ -157,7 +158,8 @@ Environment Environment::loadEnvironment( // envInfo should get deleted automatically because it uses unique_ptr return env_info->load(point); } -unique_ptr Environment::findCoordinates(const Point& point, const bool flipped) const +std::optional Environment::findCoordinates(const Point& point, const bool flipped) + const { return cells_.findCoordinates(point, flipped); } @@ -167,21 +169,19 @@ CellGrid Environment::makeCells(const FuelGrid& fuel, const ElevationGrid& eleva logging::check_equal(fuel.yllcorner(), elevation.yllcorner(), "yllcorner"); static Cell nodata{}; auto values = vector{fuel.data.size()}; - vector hashes{}; - for (Idx r = 0; r < fuel.rows(); ++r) + for (Idx y = 0; y < fuel.height(); ++y) { - for (Idx c = 0; c < fuel.columns(); ++c) + for (Idx x = 0; x < fuel.width(); ++x) { - const auto h = hashes.emplace_back(Location(r, c).hash()); - const Location loc{r, c, h}; - if (r >= 0 && r < fuel.rows() && c >= 0 && c < fuel.columns()) + const XYIdx loc{x, y}; + if (y >= 0 && y < fuel.height() && x >= 0 && x < fuel.width()) { // NOTE: this needs to translate to internal codes? const auto f = FuelType::safeCode(fuel.at(loc)); auto s = static_cast(INVALID_SLOPE); auto a = static_cast(INVALID_ASPECT); // HACK: don't calculate for outside box of cells - if (r > 0 && r < fuel.rows() - 1 && c > 0 && c < fuel.columns() - 1) + if (y > 0 && y < fuel.height() - 1 && x > 0 && x < fuel.width() - 1) { MathSize dem[9]; bool valid = true; @@ -190,9 +190,9 @@ CellGrid Environment::makeCells(const FuelGrid& fuel, const ElevationGrid& eleva for (int j = -1; j < 2; ++j) { // grid is (0, 0) at bottom left, but want [0] in array to be NW corner - auto actual_row = static_cast(r - i); - auto actual_column = static_cast(c + j); - auto cur_loc = Location{actual_row, actual_column}; + auto actual_y = static_cast(y - i); + auto actual_x = static_cast(x + j); + XYIdx cur_loc{actual_x, actual_y}; const auto v = elevation.at(cur_loc); // can't calculate slope & aspect if any surrounding cell is nodata if (elevation.nodataValue() == v) @@ -239,15 +239,14 @@ CellGrid Environment::makeCells(const FuelGrid& fuel, const ElevationGrid& eleva a = static_cast(round(aspect_azimuth)); } } - const auto cell = Cell{h, s, a, f}; - values.at(h) = cell; + const auto cell = Cell{s, a, f}; + // NOTE: this is going to be a vector that's the same size as the max size, despite the + // actual size of the contents + values.at(to_index(loc)) = cell; #ifdef DEBUG_GRIDS #ifndef VLD_RPTHOOK_INSTALL - logging::check_equal(cell.row(), r, "Cell row"); - logging::check_equal(cell.column(), c, "Cell column"); + const auto h = to_index(loc); const auto v = values.at(h); - logging::check_equal(v.row(), r, "Row"); - logging::check_equal(v.column(), c, "Column"); if (!(INVALID_SLOPE == cell.slope() || INVALID_ASPECT == cell.aspect() || INVALID_FUEL_CODE == cell.fuelCode())) { @@ -284,8 +283,8 @@ CellGrid Environment::makeCells(const FuelGrid& fuel, const ElevationGrid& eleva } return CellGrid( fuel.cellSize(), - fuel.rows(), - fuel.columns(), + fuel.width(), + fuel.height(), nodata.fullHash(), nodata, fuel.xllcorner(), @@ -309,16 +308,20 @@ Environment::Environment( (settings.save_simulation_area ? make_unique(fuel) : nullptr), (settings.save_simulation_area ? make_unique(elevation) : nullptr), makeCells(fuel, elevation), - elevation.at(Location(*elevation.findCoordinates(point, false).get())) + elevation.at([&]() { + auto loc = elevation.findCoordinates(point, false); + return XYIdx{loc->x, loc->y}; + }()) ) { // take elevation at point so that if max grid size changes elevation doesn't logging::note("Start elevation is {:d}", elevation_); } -Cell Environment::offset(const Event& event, const Idx row, const Idx column) const +Cell Environment::offset(const Event& event, const Idx x, const Idx y) const { - const auto& p = event.cell; - return cell(Location(p.row() + row, p.column() + column)); + const auto& p = event.xy; + // return cell(XYIdx{static_cast(p.x().value + x), static_cast(p.y().value + y)}); + return cell(p + XIdx{x} + YIdx{y}); } void Environment::saveToFile(const string_view output_directory) const { diff --git a/src/cpp/fs/Environment.h b/src/cpp/fs/Environment.h index 1fdef4e4c52..77463d7811c 100644 --- a/src/cpp/fs/Environment.h +++ b/src/cpp/fs/Environment.h @@ -73,22 +73,14 @@ class Environment * \param flipped Whether the grid data is flipped across the horizontal axis * \return Coordinates that would be at Point within this EnvironmentInfo, or nullptr if it is not */ - [[nodiscard]] unique_ptr findCoordinates(const Point& point, bool flipped) const; + [[nodiscard]] std::optional findCoordinates(const Point& point, bool flipped) const; /** * \brief UTM projection that this uses * \return UTM projection that this uses */ [[nodiscard]] constexpr string_view proj4() const { return cells_.proj4(); } - /** - * \brief Number of rows in grid - * \return Number of rows in grid - */ - [[nodiscard]] constexpr Idx rows() const { return cells_.rows(); } - /** - * \brief Number of columns in grid - * \return Number of columns in grid - */ - [[nodiscard]] constexpr Idx columns() const { return cells_.columns(); } + [[nodiscard]] constexpr Idx height() const { return cells_.height(); } + [[nodiscard]] constexpr Idx width() const { return cells_.width(); } /** * \brief Cell width and height (m) * \return Cell width and height (m) @@ -99,34 +91,8 @@ class Environment * \return Elevation of the origin Point */ [[nodiscard]] constexpr ElevationSize elevation() const { return elevation_; }; - /** - * \brief Cell at given row and column - * \param row Row - * \param column Column - * \return Cell at given row and column - */ - [[nodiscard]] Cell cell(const Idx row, const Idx column) const - { - return cells_.at(Location(row, column)); - } - /** - * \brief Cell at given Location - * \param location Location - * \return Cell at given Location - */ - template - [[nodiscard]] Cell cell(const Position

& position) const - { - return cells_.at(position); - } - /** - * \brief Cell at Location with offset of row and column from Location of Event - * \param event Event to use for base Location - * \param row - * \param column - * \return - */ - [[nodiscard]] Cell offset(const Event& event, const Idx row, const Idx column) const; + [[nodiscard]] Cell cell(const XYIdx& xy) const { return cells_.at(xy); } + [[nodiscard]] Cell offset(const Event& event, const Idx x, const Idx y) const; /** * \brief Make a ProbabilityMap that covers this Environment * \param time Time in simulation this ProbabilityMap represents diff --git a/src/cpp/fs/EnvironmentInfo.cpp b/src/cpp/fs/EnvironmentInfo.cpp index 22621dc2c15..6d7a0a96410 100644 --- a/src/cpp/fs/EnvironmentInfo.cpp +++ b/src/cpp/fs/EnvironmentInfo.cpp @@ -19,21 +19,21 @@ EnvironmentInfo::EnvironmentInfo( { logging::debug( "fuel: {:d}{:d} => ({:f}, {:f})", - fuel.calculateColumns(), - fuel.calculateRows(), + fuel.calculateWidth(), + fuel.calculateHeight(), fuel.xllcorner(), fuel.yllcorner() ); logging::debug( "elevation: {:d}{:d} => ({:f}, {:f})", - elevation.calculateColumns(), - elevation.calculateRows(), + elevation.calculateWidth(), + elevation.calculateHeight(), elevation.xllcorner(), elevation.yllcorner() ); logging::check_fatal( - !(fuel.calculateRows() == elevation.calculateRows() - && fuel.calculateColumns() == elevation.calculateColumns() + !(fuel.calculateHeight() == elevation.calculateHeight() + && fuel.calculateWidth() == elevation.calculateWidth() && fuel.cellSize() == elevation.cellSize() && fuel.xllcorner() == elevation.xllcorner() && fuel.yllcorner() == elevation.yllcorner()), "Grids are not aligned" @@ -66,12 +66,12 @@ Environment EnvironmentInfo::load(const Point& point) const { return Environment::load(point, in_fuel_, in_elevation_); } -unique_ptr EnvironmentInfo::findCoordinates(const Point& point, const bool flipped) +std::optional EnvironmentInfo::findCoordinates(const Point& point, const bool flipped) const { return fuel_.findCoordinates(point, flipped); } -unique_ptr EnvironmentInfo::findFullCoordinates( +std::optional EnvironmentInfo::findFullCoordinates( const Point& point, const bool flipped ) const diff --git a/src/cpp/fs/EnvironmentInfo.h b/src/cpp/fs/EnvironmentInfo.h index d28cb59d372..5180c2330d1 100644 --- a/src/cpp/fs/EnvironmentInfo.h +++ b/src/cpp/fs/EnvironmentInfo.h @@ -39,14 +39,14 @@ class EnvironmentInfo * \param flipped Whether the grid data is flipped across the horizontal axis * \return Coordinates that would be at Point within this EnvironmentInfo, or nullptr if it is not */ - [[nodiscard]] unique_ptr findCoordinates(const Point& point, bool flipped) const; + [[nodiscard]] std::optional findCoordinates(const Point& point, bool flipped) const; /** * \brief Determine FullCoordinates in the grid for the Point * \param point Point to find FullCoordinates for * \param flipped Whether the grid data is flipped across the horizontal axis * \return Coordinates that would be at Point within this EnvironmentInfo, or nullptr if it is not */ - [[nodiscard]] unique_ptr findFullCoordinates(const Point& point, bool flipped) + [[nodiscard]] std::optional findFullCoordinates(const Point& point, bool flipped) const; /** * \brief Load the full Environment @@ -54,16 +54,8 @@ class EnvironmentInfo * \return */ [[nodiscard]] Environment load(const Point& point) const; - /** - * \brief Number of rows in grid - * \return Number of rows in grid - */ - [[nodiscard]] constexpr FullIdx calculateRows() const { return fuel_.calculateRows(); } - /** - * \brief Number of columns in grid - * \return Number of columns in grid - */ - [[nodiscard]] constexpr FullIdx calculateColumns() const { return fuel_.calculateColumns(); } + [[nodiscard]] constexpr FullIdx calculateHeight() const { return fuel_.calculateHeight(); } + [[nodiscard]] constexpr FullIdx calculateWidth() const { return fuel_.calculateWidth(); } /** * \brief UTM projection that this uses * \return UTM projection that this uses diff --git a/src/cpp/fs/Event.cpp b/src/cpp/fs/Event.cpp index 3e383669228..596a36a2c15 100644 --- a/src/cpp/fs/Event.cpp +++ b/src/cpp/fs/Event.cpp @@ -2,44 +2,4 @@ #include "Event.h" namespace fs { -std::partial_ordering Event::operator<=>(const Event& rhs) const -{ - if (const auto cmp = time <=> rhs.time; 0 != cmp) - { - return cmp; - } - if (const auto cmp = type <=> rhs.type; 0 != cmp) - { - return cmp; - } - // if (const auto cmp = time_at_location <=> rhs.time_at_location; 0 != cmp) - // { - // return cmp; - // } - // if (const auto cmp = cell <=> rhs.cell; 0 != cmp) - // { - // return cmp; - // } - if (const auto cmp = cell.hash() <=> rhs.cell.hash(); 0 != cmp) - { - return cmp; - } - // if (const auto cmp = ros <=> rhs.ros; 0 != cmp) - // { - // return cmp; - // } - // if (const auto cmp = intensity <=> rhs.intensity; 0 != cmp) - // { - // return cmp; - // } - // if (const auto cmp = raz.value <=> rhs.raz.value; 0 != cmp) - // { - // return cmp; - // } - // if (const auto cmp = source <=> rhs.source; 0 != cmp) - // { - // return cmp; - // } - return std::partial_ordering::equivalent; -}; } diff --git a/src/cpp/fs/Event.h b/src/cpp/fs/Event.h index dfcfddaf7a2..14f4cceaba8 100644 --- a/src/cpp/fs/Event.h +++ b/src/cpp/fs/Event.h @@ -2,7 +2,9 @@ #ifndef FS_EVENT_H #define FS_EVENT_H #include "stdafx.h" +#include #include "Cell.h" +#include "Location.h" #include "Weather.h" namespace fs { @@ -39,10 +41,7 @@ struct Event * \brief Duration that Event Cell has been burning (decimal days) */ DurationSize time_at_location{0.0}; - /** - * \brief Cell Event takes place in - */ - Cell cell{}; + XYIdx xy{}; /** * \brief Head fire rate of spread (m/min) */ @@ -59,7 +58,18 @@ struct Event * \brief CellIndex for relative Cell that spread into from */ CellIndex source{}; - std::partial_ordering operator<=>(const Event& rhs) const; + std::partial_ordering operator<=>(const Event& rhs) const + { + if (const auto cmp = time <=> rhs.time; 0 != cmp) + { + return cmp; + } + if (const auto cmp = type <=> rhs.type; 0 != cmp) + { + return cmp; + } + return xy <=> rhs.xy; + } }; } #endif diff --git a/src/cpp/fs/FireSpread.h b/src/cpp/fs/FireSpread.h index 34b2d61325a..de7b1b312fd 100644 --- a/src/cpp/fs/FireSpread.h +++ b/src/cpp/fs/FireSpread.h @@ -4,11 +4,19 @@ #include "stdafx.h" #include "Cell.h" #include "FWI.h" -#include "InnerPos.h" +#include "Location.h" #include "Point.h" #include "Weather.h" namespace fs { +struct ROSOffset +{ + IntensitySize intensity; + ROSSize ros; + Direction raz; + Offset offset; +}; +using OffsetSet = vector; class FuelType; static constexpr MathSize MAX_SPREAD_ANGLE = 5.0; static constexpr MathSize INVALID_ROS = -1.0; diff --git a/src/cpp/fs/FireWeather.h b/src/cpp/fs/FireWeather.h index 4db90e064da..64057cd9434 100644 --- a/src/cpp/fs/FireWeather.h +++ b/src/cpp/fs/FireWeather.h @@ -3,9 +3,6 @@ #define FS_FIREWEATHER_H #include "stdafx.h" #include "FWI.h" -#ifdef DEBUG_FWI_WEATHER -#include "Log.h" -#endif namespace fs { class FuelType; diff --git a/src/cpp/fs/Grid.cpp b/src/cpp/fs/Grid.cpp index 31d41534a61..a44f3d0ebd4 100644 --- a/src/cpp/fs/Grid.cpp +++ b/src/cpp/fs/Grid.cpp @@ -17,90 +17,77 @@ using fs::try_fix_meridian; template string saveToTiffFile( const GridBase& grid, - const Idx columns, - const Idx rows, + const Idx width, + const Idx height, const tuple bounds, const string_view dir, const string_view base_name, const uint16_t bits_per_sample, const uint16_t sample_format, - std::function value_at, + std::function value_at, const int nodata_as_int ) { - uint32_t tileWidth = min(static_cast(columns), 256); - uint32_t tileHeight = min(static_cast(rows), 256); - auto min_column = std::get<0>(bounds); - auto min_row = std::get<1>(bounds); - auto max_column = std::get<2>(bounds); - auto max_row = std::get<3>(bounds); - logging::check_fatal( - min_column > max_column, "Invalid bounds for columns with {:d} => {:d}", min_column, max_column - ); - logging::check_fatal( - min_row > max_row, "Invalid bounds for rows with {:d} => {:d}", min_row, max_row - ); + uint32_t tileWidth = min(static_cast(width), 256); + uint32_t tileHeight = min(static_cast(height), 256); + auto [min_x, min_y, max_x, max_y] = bounds; + // auto min_x = std::get<0>(bounds); + // auto min_y = std::get<1>(bounds); + // auto max_x = std::get<2>(bounds); + // auto max_y = std::get<3>(bounds); + logging::check_fatal(min_x > max_x, "Invalid bounds for width with {:d} => {:d}", min_x, max_x); + logging::check_fatal(min_y > max_y, "Invalid bounds for height with {:d} => {:d}", min_y, max_y); #ifdef DEBUG_GRIDS - logging::debug( - "Bounds are ({:d}, {:d}), ({:d}, {:d}) initially", min_column, min_row, max_column, max_row - ); + logging::debug("Bounds are ({:d}, {:d}), ({:d}, {:d}) initially", min_x, min_y, max_x, max_y); #endif Idx c_min = 0; - while (c_min + static_cast(tileWidth) <= min_column) + while (c_min + static_cast(tileWidth) <= min_x) { c_min += static_cast(tileWidth); } Idx c_max = c_min + static_cast(tileWidth); - while (c_max < max_column) + while (c_max < max_x) { c_max += static_cast(tileWidth); } - min_column = c_min; - max_column = c_max; + min_x = c_min; + max_x = c_max; Idx r_min = 0; - while (r_min + static_cast(tileHeight) <= min_row) + while (r_min + static_cast(tileHeight) <= min_y) { r_min += static_cast(tileHeight); } Idx r_max = r_min + static_cast(tileHeight); - while (r_max < max_row) + while (r_max < max_y) { r_max += static_cast(tileHeight); } - min_row = r_min; - max_row = r_max; - logging::check_fatal( - min_column >= max_column, "Invalid bounds for columns with {:d} => {:d}", min_column, max_column - ); - logging::check_fatal( - min_row >= max_row, "Invalid bounds for rows with {:d} => {:d}", min_row, max_row - ); + min_y = r_min; + max_y = r_max; + logging::check_fatal(min_x >= max_x, "Invalid bounds for width with {:d} => {:d}", min_x, max_x); + logging::check_fatal(min_y >= max_y, "Invalid bounds for height with {:d} => {:d}", min_y, max_y); #ifdef DEBUG_GRIDS logging::debug( - "Bounds are ({:d}, {:d}), ({:d}, {:d}) after correction", - min_column, - min_row, - max_column, - max_row + "Bounds are ({:d}, {:d}), ({:d}, {:d}) after correction", min_x, min_y, max_x, max_y ); #endif - logging::extensive("({:d}, {:d}) => ({:d}, {:d})", min_column, min_row, max_column, max_row); - logging::check_fatal((max_row - min_row) % tileHeight != 0, "Invalid start and end rows"); - logging::check_fatal( - (max_column - min_column) % tileHeight != 0, "Invalid start and end columns" - ); - logging::extensive("Lower left corner is ({:d}, {:d})", min_column, min_row); - logging::extensive("Upper right corner is ({:d}, {:d})", max_column, max_row); - const MathSize xll = grid.xllcorner() + min_column * grid.cellSize(); + logging::extensive("({:d}, {:d}) => ({:d}, {:d})", min_x, min_y, max_x, max_y); + logging::check_fatal((max_x - min_x) % tileWidth != 0, "Invalid start and end x"); + logging::check_fatal((max_y - min_y) % tileHeight != 0, "Invalid start and end y"); + logging::extensive("Lower left corner is ({:d}, {:d})", min_x, min_y); + logging::extensive("Upper right corner is ({:d}, {:d})", max_x, max_y); + const MathSize xll = grid.xllcorner() + min_x * grid.cellSize(); // offset is different for y since it's flipped - const MathSize yll = grid.yllcorner() + (min_row)*grid.cellSize(); + const MathSize yll = grid.yllcorner() + (min_y)*grid.cellSize(); logging::extensive("Lower left corner is ({:f}, {:f})", xll, yll); - const auto num_rows = static_cast(max_row - min_row); - const auto num_columns = static_cast(max_column - min_column); + const auto height_calc = static_cast(max_y - min_y); + const auto width_calc = static_cast(max_x - min_x); // ensure this is always divisible by tile size - logging::check_fatal(0 != (num_rows % tileWidth), "{:d} rows not divisible by tiles", num_rows); logging::check_fatal( - 0 != (num_columns % tileHeight), "{:d} columns not divisible by tiles", num_columns + 0 != (height_calc % tileWidth), "Height {:d} not divisible by tiles", height_calc + ); + logging::check_fatal( + 0 != (width_calc % tileHeight), "Width {:d} not divisible by tiles", width_calc ); const auto filename = create_file_name(dir, base_name, "tif"); GeoTiff geotiff{filename, "w"}; @@ -108,7 +95,7 @@ string saveToTiffFile( auto gtif = geotiff.gtif(); logging::check_fatal(!gtif, "Cannot open file {:s} as a GEOTIFF", filename); const double xul = xll; - const double yul = grid.yllcorner() + (grid.cellSize() * max_row); + const double yul = grid.yllcorner() + (grid.cellSize() * max_y); double tiePoints[6] = {0.0, 0.0, 0.0, xul, yul, 0.0}; double pixelScale[3] = {grid.cellSize(), grid.cellSize(), 0.0}; // make sure to use floating point if values are @@ -127,8 +114,8 @@ string saveToTiffFile( ); TIFFSetField(tif, TIFFTAG_GDAL_NODATA, nodata_str.c_str()); logging::extensive("{:s} takes {:d} bits", base_name, bits_per_sample); - TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, num_columns); - TIFFSetField(tif, TIFFTAG_IMAGELENGTH, num_rows); + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width_calc); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height_calc); TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits_per_sample); TIFFSetField(tif, TIFFTAG_TILEWIDTH, tileWidth); @@ -144,30 +131,30 @@ string saveToTiffFile( logging::extensive("{:s} has buffer size {:d}", base_name, buf_size); // HACK: using R means size changes on different cpu types auto buf = static_cast(_TIFFmalloc(buf_size)); - for (uint32_t co = 0; co < num_columns; co += tileWidth) + for (uint32_t x0 = 0; x0 < width_calc; x0 += tileWidth) { - for (uint32_t ro = 0; ro < num_rows; ro += tileHeight) + for (uint32_t y0 = 0; y0 < height_calc; y0 += tileHeight) { std::fill_n(&buf[0], tileWidth * tileHeight, static_cast(nodata_as_int)); // NOTE: shouldn't need to check if writing outside of tile because we made bounds on tile // edges above need to put data from grid into buffer, but flipped vertically - for (uint32_t x = 0; x < tileWidth; ++x) + for (uint32_t x1 = 0; x1 < tileWidth; ++x1) { - for (uint32_t y = 0; y < tileHeight; ++y) + for (uint32_t y1 = 0; y1 < tileHeight; ++y1) { - const Idx r = static_cast(max_row) - (ro + y + 1); - const Idx c = static_cast(min_column) + co + x; - const Location idx(r, c); + const Idx y2 = static_cast(max_y) - (y0 + y1 + 1); + const Idx x2 = static_cast(min_x) + x0 + x1; + const XYIdx idx{x2, y2}; // might be out of bounds if not divisible by number of tiles - if (!(rows <= r || 0 > r || columns <= c || 0 > c)) + if (!(height <= y2 || 0 > y2 || width <= x2 || 0 > x2)) { // HACK: was getting invalid rasters if assigning directly into buf const R value = value_at(idx); - buf[x + y * tileWidth] = value; + buf[x1 + y1 * tileWidth] = value; } } } - const auto write_result = TIFFWriteTile(tif, buf, co, ro, 0, 0); + const auto write_result = TIFFWriteTile(tif, buf, x0, y0, 0, 0); logging::check_fatal(write_result < 0, "Cannot write tile to {:s}", filename); } } @@ -176,15 +163,15 @@ string saveToTiffFile( return filename; } string GridBase::saveToTiffFileInt( - const Idx columns, - const Idx rows, + const Idx width, + const Idx height, const tuple bounds, const string_view dir, const string_view base_name, const uint16_t bits_per_sample, // const uint16_t sample_format, const bool is_unsigned, - std::function value_at, + std::function value_at, const int nodata_as_int ) const { @@ -206,14 +193,14 @@ string GridBase::saveToTiffFileInt( { return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -221,14 +208,14 @@ string GridBase::saveToTiffFileInt( { return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -236,14 +223,14 @@ string GridBase::saveToTiffFileInt( { return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -255,14 +242,14 @@ string GridBase::saveToTiffFileInt( { return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -270,14 +257,14 @@ string GridBase::saveToTiffFileInt( { return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -285,14 +272,14 @@ string GridBase::saveToTiffFileInt( { return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -304,14 +291,14 @@ string GridBase::saveToTiffFileInt( )); } string GridBase::saveToTiffFileFloat( - const Idx columns, - const Idx rows, + const Idx width, + const Idx height, const tuple bounds, const string_view dir, const string_view base_name, // const uint16_t bits_per_sample, // const uint16_t sample_format, - std::function value_at, + std::function value_at, const int nodata_as_int ) const { @@ -320,14 +307,14 @@ string GridBase::saveToTiffFileFloat( constexpr auto sample_format = SAMPLEFORMAT_IEEEFP; return saveToTiffFile( *this, - columns, - rows, + width, + height, bounds, dir, base_name, bits_per_sample, sample_format, - [&](Location idx) { return static_cast(value_at(idx)); }, + [&](const XYIdx& idx) { return static_cast(value_at(idx)); }, nodata_as_int ); } @@ -382,18 +369,18 @@ void GridBase::createPrj(const string_view dir, const string_view base_name) con out << std::format("{:0.1f} /* false northing (meters)\n", stod(y_0)); out.close(); } -unique_ptr GridBase::findCoordinates(const Point& point, const bool flipped) const +std::optional GridBase::findCoordinates(const Point& point, const bool flipped) const { auto full = findFullCoordinates(point, flipped); - return make_unique( - static_cast(std::get<0>(*full)), - static_cast(std::get<1>(*full)), - std::get<2>(*full), - std::get<3>(*full) - ); + if (!full.has_value()) + { + return {}; + } + return Coordinates{ + static_cast(full->x), static_cast(full->y), full->x_sub, full->y_sub + }; } -// Use pair instead of Location, so we can go above max columns & rows -unique_ptr GridBase::findFullCoordinates(const Point& point, const bool flipped) +std::optional GridBase::findFullCoordinates(const Point& point, const bool flipped) const { MathSize x; @@ -427,7 +414,7 @@ unique_ptr GridBase::findFullCoordinates(const Point& point, co deviation, MAX_DEVIATION ); - return nullptr; + return {}; } else if (abs(deviation * 10) > MAX_DEVIATION) { @@ -443,37 +430,35 @@ unique_ptr GridBase::findFullCoordinates(const Point& point, co // these are already flipped across the y-axis on reading, so it's the same as for x now auto actual_y = (!flipped) ? (y - this->yllcorner_) / this->cell_size_ : (yurcorner_ - y) / cell_size_; - const auto column = static_cast(actual_x); - const auto row = static_cast(round(actual_y - 0.5)); - if (0 > column || column >= calculateColumns() || 0 > row || row >= calculateRows()) + const auto x1 = static_cast(actual_x); + const auto y1 = static_cast(round(actual_y - 0.5)); + if (0 > x1 || x1 >= calculateWidth() || 0 > y1 || y1 >= calculateHeight()) { logging::verbose( "Returning nullptr from findFullCoordinates() for ({:f}, {:f}) => ({:d}, {:d})", actual_x, actual_y, - column, - row + x1, + y1 ); - return nullptr; + return {}; } - const auto sub_x = static_cast((actual_x - column) * 1000); - const auto sub_y = static_cast((actual_y - row) * 1000); - return make_unique( - static_cast(row), static_cast(column), sub_x, sub_y - ); + const auto sub_x = static_cast((actual_x - x1) * 1000); + const auto sub_y = static_cast((actual_y - y1) * 1000); + return FullCoordinates{static_cast(x1), static_cast(y1), sub_x, sub_y}; } void write_ascii_header( ofstream& out, - const MathSize num_columns, - const MathSize num_rows, + const MathSize width, + const MathSize height, const MathSize xll, const MathSize yll, const MathSize cell_size, const MathSize no_data ) { - out << "ncols " << num_columns << "\n"; - out << "nrows " << num_rows << "\n"; + out << "ncols " << width << "\n"; + out << "nrows " << height << "\n"; out << "xllcorner " << fixed << setprecision(6) << xll << "\n"; out << "yllcorner " << fixed << setprecision(6) << yll << "\n"; out << "cellsize " << cell_size << "\n"; @@ -486,12 +471,12 @@ void write_ascii_header( GTIFDefn definition; if (GTIFGetDefn(geotiff.gtif(), &definition)) { - uint32_t columns; - uint32_t rows; - TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &columns); - TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &rows); + uint32_t width; + uint32_t height; + TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width); + TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height); double x = 0.0; - double y = rows; + double y = height; logging::check_fatal( !GTIFImageToPCS(gtif, &x, &y), "Unable to translate image to PCS coordinates." ); @@ -525,8 +510,8 @@ void write_ascii_header( GTIFGetProj4Defn(&definition), std::free }; auto proj4 = string(proj4_char.get()); - const auto xurcorner = xllcorner + cell_width * columns; - const auto yurcorner = yllcorner + cell_width * rows; + const auto xurcorner = xllcorner + cell_width * width; + const auto yurcorner = yllcorner + cell_width * height; return {cell_width, xllcorner, yllcorner, xurcorner, yurcorner, string(proj4)}; } throw runtime_error("Cannot read TIFF header"); diff --git a/src/cpp/fs/Grid.h b/src/cpp/fs/Grid.h index 38fdd3a7f26..62fc6e8554d 100644 --- a/src/cpp/fs/Grid.h +++ b/src/cpp/fs/Grid.h @@ -7,27 +7,9 @@ #include "Point.h" #include "Settings.h" #include "tiff.h" -/** - * \brief Provides hash function for Location. - */ -template <> -struct std::hash -{ - /** - * \brief Get hash value for a Location - * \param location Location to get value for - * \return Hash value for a Location - */ - [[nodiscard]] constexpr fs::HashSize operator()(const fs::Location& location) const noexcept - { - return location.hash(); - } -}; namespace fs { using namespace logging; -using fs::Location; -using fs::Position; using NodataIntType = int64_t; string create_file_name( const string_view dir, @@ -50,19 +32,11 @@ class GridBase * \return Cell height and width in metres. */ [[nodiscard]] constexpr MathSize cellSize() const noexcept { return cell_size_; } - /** - * \brief Number of rows in the GridBase. - * \return Number of rows in the GridBase. - */ - [[nodiscard]] constexpr FullIdx calculateRows() const noexcept + [[nodiscard]] constexpr FullIdx calculateHeight() const noexcept { return static_cast((yurcorner() - yllcorner()) / cellSize()) - 1; } - /** - * \brief Number of columns in the GridBase. - * \return Number of columns in the GridBase. - */ - [[nodiscard]] constexpr FullIdx calculateColumns() const noexcept + [[nodiscard]] constexpr FullIdx calculateWidth() const noexcept { return static_cast((xurcorner() - xllcorner()) / cellSize()) - 1; } @@ -121,35 +95,35 @@ class GridBase * \param flipped Whether or not Grid data is flipped along x axis * \return Coordinates for Point translated to Grid */ - [[nodiscard]] unique_ptr findCoordinates(const Point& point, bool flipped) const; + [[nodiscard]] std::optional findCoordinates(const Point& point, bool flipped) const; /** * \brief Find FullCoordinates for Point * \param point Point to translate to Grid Coordinate * \param flipped Whether or not Grid data is flipped along x axis * \return Coordinates for Point translated to Grid */ - [[nodiscard]] unique_ptr findFullCoordinates(const Point& point, bool flipped) + [[nodiscard]] std::optional findFullCoordinates(const Point& point, bool flipped) const; string saveToTiffFileInt( - const Idx columns, - const Idx rows, + const Idx width, + const Idx height, const tuple bounds, const string_view dir, const string_view base_name, const uint16_t bits_per_sample, const bool is_unsigned, - std::function value_at, + std::function value_at, const int nodata_as_int ) const; string saveToTiffFileFloat( - const Idx columns, - const Idx rows, + const Idx width, + const Idx height, const tuple bounds, const string_view dir, const string_view base_name, // const uint16_t bits_per_sample, // const uint16_t sample_format, - std::function value_at, + std::function value_at, const int nodata_as_int ) const; @@ -181,8 +155,8 @@ class GridBase }; void write_ascii_header( ofstream& out, - MathSize num_columns, - MathSize num_rows, + MathSize width, + MathSize height, MathSize xll, MathSize yll, MathSize cell_size, @@ -199,16 +173,8 @@ template class Grid : public GridBase { public: - /** - * \brief Number of rows in the GridBase. - * \return Number of rows in the GridBase. - */ - [[nodiscard]] constexpr Idx rows() const noexcept { return rows_; } - /** - * \brief Number of columns in the GridBase. - * \return Number of columns in the GridBase. - */ - [[nodiscard]] constexpr Idx columns() const noexcept { return columns_; } + [[nodiscard]] constexpr Idx height() const noexcept { return height_; } + [[nodiscard]] constexpr Idx width() const noexcept { return width_; } /** * \brief Value used for grid locations that have no data. * \return Value used for grid locations that have no data. @@ -222,49 +188,16 @@ class Grid : public GridBase { return nodata_value_; } // NOTE: only use this for simple types because it's returning by value - /** - * \brief Value for grid at given Location. - * \param location Location to get value for. - * \return Value at grid Location. - */ - [[nodiscard]] virtual T at(const Location& location) const = 0; - template - [[nodiscard]] T at(const Position

& position) const - { - return at(Location{position.hash()}); - } + [[nodiscard]] virtual T at(const XYIdx& location) const = 0; // NOTE: use set instead of at to avoid issues with bool - /** - * \brief Set value for grid at given Location. - * \param location Location to set value for. - * \param value Value to set at grid Location. - * \return None - */ - virtual void set(const Location& location, T value) = 0; - template - void set(const Position

& position, const T value) - { - set(Location{position.hash()}, value); - } + virtual void set(const XYIdx& location, T value) = 0; protected: Grid() = default; - /** - * \brief Constructor - * \param cell_size Cell width and height (m) - * \param rows Number of rows - * \param columns Number of columns - * \param nodata_input Value that represents no data for type V - * \param nodata_value Value that represents no data for type T - * \param nodata Integer value that represents no data - * \param xllcorner Lower left corner X coordinate (m) - * \param yllcorner Lower left corner Y coordinate (m) - * \param proj4 Proj4 projection definition - */ Grid( const MathSize cell_size, - const Idx rows, - const Idx columns, + const Idx width, + const Idx height, const V nodata_input, const T nodata_value, const MathSize xllcorner, @@ -274,13 +207,11 @@ class Grid : public GridBase const string_view proj4 ) noexcept : GridBase(cell_size, xllcorner, yllcorner, xurcorner, yurcorner, proj4), - nodata_input_(nodata_input), nodata_value_(nodata_value), rows_(rows), columns_(columns) + nodata_input_(nodata_input), nodata_value_(nodata_value), height_(height), width_(width) { #ifdef DEBUG_GRIDS - logging::check_fatal(rows > MAX_ROWS, "Too many rows ({:d} > {:d})", rows, MAX_ROWS); - logging::check_fatal( - columns > MAX_COLUMNS, "Too many columns ({:d} > {:d})", columns, MAX_COLUMNS - ); + logging::check_fatal(height > MAX_HEIGHT, "Height too large ({:d} > {:d})", height, MAX_HEIGHT); + logging::check_fatal(width > MAX_WIDTH, "Width too large ({:d} > {:d})", width, MAX_WIDTH); #endif #ifdef DEBUG_GRIDS // enforce converting to an int and back produces same V @@ -300,8 +231,8 @@ class Grid : public GridBase Grid(const GridBase& grid_info, V no_data) noexcept : Grid( grid_info.cellSize(), - static_cast(grid_info.calculateRows()), - static_cast(grid_info.calculateColumns()), + static_cast(grid_info.calculateHeight()), + static_cast(grid_info.calculateWidth()), no_data, to_string(no_data), grid_info.xllcorner(), @@ -321,14 +252,8 @@ class Grid : public GridBase * \brief Value to use for representing no data at a Location. */ T nodata_value_; - /** - * \brief Number of rows in the grid. - */ - Idx rows_{}; - /** - * \brief Number of columns in the grid. - */ - Idx columns_{}; + Idx height_{}; + Idx width_{}; }; /** * \brief A Grid that defines the data structure used for storing values. @@ -341,24 +266,10 @@ class GridData : public Grid { public: using Grid::Grid; - /** - * \brief Constructor - * \param cell_size Cell width and height (m) - * \param rows Number of rows - * \param columns Number of columns - * \param nodata_input Value that represents no data for type V - * \param nodata_value Value that represents no data for type T - * \param xllcorner Lower left corner X coordinate (m) - * \param yllcorner Lower left corner Y coordinate (m) - * \param xurcorner Upper right corner X coordinate (m) - * \param yurcorner Upper right corner Y coordinate (m) - * \param proj4 Proj4 projection definition - * \param data Data to populate GridData with - */ GridData( const MathSize cell_size, - const Idx rows, - const Idx columns, + const Idx width, + const Idx height, const V nodata_input, const T nodata_value, const MathSize xllcorner, @@ -370,8 +281,8 @@ class GridData : public Grid ) : Grid( cell_size, - rows, - columns, + width, + height, nodata_input, nodata_value, xllcorner, @@ -432,38 +343,37 @@ class GridData : public Grid const R no_data ) const { - tuple bounds = dataBounds(); - auto min_column = std::get<0>(bounds); - auto min_row = std::get<1>(bounds); - auto max_column = std::get<2>(bounds); - auto max_row = std::get<3>(bounds); + const auto [min_x, min_y, max_x, max_y] = dataBounds(); + // auto bounds = dataBounds(); + // auto min_x = std::get<0>(bounds); + // auto min_y = std::get<1>(bounds); + // auto max_x = std::get<2>(bounds); + // auto max_y = std::get<3>(bounds); #ifdef DEBUG_GRIDS - logging::debug( - "Bounds are ({:d}, {:d}), ({:d}, {:d})", min_column, min_row, max_column, max_row - ); + logging::debug("Bounds are ({:d}, {:d}), ({:d}, {:d})", min_x, min_y, max_x, max_y); #endif - logging::extensive("Lower left corner is ({:d}, {:d})", min_column, min_row); - logging::extensive("Upper right corner is ({:d}, {:d})", max_column, max_row); - const MathSize xll = this->xllcorner() + min_column * this->cellSize(); + logging::extensive("Lower left corner is ({:d}, {:d})", min_x, min_y); + logging::extensive("Upper right corner is ({:d}, {:d})", max_x, max_y); + const MathSize xll = this->xllcorner() + min_x * this->cellSize(); // offset is different for y since it's flipped - const MathSize yll = this->yllcorner() + (min_row) * this->cellSize(); + const MathSize yll = this->yllcorner() + (min_y) * this->cellSize(); logging::extensive("Lower left corner is ({:f}, {:f})", xll, yll); // HACK: make sure it's always at least 1 - const auto num_rows = static_cast(max_row) - min_row + 1; - const auto num_columns = static_cast(max_column) - min_column + 1; + const auto height_calc = static_cast(max_y) - min_y + 1; + const auto width_calc = static_cast(max_x) - min_x + 1; const auto filename = create_file_name(dir, base_name, "asc"); ofstream out{filename}; write_ascii_header( - out, num_columns, num_rows, xll, yll, this->cellSize(), static_cast(no_data) + out, width_calc, height_calc, xll, yll, this->cellSize(), static_cast(no_data) ); - for (Idx ro = 0; ro < num_rows; ++ro) + for (Idx y0 = 0; y0 < height_calc; ++y0) { // HACK: do this so that we always get at least one pixel in output // need to output in reverse order since (0,0) is bottom left - const Idx r = static_cast(max_row) - ro; - for (Idx co = 0; co < num_columns; ++co) + const Idx y1 = static_cast(max_y) - y0; + for (Idx x0 = 0; x0 < width_calc; ++x0) { - const Location idx(static_cast(r), static_cast(min_column + co)); + const XYIdx idx{static_cast(min_x + x0), static_cast(y1)}; // HACK: use + here so that it gets promoted to a printable number // prevents char type being output as characters out << +(convert(this->at(idx))) << " "; @@ -501,14 +411,14 @@ class GridData : public Grid // static_cast(no_data), static_cast(this->nodataInput()), "integer nodata" // ); return GridBase::saveToTiffFileFloat( - this->columns(), - this->rows(), + this->width(), + this->height(), this->dataBounds(), dir, base_name, // bps, // SAMPLEFORMAT_IEEEFP, - [&](Location idx) { return static_cast(convert(this->at(idx))); }, + [&](const XYIdx& idx) { return static_cast(convert(this->at(idx))); }, no_data ); } @@ -528,14 +438,14 @@ class GridData : public Grid // static_cast(no_data), static_cast(this->nodataInput()), "integer nodata" // ); return GridBase::saveToTiffFileInt( - this->columns(), - this->rows(), + this->width(), + this->height(), this->dataBounds(), dir, base_name, bps, std::is_unsigned_v, - [&](Location idx) { return static_cast(convert(this->at(idx))); }, + [&](const XYIdx& idx) { return static_cast(convert(this->at(idx))); }, no_data ); } diff --git a/src/cpp/fs/GridMap.h b/src/cpp/fs/GridMap.h index 4a42ebf7fe9..d16507546de 100644 --- a/src/cpp/fs/GridMap.h +++ b/src/cpp/fs/GridMap.h @@ -3,6 +3,7 @@ #define FS_GRIDMAP_H #include "stdafx.h" #include "Grid.h" +#include "Location.h" namespace fs { /** @@ -11,29 +12,14 @@ namespace fs * \tparam V Type of data used as an input when initializing. */ template -class GridMap : public GridData> +class GridMap : public GridData> { public: - /** - * \brief Determine if Location has a value - * \param location Location to determine if present in GridMap - * \return Whether or not a value is present for the Location - */ - [[nodiscard]] bool contains(const Location& location) const + [[nodiscard]] bool contains(const XYIdx& location) const { return this->data.end() != this->data.find(location); } - template - [[nodiscard]] bool contains(const Position

& position) const - { - return contains(Location{position.hash()}); - } - /** - * \brief Retrieve value at Location - * \param location Location to get value for - * \return Value at Location - */ - [[nodiscard]] T at(const Location& location) const override + [[nodiscard]] T at(const XYIdx& location) const override { const auto value = this->data.find(location); if (value == this->data.end()) @@ -42,45 +28,22 @@ class GridMap : public GridData> } return get<1>(*value); } - template - [[nodiscard]] T at(const Position

& position) const - { - return at(Location{position.hash()}); - } /** * \brief Set value at Location * \param location Location to set value for * \param value Value to set at Location */ - void set(const Location& location, const T value) override + void set(const XYIdx& location, const T value) override { this->data[location] = value; assert(at(location) == value); } - template - void set(const Position

& position, const T value) - { - return set(Location{position.hash()}, value); - } GridMap() noexcept = default; ~GridMap() override = default; - /** - * \brief Constructor - * \param cell_size Cell width and height (m) - * \param rows Number of rows - * \param columns Number of columns - * \param no_data Value that represents no data - * \param nodata Integer value that represents no data - * \param xllcorner Lower left corner X coordinate (m) - * \param yllcorner Lower left corner Y coordinate (m) - * \param xllcorner Upper right corner X coordinate (m) - * \param yllcorner Upper right corner Y coordinate (m) - * \param proj4 Proj4 projection definition - */ GridMap( const MathSize cell_size, - const Idx rows, - const Idx columns, + const Idx width, + const Idx height, T no_data, const int nodata, const MathSize xllcorner, @@ -89,10 +52,10 @@ class GridMap : public GridData> const MathSize yurcorner, const string_view proj4 ) - : GridData>( + : GridData>( cell_size, - rows, - columns, + width, + height, no_data, nodata, xllcorner, @@ -100,14 +63,14 @@ class GridMap : public GridData> xurcorner, yurcorner, proj4, - map() + map() ) { constexpr auto max_hash = numeric_limits::max(); // HACK: we don't want overflow errors, but we want to play with the hash size - const auto max_columns = static_cast(max_hash) / static_cast(this->rows()); + const auto MAX_WIDTH = static_cast(max_hash) / static_cast(this->height()); logging::check_fatal( - this->columns() >= max_columns, + this->width() >= MAX_WIDTH, "Grid is too big for cells to be hashed - " "recompile with a larger HashSize value" ); @@ -145,8 +108,8 @@ class GridMap : public GridData> GridMap(const GridBase& grid_info, T no_data) : GridMap( grid_info.cellSize(), - static_cast(grid_info.calculateRows()), - static_cast(grid_info.calculateColumns()), + static_cast(grid_info.calculateWidth()), + static_cast(grid_info.calculateHeight()), no_data, static_cast(no_data), grid_info.xllcorner(), @@ -156,11 +119,11 @@ class GridMap : public GridData> string(grid_info.proj4()) ) { } - GridMap(GridMap&& rhs) noexcept : GridData>(std::move(rhs)) + GridMap(GridMap&& rhs) noexcept : GridData>(std::move(rhs)) { this->data = std::move(rhs.data); } - GridMap(const GridMap& rhs) : GridData>(rhs) { this->data = rhs.data; } + GridMap(const GridMap& rhs) : GridData>(rhs) { this->data = rhs.data; } GridMap& operator=(GridMap&& rhs) noexcept { if (this != &rhs) @@ -185,30 +148,30 @@ class GridMap : public GridData> protected: tuple dataBounds() const override { - Idx min_row = this->rows(); - Idx max_row = 0; - Idx min_column = this->columns(); - Idx max_column = 0; + Idx min_y = this->height(); + Idx max_y = 0; + Idx min_x = this->width(); + Idx max_x = 0; for (const auto& kv : this->data) { - const Idx r = kv.first.row(); - const Idx c = kv.first.column(); - min_row = min(min_row, r); - max_row = max(max_row, r); - min_column = min(min_column, c); - max_column = max(max_column, c); + const Idx r = kv.first.y_value(); + const Idx c = kv.first.x_value(); + min_y = min(min_y, r); + max_y = max(max_y, r); + min_x = min(min_x, c); + max_x = max(max_x, c); } // do this so that we take the center point when there's no data since it should // stay the same if the grid is centered on the fire - if (min_row > max_row) + if (min_y > max_y) { - min_row = max_row = this->rows() / 2; + min_y = max_y = this->height() / 2; } - if (min_column > max_column) + if (min_x > max_x) { - min_column = max_column = this->columns() / 2; + min_x = max_x = this->width() / 2; } - return tuple{min_column, min_row, max_column, max_row}; + return {min_x, min_y, max_x, max_y}; } public: @@ -244,23 +207,24 @@ class GridMap : public GridData> * \brief Make a list of all Locations that are on the edge of cells with a value * \return A list of all Locations that are on the edge of cells with a value */ - [[nodiscard]] list makeEdge() const + [[nodiscard]] list makeEdge() const { - list edge{}; + list edge{}; for (const auto& kv : this->data) { auto loc = kv.first; + // FIX: any way to avoid decomposing? + auto [x_loc, y_loc] = hash_to_xy(loc); auto on_edge = false; for (Idx r = -1; !on_edge && r <= 1; ++r) { - const Idx row_index = loc.row() + r; - if (!(row_index < 0 || row_index >= this->rows())) + const Idx y = y_loc.value + r; + if (!(y < 0 || y >= this->height())) { for (Idx c = -1; !on_edge && c <= 1; ++c) { - const Idx col_index = loc.column() + c; - if (!(col_index < 0 || col_index >= this->columns()) - && this->data.find(Location(row_index, col_index)) == this->data.end()) + const Idx x = x_loc.value + c; + if (!(x < 0 || x >= this->width()) && this->data.find(XYIdx{x, y}) == this->data.end()) { on_edge = true; } @@ -282,14 +246,14 @@ class GridMap : public GridData> * \brief Make a list of all Locations that have a value * \return A list of all Locations that have a value */ - [[nodiscard]] list makeList() const + [[nodiscard]] list makeList() const { - list result{this->data.size()}; + list result{this->data.size()}; std::transform( this->data.begin(), this->data.end(), result.begin(), - [](const pair& kv) { return kv.first; } + [](const pair& kv) { return kv.first; } ); return result; } diff --git a/src/cpp/fs/InnerPos.h b/src/cpp/fs/InnerPos.h deleted file mode 100644 index b15d8bee41a..00000000000 --- a/src/cpp/fs/InnerPos.h +++ /dev/null @@ -1,80 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ -#ifndef FS_INNERPOS_H -#define FS_INNERPOS_H -#include "stdafx.h" -#include "Location.h" -#include "Weather.h" -namespace fs -{ -template -class BoundedPoint : public pair -{ -protected: - using class_type = BoundedPoint; - static constexpr auto INVALID_X = XMin - 1; - static constexpr auto INVALID_Y = YMin - 1; - -public: - using pair::pair; - constexpr BoundedPoint() noexcept : BoundedPoint(XMin - 1, YMin - 1) { } - constexpr S x() const { return std::get<0>(*this); } - constexpr S y() const { return std::get<1>(*this); } - /** - * \brief Add offset to position and return result - */ - template - [[nodiscard]] constexpr T add(const O& o) const noexcept - { - return static_cast(class_type(this->first + o.first, this->second + o.second)); - } -}; -/** - * \brief Offset from a position - */ -class Offset : public BoundedPoint -{ -public: - /** - * \brief Collection of Offsets - */ - using BoundedPoint::BoundedPoint; -}; -using ROSOffset = std::tuple; -using OffsetSet = vector; -/** - * \brief The position within a Cell that a spreading point has. - */ -class InnerPos : public BoundedPoint -{ - using BoundedPoint::BoundedPoint; -}; -/** - * \brief The position within the Environment that a spreading point has. - */ -class XYPos : public BoundedPoint -{ -public: - using BoundedPoint::BoundedPoint; - Location location() const - { - // HACK: Location is (row, column) and this is (x, y) - return {static_cast(second), static_cast(first)}; - } -}; -/** - * \brief The position within the Environment that a spreading point has. - */ -class CellPos : public BoundedPoint -{ -public: - using BoundedPoint::BoundedPoint; - Location location() const - { - // HACK: Location is (row, column) and this is (x, y) - return {static_cast(second), static_cast(first)}; - } -}; -static constexpr MathSize x(const auto& p) { return p.x(); } -static constexpr MathSize y(const auto& p) { return p.y(); } -} -#endif diff --git a/src/cpp/fs/IntensityMap.cpp b/src/cpp/fs/IntensityMap.cpp index bbcd6f1e607..d630e7a186b 100644 --- a/src/cpp/fs/IntensityMap.cpp +++ b/src/cpp/fs/IntensityMap.cpp @@ -1,16 +1,32 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ #include "stdafx.h" #include "IntensityMap.h" +#include "Location.h" #include "Model.h" #include "Perimeter.h" #include "unstable.h" +#include "Util.h" #include "Weather.h" namespace fs { +template +std::optional> make_if_saving(const Model& model) +{ + // HACK: resolve once and fail if not set already + static auto& settings = fs::settings::instance(); + const auto calc_fi_ros_raz = settings.save_individual || settings.save_intensity; + if (calc_fi_ros_raz) + { + return model.environment().makeMap(false); + } + return {}; +} IntensityMap::IntensityMap(const Model& model) noexcept - : model_(model), intensity_max_(model.environment().makeMap(false)), - rate_of_spread_at_max_(model.environment().makeMap(false)), - direction_of_spread_at_max_(model.environment().makeMap(false)), + : model_{model}, + // HACK: always assign this so we can use the iterator + intensity_max_{model.environment().makeMap(false)}, + rate_of_spread_at_max_{make_if_saving(model)}, + direction_of_spread_at_max_{make_if_saving(model)}, is_burned_{model.environment().unburnable()} { } IntensityMap::IntensityMap(const IntensityMap& rhs) @@ -25,7 +41,7 @@ IntensityMap::IntensityMap(const IntensityMap& rhs) void IntensityMap::applyPerimeter(const Perimeter& perimeter) noexcept { std::for_each( -#ifndef __APPLE__ +#if !defined(__APPLE__) || !defined(__clang__) // apple clang doesn't support this? std::execution::par_unseq, #endif @@ -34,28 +50,28 @@ void IntensityMap::applyPerimeter(const Perimeter& perimeter) noexcept [this](const auto& location) { ignite(location); } ); } -bool IntensityMap::canBurn(const Location& location) const { return !hasBurned(location); } -bool IntensityMap::hasBurned(const Location& location) const +bool IntensityMap::canBurn(const XYIdx& location) const { return !hasBurned(location); } +bool IntensityMap::hasBurned(const XYIdx& location) const { lock_guard lock(mutex_); - return is_burned_.at(location.hash()); + return is_burned_.at(location); } -bool IntensityMap::isSurrounded(const Location& location) const +bool IntensityMap::isSurrounded(const XYIdx& location) const { // implement here so we can just lock once lock_guard lock(mutex_); - const auto x = location.column(); - const auto y = location.row(); - const auto min_row = static_cast(max(y - 1, 0)); - const auto max_row = min(y + 1, this->rows() - 1); - const auto min_column = static_cast(max(x - 1, 0)); - const auto max_column = min(x + 1, this->columns() - 1); - for (auto r = min_row; r <= max_row; ++r) + // FIX: same logic as makeEdge() + const auto [x0, y0] = hash_to_xy_value(location); + const auto min_y = static_cast(max(y0 - 1, 0)); + const auto max_y = min(y0 + 1, this->height() - 1); + const auto min_x = static_cast(max(x0 - 1, 0)); + const auto max_x = min(x0 + 1, this->width() - 1); + for (auto y1 = min_y; y1 <= max_y; ++y1) { - for (auto c = min_column; c <= max_column; ++c) + for (auto x1 = min_x; x1 <= max_x; ++x1) { // actually check x, y too since we care if the cell itself is burned - if (!is_burned_.at(Location(r, c).hash())) + if (!is_burned_.at(XYIdx{x1, y1})) { return false; } @@ -63,43 +79,57 @@ bool IntensityMap::isSurrounded(const Location& location) const } return true; } -void IntensityMap::ignite(const Location& location) -{ - burn(location, 1, 0, fs::Direction::Invalid()); -} +void IntensityMap::ignite(const XYIdx& location) { burn(location, 1, 0, fs::Direction::Invalid()); } void IntensityMap::burn( - const Location& location, + const XYIdx& location, IntensitySize intensity, MathSize ros, fs::Direction raz ) { + // HACK: resolve once and fail if not set already + static auto& settings = fs::settings::instance(); lock_guard lock(mutex_); - if (!is_burned_.at(location.hash())) + if (!is_burned_.at(location)) { intensity_max_.set(location, intensity); - rate_of_spread_at_max_.set(location, ros); - direction_of_spread_at_max_.set(location, raz.asDegreesSize()); - is_burned_.set(location.hash()); + if (rate_of_spread_at_max_.has_value()) + { + rate_of_spread_at_max_->set(location, ros); + direction_of_spread_at_max_->set(location, raz.asDegreesSize()); + } + is_burned_.set(location); } - else + // HACK: don't bother updating intensity if not saving + else if (settings.save_intensity) { const auto intensity_old = intensity_max_.at(location); if (intensity_old < intensity) { intensity_max_.set(location, intensity); } - // update ros and direction if higher ros - const auto ros_old = rate_of_spread_at_max_.at(location); - if (ros_old < ros) + if (rate_of_spread_at_max_.has_value()) { - rate_of_spread_at_max_.set(location, ros); - direction_of_spread_at_max_.set(location, raz.asDegreesSize()); + // update ros and direction if higher ros + const auto ros_old = rate_of_spread_at_max_->at(location); + if (ros_old < ros) + { + rate_of_spread_at_max_->set(location, ros); + direction_of_spread_at_max_->set(location, raz.asDegreesSize()); + } } } } FileList IntensityMap::save(const string_view dir, const string_view base_name) const { + // HACK: resolve once and fail if not set already + static auto& settings = fs::settings::instance(); + // HACK: intensity_max_ will have values so we can iterate for probability grid, but we don't want + // to save intensity grids + if (!settings.save_intensity) + { + return FileList{}; + } lock_guard lock(mutex_); const auto name_intensity = string(base_name) + "_intensity"; const auto name_ros = string(base_name) + "_ros"; @@ -108,20 +138,15 @@ FileList IntensityMap::save(const string_view dir, const string_view base_name) auto files = intensity_max_.saveToFile(dir, name_intensity); // // HACK: writing a double to a tiff seems to not work? // double is way too much precision for outputs - files.append_range(rate_of_spread_at_max_.saveToFile(dir, name_ros)); - files.append_range(direction_of_spread_at_max_.saveToFile(dir, name_raz)); + files.append_range(rate_of_spread_at_max_->saveToFile(dir, name_ros)); + files.append_range(direction_of_spread_at_max_->saveToFile(dir, name_raz)); return files; } -MathSize IntensityMap::fireSize() const -{ - lock_guard lock(mutex_); - return intensity_max_.fireSize(); -} -map::const_iterator IntensityMap::cend() const noexcept +map::const_iterator IntensityMap::cend() const noexcept { return intensity_max_.data.cend(); } -map::const_iterator IntensityMap::cbegin() const noexcept +map::const_iterator IntensityMap::cbegin() const noexcept { return intensity_max_.data.cbegin(); } diff --git a/src/cpp/fs/IntensityMap.h b/src/cpp/fs/IntensityMap.h index dbf7e632f05..6abb829b7e7 100644 --- a/src/cpp/fs/IntensityMap.h +++ b/src/cpp/fs/IntensityMap.h @@ -32,76 +32,19 @@ class IntensityMap IntensityMap(IntensityMap&& rhs) = delete; IntensityMap& operator=(const IntensityMap& rhs) = delete; IntensityMap& operator=(IntensityMap&& rhs) noexcept = delete; - /** - * \brief Number of rows in this extent - * \return Number of rows in this extent - */ - [[nodiscard]] Idx rows() const { return intensity_max_.rows(); } - /** - * \brief Number of columns in this extent - * \return Number of columns in this extent - */ - [[nodiscard]] Idx columns() const { return intensity_max_.columns(); } + [[nodiscard]] Idx height() const { return is_burned_.height(); } + [[nodiscard]] Idx width() const { return is_burned_.width(); } /** * \brief Set cells in the map to be burned based on Perimeter * \param perimeter Perimeter to burn cells based on */ void applyPerimeter(const Perimeter& perimeter) noexcept; - /** - * \brief Whether or not the Cell with the given hash can burn - * \param hash Hash for Cell to check - * \return Whether or not the Cell with the given hash can burn - */ - [[nodiscard]] bool canBurn(const Location& location) const; - template - [[nodiscard]] bool canBurn(const Position

& position) const - { - return canBurn(Location{position.hash()}); - } - /** - * \brief Whether or not the Location with the given hash can burn - * \param hash Hash for Location to check - * \return Whether or not the Location with the given hash can burn - */ - [[nodiscard]] bool hasBurned(const Location& location) const; - template - [[nodiscard]] bool hasBurned(const Position

& position) const - { - return hasBurned(Location{position.hash()}); - } - /** - * \brief Whether or not all Locations surrounding the given Location are burned - * \param location Location to check - * \return Whether or not all Locations surrounding the given Location are burned - */ - [[nodiscard]] bool isSurrounded(const Location& location) const; - template - [[nodiscard]] bool isSurrounded(const Position

& position) const - { - return isSurrounded(Location{position.hash()}); - } - /** - * \brief Mark given location as burned - * \param location Location to burn - */ - void ignite(const Location& location); - template - void ignite(const Position

& position) - { - ignite(Location{position.hash()}); - } + [[nodiscard]] bool canBurn(const XYIdx& location) const; + [[nodiscard]] bool hasBurned(const XYIdx& location) const; + [[nodiscard]] bool isSurrounded(const XYIdx& location) const; + void ignite(const XYIdx& location); public: - template - void burn( - const Position

& position, - const IntensitySize intensity, - const MathSize ros, - const fs::Direction& raz - ) - { - burn(Location{position.hash()}, intensity, ros, raz); - } /** * \brief Update Location with specified values * \param location Location to burn @@ -109,7 +52,7 @@ class IntensityMap * \param ros Rate of spread to check against maximu (m/min) * \param raz Spread azimuth for ros */ - void burn(const Location& location, IntensitySize intensity, MathSize ros, fs::Direction raz); + void burn(const XYIdx& location, IntensitySize intensity, MathSize ros, fs::Direction raz); /** * \brief Save contents to file * \param dir Directory to save to @@ -121,36 +64,30 @@ class IntensityMap * \brief Size of the fire represented by this * \return Size of the fire represented by this */ - [[nodiscard]] MathSize fireSize() const; + [[nodiscard]] MathSize fireSize() const { return is_burned_.fireSize(); } /** * \brief Iterator for underlying GridMap * \return Iterator for underlying GridMap */ - [[nodiscard]] map::const_iterator cbegin() const noexcept; + [[nodiscard]] map::const_iterator cbegin() const noexcept; /** * \brief Iterator for underlying GridMap * \return Iterator for underlying GridMap */ - [[nodiscard]] map::const_iterator cend() const noexcept; + [[nodiscard]] map::const_iterator cend() const noexcept; private: /** * \brief Model map is for */ const Model& model_; - /** - * \brief Map of intensity that cells have burned at - */ + // Map of intensity that cells have burned at GridMap intensity_max_{}; // HACK: just add ROS/RAZ into this object for now - /** - * \brief Map of rate of spread/direction that cells have burned with at max ros - */ - GridMap rate_of_spread_at_max_{}; - GridMap direction_of_spread_at_max_{}; - /** - * \brief bitset denoting cells that can no longer burn - */ + // Map of rate of spread/direction that cells have burned with at max ros + std::optional> rate_of_spread_at_max_{}; + std::optional> direction_of_spread_at_max_{}; + // bitset denoting cells that can no longer burn BurnedData is_burned_{}; }; } diff --git a/src/cpp/fs/Iteration.cpp b/src/cpp/fs/Iteration.cpp index baf1baa09c3..95cbb069f41 100644 --- a/src/cpp/fs/Iteration.cpp +++ b/src/cpp/fs/Iteration.cpp @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ #include "Iteration.h" -#include "Cell.h" #include "Scenario.h" namespace fs { @@ -12,7 +11,7 @@ Iteration::~Iteration() } } Iteration::Iteration(vector scenarios) noexcept : scenarios_(std::move(scenarios)) { } -Iteration* Iteration::reset_with_new_start(const shared_ptr& start_cell) +Iteration* Iteration::reset_with_new_start(const XYIdx& start_cell) { // HACK: resolve once and fail if not set already static const auto& settings = fs::settings::instance(); diff --git a/src/cpp/fs/Iteration.h b/src/cpp/fs/Iteration.h index 269ac4985fd..20b207c0ebb 100644 --- a/src/cpp/fs/Iteration.h +++ b/src/cpp/fs/Iteration.h @@ -2,7 +2,7 @@ #ifndef FS_ITERATION_H #define FS_ITERATION_H #include "stdafx.h" -#include "Cell.h" +#include "Location.h" #include "SafeVector.h" namespace fs { @@ -29,7 +29,7 @@ class Iteration * \param start_cell Cell to start ignition in * \return This */ - Iteration* reset_with_new_start(const shared_ptr& start_cell); + Iteration* reset_with_new_start(const XYIdx& start_cell); /** * \brief Create new thresholds for use in each Scenario * \param mt_extinction Extinction thresholds diff --git a/src/cpp/fs/Location.cpp b/src/cpp/fs/Location.cpp index 983362f069d..8e288432dc0 100644 --- a/src/cpp/fs/Location.cpp +++ b/src/cpp/fs/Location.cpp @@ -2,19 +2,4 @@ #include "Location.h" namespace fs { -CellIndex relativeIndex(const Location& src, const Location& dst) noexcept -{ - static constexpr CellIndex DIRECTIONS[9] = { - DIRECTION_SW, - DIRECTION_S, - DIRECTION_SE, - DIRECTION_W, - DIRECTION_NONE, - DIRECTION_E, - DIRECTION_NW, - DIRECTION_N, - DIRECTION_NE - }; - return DIRECTIONS[((src.column() - dst.column()) + 1) + 3 * ((src.row() - dst.row()) + 1)]; -} } diff --git a/src/cpp/fs/Location.h b/src/cpp/fs/Location.h index 6727cfb5f42..3f28119475a 100644 --- a/src/cpp/fs/Location.h +++ b/src/cpp/fs/Location.h @@ -2,284 +2,166 @@ #ifndef FS_LOCATION_H #define FS_LOCATION_H #include "stdafx.h" -#ifdef DEBUG_GRIDS -#include "Log.h" -#endif +#include "StrictType.h" #include "Util.h" namespace fs { -// have static versions of these outside Position so we can test with static_assert -/** - * \brief Create a hash from given values - * \param XYBits Number of bits to use for storing one coordinate of Position data - * \param row Row - * \param column Column - * \return Hash - */ -[[nodiscard]] static inline constexpr HashSize do_hash( - const uint32_t XYBits, - const Idx row, - const Idx column -) noexcept +constexpr auto CELL_CENTER = static_cast(0.5); +// Number of bits to use for storing one coordinate of Position data +static constexpr HashSize XYBits = std::bit_width(MAX_HEIGHT - 1); +static_assert(pow_int(2) == MAX_HEIGHT); +static_assert(pow_int(2) == MAX_WIDTH); +static constexpr HashSize ColumnMask = bit_mask(); +// // Number of bits to use for storing Position data +// static constexpr HashSize PositionBits = XYBits * 2; +// // Hash mask for bits being used for Position data +// static constexpr Topo HashMask = bit_mask(); +// static_assert(HashMask >= static_cast(MAX_WIDTH) * MAX_HEIGHT - 1); +// static_assert(HashMask <= std::numeric_limits::max()); +// HACK: use same invalid value as Idx versions +// decimal cell position in the X direction +struct XPos : public StrictType::max()> { - return (static_cast(row) << XYBits) + static_cast(column); -} -/** - * \brief Row from hash - * \param XYBits Number of bits to use for storing one coordinate of Position data - * \param hash hash to extract row from - * \return Row from hash - */ -[[nodiscard]] static inline constexpr Idx unhash_row( - const uint32_t XYBits, - const Topo hash -) noexcept + using StrictType::StrictType; + auto operator<=>(const XPos& rhs) const = default; +}; +// decimal cell position in the Y direction +struct YPos : public StrictType::max()> { - // don't need to use mask since bits just get shifted out - return static_cast(hash >> XYBits); -} -/** - * \brief Column - * \param ColumnMask Hash mask for bits being used for Position data - * \param hash hash to extract column from - * \return Column - */ -[[nodiscard]] static inline constexpr Idx unhash_column( - const Topo ColumnMask, - const Topo hash -) noexcept + using StrictType::StrictType; + auto operator<=>(const YPos& rhs) const = default; +}; +struct XIdx : public StrictType::max()> { - return static_cast(hash & ColumnMask); -} -/** - * \brief A Position with a row and column. - */ -template -class Position + using StrictType::StrictType; + explicit constexpr XIdx(const XPos& x) : XIdx(static_cast(x.value)) { } + auto operator<=>(const XIdx& rhs) const = default; +}; +struct YIdx : public StrictType::max()> +{ + using StrictType::StrictType; + explicit constexpr YIdx(const YPos& y) : YIdx(static_cast(y.value)) { } + auto operator<=>(const YIdx& rhs) const = default; +}; +// HACK: don't actually have bounds for now +// The position within the Environment that a spreading point has. +struct XYPos +{ + XPos x{}; + YPos y{}; + static constexpr XYPos Invalid() noexcept { return XYPos{XPos::Invalid(), YPos::Invalid()}; } + auto operator<=>(const XYPos& rhs) const = default; +}; +// a point that is within a set of min/max bounds +template +struct BoundedPoint; +// Offset from a position +using Offset = BoundedPoint; +// The position within the Environment that a spreading point has. +using CellPos = BoundedPoint; +template +struct BoundedPoint +{ + S x{XMin - 1}; + S y{YMin - 1}; + auto operator==(const BoundedPoint& rhs) const noexcept { return x == rhs.x && y == rhs.y; } + auto operator<=>(const BoundedPoint& rhs) const noexcept + { + if (auto cmp = x <=> rhs.x; 0 != cmp) + { + return cmp; + } + return y <=> rhs.y; + } + using class_type = BoundedPoint; + static constexpr auto INVALID_X = XMin - 1; + static constexpr auto INVALID_Y = YMin - 1; +}; +struct XYIdx { public: - Position() = default; - /** - * \brief Row - * \return Row - */ - [[nodiscard]] constexpr Idx row() const noexcept { return unhashRow(hash()); } - /** - * \brief Column - * \return Column - */ - [[nodiscard]] constexpr Idx column() const noexcept { return unhashColumn(hash()); } - /** - * \brief Hash derived from row and column - * \return Hash derived from row and column - */ - [[nodiscard]] constexpr HashSize hash() const noexcept + // access value for format + constexpr inline Idx x_value() const noexcept { return x.value; } + constexpr inline Idx y_value() const noexcept { return y.value; } + constexpr XYIdx() = default; + explicit constexpr XYIdx(const XIdx& x0, const YIdx& y0) : x{x0}, y{y0} { #ifdef DEBUG_POINTS - constexpr int num_bits = std::numeric_limits::digits; - constexpr Topo m = bit_mask(); - logging::check_equal( - static_cast(topo_data_), static_cast(m & topo_data_), "hash()" - ); + // HACK: don't use logging since breaks constexpr + if (-1 >= x.value || MAX_WIDTH <= x.value || -1 >= y.value || MAX_HEIGHT <= y.value) + { + cout << std::format("xy out of bounds: ({}, {})", x.value, y.value); + exit(-1); + } #endif - // can get away with just casting because all the other bits are outside this area - return static_cast(topo_data_); } - /** - * \brief Equality operator - * \param rhs Position to compare to - * \return Whether or not these are equivalent - */ - [[nodiscard]] constexpr bool operator==(const Position& rhs) const noexcept - { - return hash() == rhs.hash(); - } - /** - * \brief Inequality operator - * \param rhs Position to compare to - * \return Whether or not these are not equivalent - */ - [[nodiscard]] constexpr bool operator!=(const Position& rhs) const noexcept - { - return !(*this == rhs); - } - -protected: - /** - * \brief Stored hash that contains row and column data - */ - V topo_data_; - /** - * \brief Number of bits to use for storing one coordinate of Position data - */ - static constexpr uint32_t XYBits = std::bit_width(MAX_ROWS - 1); - static_assert(pow_int(2) == MAX_ROWS); - static_assert(pow_int(2) == MAX_COLUMNS); - /** - * \brief Number of bits to use for storing Position data - */ - static constexpr uint32_t PositionBits = XYBits * 2; - /** - * \brief Hash mask for bits being used for Position data - */ - static constexpr Topo ColumnMask = bit_mask(); - /** - * \brief Hash mask for bits being used for Position data - */ - static constexpr Topo HashMask = bit_mask(); - static_assert(HashMask >= static_cast(MAX_COLUMNS) * MAX_ROWS - 1); - static_assert(HashMask <= std::numeric_limits::max()); - /** - * \brief Construct with given hash that may contain data from subclasses - * \param topo Hash to store - */ - explicit constexpr Position(const Topo& topo) noexcept : topo_data_(static_cast(topo)) { } - /** - * \brief Create a hash from given values - * \param row Row - * \param column Column - * \return Hash - */ - [[nodiscard]] static constexpr HashSize doHash(const Idx row, const Idx column) noexcept + template + explicit constexpr XYIdx(const T& x0, const T& y0) + : XYIdx(XIdx{static_cast(x0)}, YIdx{static_cast(y0)}) + { } + template + explicit constexpr XYIdx(const BoundedPoint& pt) noexcept + : XYIdx(pt.x, pt.y) + { } + explicit constexpr XYIdx(const XYPos& xy) noexcept : XYIdx(XIdx{xy.x}, YIdx{xy.y}) { } + constexpr auto operator==(const XYIdx& rhs) const noexcept { return x == rhs.x && y == rhs.y; } + constexpr auto operator<=>(const XYIdx& rhs) const noexcept { - return do_hash(XYBits, row, column); -// make sure hashing/unhashing works -#define ROW_MIN 0 -#define ROW_MAX (MAX_ROWS - 1) -#define COL_MIN 0 -#define COL_MAX (MAX_COLUMNS - 1) - static_assert(ROW_MIN == unhash_row(XYBits, do_hash(XYBits, ROW_MIN, COL_MIN))); - static_assert(COL_MIN == unhash_column(ColumnMask, do_hash(XYBits, ROW_MIN, COL_MIN))); - static_assert(ROW_MIN == unhash_row(XYBits, do_hash(XYBits, ROW_MIN, COL_MAX))); - static_assert(COL_MAX == unhash_column(ColumnMask, do_hash(XYBits, ROW_MIN, COL_MAX))); - static_assert(ROW_MAX == unhash_row(XYBits, do_hash(XYBits, ROW_MAX, COL_MIN))); - static_assert(COL_MIN == unhash_column(ColumnMask, do_hash(XYBits, ROW_MAX, COL_MIN))); - static_assert(ROW_MAX == unhash_row(XYBits, do_hash(XYBits, ROW_MAX, COL_MAX))); - static_assert(COL_MAX == unhash_column(ColumnMask, do_hash(XYBits, ROW_MAX, COL_MAX))); -#undef ROW_MIN -#undef ROW_MAX -#undef COL_MIN -#undef COL_MAX + // maintain same order as hash was, regardless of how actual object is stored + if (const auto cmp = y <=> rhs.y; 0 != cmp) + { + return cmp; + } + return x <=> rhs.x; } - /** - * \brief Row from hash - * \param hash hash to extract row from - * \return Row from hash - */ - [[nodiscard]] static constexpr Idx unhashRow(const Topo hash) noexcept + XYPos operator+(const XYPos& rhs) const noexcept { - return unhash_row(XYBits, hash); + return XYPos{XPos{rhs.x.value + x.value}, YPos{rhs.y.value + y.value}}; } - /** - * \brief Column - * \param hash hash to extract column from - * \return Column - */ - [[nodiscard]] static constexpr Idx unhashColumn(const Topo hash) noexcept + XYPos center() const noexcept { return *this + XYPos{XPos{CELL_CENTER}, YPos{CELL_CENTER}}; } + XYIdx operator+(const XYIdx& rhs) const noexcept { return XYIdx{x + rhs.x, y + rhs.y}; } + XYIdx operator+(const XIdx& rhs) const noexcept { return XYIdx{x + rhs, y}; } + XYIdx operator+(const YIdx& rhs) const noexcept { return XYIdx{x, y + rhs}; } + /** + * Determine the direction that a given cell is in from another cell. This is the + * same convention as wind (i.e. the direction it is coming from, not the direction + * it is going towards). + * @param src The cell to find directions relative to + * @param dst The cell to find the direction of + * @return Direction that you would have to go in to get to dst from src + */ + inline constexpr CellIndex relativeIndex(const XYIdx& rhs) const noexcept { - return unhash_column(ColumnMask, hash); + constexpr CellIndex DIRECTIONS[9] = { + DIRECTION_SW, + DIRECTION_S, + DIRECTION_SE, + DIRECTION_W, + DIRECTION_NONE, + DIRECTION_E, + DIRECTION_NW, + DIRECTION_N, + DIRECTION_NE + }; + return DIRECTIONS[((x - rhs.x).value + 1) + 3 * ((y - rhs.y).value + 1)]; } + XIdx x{}; + YIdx y{}; }; -template -inline bool operator<(const Position& lhs, const Position& rhs) -{ - return lhs.hash() < rhs.hash(); -} -template -inline bool operator>(const Position& lhs, const Position& rhs) +inline XYPos operator+(const XYPos& lhs, const XYIdx& rhs) noexcept { return rhs + lhs; } +static inline constexpr std::pair hash_to_xy(const XYIdx& xy) noexcept { - return rhs < lhs; + return {xy.x, xy.y}; } -template -inline bool operator<=(const Position& lhs, const Position& rhs) +static inline constexpr std::pair hash_to_xy_value(const XYIdx& xy) noexcept { - return !(lhs > rhs); + return {xy.x_value(), xy.y_value()}; } -template -inline bool operator>=(const Position& lhs, const Position& rhs) +static inline constexpr size_t to_index(const XYIdx& xy) noexcept { - return !(lhs < rhs); + // NOTE: do this here since fixed grid is the only reason for this? + return (static_cast(xy.y_value()) << XYBits) + static_cast(xy.x_value()); } -#ifdef DEBUG_DIRECTIONS -// FIX: seems like there must be something with enum type that would be better? -static const map DIRECTION_NAMES{ - {DIRECTION_NONE, "NONE"}, - {DIRECTION_W, "W"}, - {DIRECTION_E, "E"}, - {DIRECTION_S, "S"}, - {DIRECTION_N, "N"}, - {DIRECTION_SW, "SW"}, - {DIRECTION_NE, "NE"}, - {DIRECTION_NW, "NW"}, - {DIRECTION_SE, "SE"} -}; -#endif -class Location : public Position -{ -public: - using Position::Position; - /** - * \brief Construct using hash of row and column - * \param hash HashSize derived form row and column - */ -// NOTE: do this so that we don't get warnings about unused variables in release mode -#ifndef DEBUG_GRIDS - explicit constexpr Location(const Idx, const Idx, const HashSize hash) noexcept -#else - explicit Location(const Idx row, const Idx column, const HashSize hash) noexcept -#endif - : Location(hash & HashMask) - { -#ifdef DEBUG_GRIDS - logging::check_fatal( - row < 0 || row >= MAX_ROWS, "Row {:d} is out of bounds ({:d}, {:d})", row, 0, MAX_ROWS - ); - logging::check_fatal( - column < 0 || column >= MAX_COLUMNS, - "Column {:d} is out of bounds ({:d}, {:d})", - column, - 0, - MAX_COLUMNS - ); - logging::check_fatal( - (row != unhashRow(topo_data_)) || column != unhashColumn(topo_data_), - "Hash is incorrect ({:d}, {:d})", - row, - column - ); -#endif - } - /** - * \brief Constructor - * \param row Row - * \param column Column - */ - Location(const Idx row, const Idx column) noexcept - : Location(row, column, doHash(row, column) & HashMask) - { -#ifdef DEBUG_GRIDS - logging::check_fatal( - row >= MAX_ROWS || column >= MAX_COLUMNS, "Location out of bounds ({:d}, {:d})", row, column - ); -#endif - } - Location(const Coordinates& coord) : Location(std::get<0>(coord), std::get<1>(coord)) { } - /** - * \brief Construct with given hash that may contain data from subclasses - * \param hash_size Hash to store - */ - explicit constexpr Location(const HashSize& hash_size) noexcept - : Position(static_cast(hash_size)) - { } - /** - * \brief Construct with given hash that may contain data from subclasses - * \param hash_size Hash to store - */ - template - explicit constexpr Location(const Position

& position) noexcept - : Position(static_cast(position.hash())) - { } -}; /** * Determine the direction that a given cell is in from another cell. This is the * same convention as wind (i.e. the direction it is coming from, not the direction @@ -288,6 +170,9 @@ class Location : public Position * @param dst The cell to find the direction of * @return Direction that you would have to go in to get to dst from src */ -CellIndex relativeIndex(const Location& src, const Location& dst) noexcept; +inline constexpr CellIndex relativeIndex(const XYIdx& src, const XYIdx& dst) noexcept +{ + return src.relativeIndex(dst); +} } #endif diff --git a/src/cpp/fs/Log.cpp b/src/cpp/fs/Log.cpp index 520403b00ff..077c033d101 100644 --- a/src/cpp/fs/Log.cpp +++ b/src/cpp/fs/Log.cpp @@ -59,7 +59,7 @@ int open_log_file(const char* filename) noexcept lock_guard lock(mutex_); // turn off buffering so lines write to file immediately auto outfile = out_file(); - outfile->rdbuf()->pubsetbuf(0, 0); + outfile->rdbuf()->pubsetbuf(nullptr, 0); outfile->open(filename); if (!outfile->is_open()) { @@ -113,14 +113,8 @@ string output_no_check(const logging::level log_level, const string msg) DEBUG_N auto msg_fmt = format_log_message(LOG_LABELS[static_cast(log_level)], msg); lock_guard lock(mutex_); cout << msg_fmt << "\n"; -#ifndef NDEBUG - // if debugging then output everything to log file but not necessarily stdout - if (should_log(max(logging::get_log_level(), logging::level::verbose))) -#endif - { - // output is out_file_ or pre_file_log_ if not open yet - *output_ << msg_fmt << "\n"; - } + // output is out_file_ or pre_file_log_ if not open yet + *output_ << msg_fmt << "\n"; return msg_fmt; } catch (const std::exception& ex) diff --git a/src/cpp/fs/Log.h b/src/cpp/fs/Log.h index b2513841f23..ca96dc3fdbd 100644 --- a/src/cpp/fs/Log.h +++ b/src/cpp/fs/Log.h @@ -59,13 +59,10 @@ template Args&&... args ) DEBUG_NOEXCEPT_OFF { - // log everything in debug build -#ifdef NDEBUG if (should_not_log(log_level)) { return ""; } -#endif return output_no_check(log_level, format, std::forward(args)...); } template @@ -134,8 +131,11 @@ inline int fatal(const std::exception& ex, std::format_string format, A return fatal(ex); } template -inline void check_fatal(bool condition, std::format_string format, Args&&... args) - DEBUG_NOEXCEPT_OFF +inline constexpr void check_fatal( + bool condition, + std::format_string format, + Args&&... args +) DEBUG_NOEXCEPT_OFF { if (!condition) { diff --git a/src/cpp/fs/LogPoints.cpp b/src/cpp/fs/LogPoints.cpp index 59575e199b3..03b1861f8eb 100644 --- a/src/cpp/fs/LogPoints.cpp +++ b/src/cpp/fs/LogPoints.cpp @@ -63,7 +63,7 @@ void LogPoints::log_unchecked( ) const noexcept { #ifdef LOG_POINTS_RELATIVE - constexpr auto MID = MAX_COLUMNS / 2; + constexpr auto MID = MAX_WIDTH / 2; const auto p_x = x - MID; const auto p_y = y - MID; const auto t = time - start_time_; diff --git a/src/cpp/fs/LogPoints.h b/src/cpp/fs/LogPoints.h index 4e9a21a2ae8..db9858e75e9 100644 --- a/src/cpp/fs/LogPoints.h +++ b/src/cpp/fs/LogPoints.h @@ -2,7 +2,6 @@ #ifndef FS_LOG_POINTS_H #define FS_LOG_POINTS_H #include "stdafx.h" -#include "InnerPos.h" namespace fs { constexpr auto STAGE_CONDENSE = 'C'; @@ -26,15 +25,13 @@ class LogPoints template void log(size_t step, const char stage, const DurationSize time, const R& points) const noexcept { -#ifdef DEBUG_POINTS - logging::check_fatal(points.empty(), "Logging empty points"); -#endif + assert(!points.empty() && "Logging empty points"); // don't loop if not logging if (isLogging()) { for (const auto& p : points) { - log_unchecked(step, stage, time, x(p), y(p)); + log_unchecked(step, stage, time, p.x.value, p.y.value); } } } diff --git a/src/cpp/fs/LogValue.h b/src/cpp/fs/LogValue.h index b11fc6548ea..b043b8c9c07 100644 --- a/src/cpp/fs/LogValue.h +++ b/src/cpp/fs/LogValue.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ #ifndef FS_LOG_VALUE_H #define FS_LOG_VALUE_H +#include #include "unstable.h" namespace fs { diff --git a/src/cpp/fs/Model.cpp b/src/cpp/fs/Model.cpp index 396c7f8c630..88323c605eb 100644 --- a/src/cpp/fs/Model.cpp +++ b/src/cpp/fs/Model.cpp @@ -4,6 +4,7 @@ #include "FireWeather.h" #include "FWI.h" #include "Input.h" +#include "Location.h" #include "Log.h" #include "Observer.h" #include "Perimeter.h" @@ -214,7 +215,7 @@ void Model::readWeather( } #ifdef DEBUG_WEATHER const auto month = t.tm_mon + 1; - const auto row_fmt = std::format( + const auto fmt_line = std::format( "{:d},{:d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f}", cur, year_, @@ -235,8 +236,8 @@ void Model::readWeather( w.bui.value, w.fwi.value ); - logging::debug(row_fmt.c_str()); - out << row_fmt << "\r\n"; + logging::debug(fmt_line.c_str()); + out << fmt_line << "\r\n"; #endif } } @@ -264,12 +265,14 @@ void Model::readWeather( } } } -void Model::findStarts(const Location location) +void Model::findStarts(const XYIdx& location) { logging::error("Trying to start a fire in non-fuel"); Idx range = 1; - // HACK: should always be centered in the grid - while (starts_.empty() && (range < (MAX_COLUMNS / 2))) + auto [x_loc, y_loc] = hash_to_xy(location); + const auto width = env_->width(); + const auto height = env_->height(); + while (starts_.empty() && (range < (MAX_WIDTH / 2))) { for (Idx x = -range; x <= range; ++x) { @@ -278,10 +281,21 @@ void Model::findStarts(const Location location) // make sure we only look at the outside of the box if (1 == range || abs(x) == range || abs(y) == range) { - const auto loc = env_->cell(Location(location.row() + y, location.column() + x)); - if (!is_null_fuel(loc)) + // is this going to work if negative? + // const auto xy = location + XIdx{x} + YIdx{y}; + const XIdx x1{x_loc + XIdx{x}}; + if (0 <= x1.value && x1.value < width) { - starts_.push_back(make_shared(cell(loc))); + const YIdx y1{y_loc + YIdx{y}}; + if (0 <= y1.value && y1.value < height) + { + const XYIdx xy{x1, y1}; + const auto for_cell = env_->cell(xy); + if (!is_null_fuel(for_cell)) + { + starts_.push_back(xy); + } + } } } } @@ -297,21 +311,22 @@ void Model::findStarts(const Location location) for (const auto& s : starts_) { std::ignore = - logging::output_no_check(logging::level::info, "\t{:d}, {:d}", s->row(), s->column()); + logging::output_no_check(logging::level::info, "\t{:d}, {:d}", s.x_value(), s.y_value()); } } } void Model::findAllStarts() { logging::note("Running scenarios for every possible start location"); - for (Idx x = 0; x < env_->columns(); ++x) + for (Idx x = 0; x < env_->width(); ++x) { - for (Idx y = 0; y < env_->rows(); ++y) + for (Idx y = 0; y < env_->height(); ++y) { - const auto loc = env_->cell(Location(y, x)); - if (!is_null_fuel(loc)) + const XYIdx xy{x, y}; + const auto for_cell = env_->cell(xy); + if (!is_null_fuel(for_cell)) { - starts_.push_back(make_shared(cell(loc))); + starts_.push_back(xy); } } } @@ -326,7 +341,7 @@ void Model::makeStarts( { // HACK: resolve once and fail if not set already static const auto& settings = fs::settings::instance(); - Location location(std::get<0>(coordinates), std::get<1>(coordinates)); + XYIdx location{coordinates.x, coordinates.y}; if (!perim.empty()) { logging::note("Initializing from perimeter {:s}", perim.canonical()); @@ -337,7 +352,9 @@ void Model::makeStarts( const auto s = burned.size(); if (1 >= s) { - logging::note("Converting perimeter into point since size is {:d} cell(s)", s); + logging::note( + "Converting perimeter into point since size is {:d} cell{:s}", s, (1 == s ? "" : "s") + ); // use whatever the one cell is instead of the lat/long if (1 == s) { @@ -360,7 +377,7 @@ void Model::makeStarts( logging::check_fatal(size != 0 && !perim.empty(), "Can't specify size and perimeter"); // we have a perimeter to start from // HACK: make sure this isn't empty - starts_.push_back(make_shared(cell(location))); + starts_.push_back(location); logging::note( "Fire starting with size {:0.1f} ha", env_->to_hectares(perimeter_->burned.size()) ); @@ -382,13 +399,14 @@ void Model::makeStarts( else { logging::note("Fire starting with size {:0.1f} ha", env_->to_hectares(1)); - if (0 == size && is_null_fuel(cell(location))) + const auto for_cell = cell(location); + if (0 == size && is_null_fuel(for_cell)) { findStarts(location); } else { - starts_.push_back(make_shared(cell(location))); + starts_.push_back(location); } } } @@ -429,7 +447,16 @@ Iteration Model::readScenarios( if (settings.is_surface()) { setup_scenario(new Scenario( - this, 0, &wx_.at(0), &wx_daily_.at(0), start, starts_.at(0), start_point, start_day, last_date + this, + 0, + &wx_.at(0), + &wx_daily_.at(0), + start, + nullptr, + starts_.at(0), + start_point, + start_day, + last_date )); } else @@ -439,10 +466,24 @@ Iteration Model::readScenarios( const auto id = kv.first; const auto* cur_wx = &kv.second; const auto* cur_daily = &wx_daily_.at(id); + // FIX: this should simplify to the loop and passing both location & perim if (nullptr != perimeter_) { + logging::check_equal( + starts_.size(), static_cast(1), "start locations with perimeter" + ); + const auto& cur_start = starts_.at(0); setup_scenario(new Scenario( - this, id, cur_wx, cur_daily, start, perimeter_, start_point, start_day, last_date + this, + id, + cur_wx, + cur_daily, + start, + perimeter_, + cur_start, + start_point, + start_day, + last_date )); } else @@ -451,7 +492,16 @@ Iteration Model::readScenarios( { // should always have at least the day before the fire in the weather stream setup_scenario(new Scenario( - this, id, cur_wx, cur_daily, start, cur_start, start_point, start_day, last_date + this, + id, + cur_wx, + cur_daily, + start, + perimeter_, + cur_start, + start_point, + start_day, + last_date )); } } @@ -871,7 +921,7 @@ map> Model::runIterations( bool is_being_cancelled = false; // HACK: use initial value for type auto timer = std::thread([&]() { - constexpr auto CHECK_INTERVAL = std::chrono::seconds(1); + constexpr auto CHECK_INTERVAL = 1s; bool keep_checking{true}; const bool is_limited = 0 != settings.maximum_time_seconds; const bool with_interim = 0 != interim_save_interval_.count(); @@ -1175,17 +1225,17 @@ int Model::runScenarios( const auto position = env.findCoordinates(start_point, false); #ifndef NDEBUG logging::check_fatal( - std::get<0>(*position) > MAX_ROWS || std::get<1>(*position) > MAX_COLUMNS, + position->y > MAX_HEIGHT || position->x > MAX_WIDTH, "Location loaded outside of grid at position ({:d}, {:d})", - std::get<0>(*position), - std::get<1>(*position) + position->y, + position->x ); #endif - logging::info("Position is ({:d}, {:d})", std::get<0>(*position), std::get<1>(*position)); - const Location location{std::get<0>(*position), std::get<1>(*position)}; + logging::info("Position is ({:d}, {:d})", position->x, position->y); + const XYIdx location{position->x, position->y}; Model model(start_time, output_directory, start_point, &env); - logging::note("Grid has size ({:d}, {:d})", env.rows(), env.columns()); - logging::note("Fire start position is cell ({:d}, {:d})", location.row(), location.column()); + logging::note("Grid has size ({:d}, {:d})", env.width(), env.height()); + logging::note("Fire start position is cell ({:d}, {:d})", location.x_value(), location.y_value()); auto start_hour = ((start_time.tm_hour + (static_cast(start_time.tm_min) / 60)) / DAY_HOURS); logging::note( @@ -1291,7 +1341,7 @@ void Model::outputWeather(map& weather, const char* file_na month_and_day(year_, day, &month, &day_of_month); if (nullptr != w) { - const auto fmt_row = std::format( + const auto fmt_line = std::format( "{:d},{:d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f},{:1.6f}", i, year_, @@ -1312,7 +1362,7 @@ void Model::outputWeather(map& weather, const char* file_na w->bui.value, w->fwi.value ); - out << fmt_row << "\r\n"; + out << fmt_line << "\r\n"; SlopeSize SLOPE_MAX = MAX_SLOPE_FOR_DISTANCE; SlopeSize SLOPE_INCREMENT = 200; AspectSize ASPECT_MAX = 360; @@ -1342,7 +1392,7 @@ void Model::outputWeather(map& weather, const char* file_na fuel_name, w ); - const auto fmt_row = std::format( + const auto fmt_line = std::format( "{:d},{:d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:c},{:1.6g},{:1.6g},{:1.6g},{:1.6g},{:1.6g}\r\n", i, year_, @@ -1371,8 +1421,8 @@ void Model::outputWeather(map& weather, const char* file_na spread.surfaceFuelConsumption(), spread.totalFuelConsumption() ); - cout << fmt_row; - out_fbp << fmt_row; + cout << fmt_line; + out_fbp << fmt_line; } } } diff --git a/src/cpp/fs/Model.h b/src/cpp/fs/Model.h index cb9b8bf0a91..9384eff25ad 100644 --- a/src/cpp/fs/Model.h +++ b/src/cpp/fs/Model.h @@ -135,37 +135,13 @@ class Model const LazyPath& perimeter, const size_t size ); - /** - * \brief Cell at the given row and column - * \param row Row - * \param column Column - * \return Cell at the given row and column - */ [[nodiscard]] - Cell cell(const Idx row, const Idx column) const - { - return env_->cell(row, column); - } - /** - * \brief Cell at the given Location - * \param location Location to get Cell for - * \return Cell at the given Location - */ - template - [[nodiscard]] constexpr Cell cell(const Position

& position) const + Cell cell(const XYIdx xy) const { - return env_->cell(position); + return env_->cell(xy); } - /** - * \brief Number of rows in extent - * \return Number of rows in extent - */ - [[nodiscard]] constexpr Idx rows() const { return env_->rows(); } - /** - * \brief Number of columns in extent - * \return Number of columns in extent - */ - [[nodiscard]] constexpr Idx columns() const { return env_->columns(); } + [[nodiscard]] constexpr Idx height() const { return env_->height(); } + [[nodiscard]] constexpr Idx width() const { return env_->width(); } /** * \brief Cell width and height (m) * \return Cell width and height (m) @@ -390,7 +366,7 @@ class Model * \brief Find Cell(s) that can burn closest to Location * \param location Location to look for start Cells */ - void findStarts(Location location); + void findStarts(const XYIdx& location); /** * \brief Differences between date and the date of minimum foliar moisture content */ @@ -406,7 +382,7 @@ class Model /** * \brief Cell(s) that can burn closest to start Location */ - vector> starts_{}; + vector starts_{}; /** * \brief Time to use for simulation start */ diff --git a/src/cpp/fs/Observer.h b/src/cpp/fs/Observer.h index b1bc9ba8e1a..fd58274554a 100644 --- a/src/cpp/fs/Observer.h +++ b/src/cpp/fs/Observer.h @@ -82,7 +82,7 @@ class MapObserver : public IObserver * \brief Handle given event * \param event Event to handle */ - void handleEvent(const Event& event) override { map_.set(event.cell, getValue(event)); } + void handleEvent(const Event& event) override { map_.set(event.xy, getValue(event)); } /** * \brief Save observations * \param dir Directory to save to diff --git a/src/cpp/fs/Perimeter.cpp b/src/cpp/fs/Perimeter.cpp index c75071efb7d..2a20b83c651 100644 --- a/src/cpp/fs/Perimeter.cpp +++ b/src/cpp/fs/Perimeter.cpp @@ -24,27 +24,31 @@ BurnedMap::BurnedMap(const Grid& perim_grid, const const auto offset_x = static_cast((this->xllcorner() - perim_grid.xllcorner()) / this->cellSize()); const auto perim_origin = - static_cast(perim_grid.rows() + perim_grid.yllcorner() / this->cellSize()); - const auto this_origin = static_cast(this->rows() + this->yllcorner() / this->cellSize()); + static_cast(perim_grid.height() + perim_grid.yllcorner() / this->cellSize()); + const auto this_origin = static_cast(this->height() + this->yllcorner() / this->cellSize()); const auto offset_y = static_cast((perim_origin - this_origin)); // make sure we don't go out of bounds on grid - const auto min_column = static_cast(offset_x < 0 ? abs(offset_x) : 0); - const auto max_columns = min(this->columns(), perim_grid.columns()); - const auto min_row = static_cast(offset_y < 0 ? abs(offset_y) : 0); - const auto max_rows = min(this->rows(), static_cast(perim_grid.rows() - abs(offset_y))); + const auto min_x = static_cast(offset_x < 0 ? abs(offset_x) : 0); + const auto max_width = min(this->width(), perim_grid.width()); + const auto min_y = static_cast(offset_y < 0 ? abs(offset_y) : 0); + const auto max_height = + min(this->height(), static_cast(perim_grid.height() - abs(offset_y))); logging::note("Correcting perimeter raster offset by {:d}{:d} cells", offset_x, offset_y); size_t count = 0; // since it was read in as a vector we need to check all the cells - for (auto r = min_row; r < max_rows; ++r) + for (auto y = min_y; y < max_height; ++y) { - for (auto c = min_column; c < max_columns; ++c) + for (auto x = min_x; x < max_width; ++x) { - const auto loc = env.cell(r, c); - const Location fixed_loc(static_cast(r + offset_y), static_cast(c + offset_x)); + const XYIdx xy{x, y}; + const auto loc = env.cell(xy); + const auto x0 = static_cast(x + offset_x); + const auto y0 = static_cast(y + offset_y); + const XYIdx fixed_loc{x0, y0}; const auto value = perim_grid.at(fixed_loc); if (value != perim_grid.nodataValue() && !is_null_fuel(loc)) { - this->GridMap::set(loc, value); + this->GridMap::set(xy, value); ++count; } } @@ -53,7 +57,8 @@ BurnedMap::BurnedMap(const Grid& perim_grid, const #ifdef DEBUG_GRIDS for (auto& kv : data) { - logging::check_fatal(is_null_fuel(env.cell(kv.first)), "Null fuel in BurnedData"); + auto& loc = kv.first; + logging::check_fatal(is_null_fuel(env.cell(loc)), "Null fuel in BurnedData"); } #endif logging::info("Loaded burned area of size {:d} ha", size_hectares_); @@ -69,7 +74,7 @@ BurnedMap make_burned_map(const LazyPath& perim, const Point& point, const Envir Perimeter::Perimeter(const LazyPath& perim, const Point& point, const Environment& env) : Perimeter(make_burned_map(perim, point, env)) { } -BurnedMap make_burned_map(const Location& location, const size_t size, const Environment& env) +BurnedMap make_burned_map(const XYIdx& location, const size_t size, const Environment& env) { // NOTE: FwiWeather is unused but could change this to try doing length to breadth ratio auto perim_grid = env.makeMap(0); @@ -80,6 +85,8 @@ BurnedMap make_burned_map(const Location& location, const size_t size, const Env auto max_distance = sqrt(num_cells / M_PI); perim_grid.set(location, 1); ++count; + const auto& x_loc = location.x.value; + const auto& y_loc = location.y.value; // HACK: assume fuel for origin matches the rest of the fire while (num_cells > count) { @@ -91,12 +98,10 @@ BurnedMap make_burned_map(const Location& location, const size_t size, const Env // look at any cell that's within the range if (sqrt(pow_int<2>(x) + pow_int<2>(y)) < max_distance) { - const auto r = static_cast(location.row() + x); - const auto c = static_cast(location.column() + y); - const auto loc = env.cell(r, c); - if (1 != perim_grid.at(loc) && !is_null_fuel(loc)) + const XYIdx xy{x_loc + x, y_loc + y}; + if (1 != perim_grid.at(xy) && !is_null_fuel(env.cell(xy))) { - perim_grid.set(loc, 1); + perim_grid.set(xy, 1); ++count; } } @@ -106,7 +111,7 @@ BurnedMap make_burned_map(const Location& location, const size_t size, const Env } return BurnedMap(perim_grid, env); } -Perimeter::Perimeter(const Location& location, const size_t size, const Environment& env) +Perimeter::Perimeter(const XYIdx& location, const size_t size, const Environment& env) : Perimeter(make_burned_map(location, size, env)) { } } diff --git a/src/cpp/fs/Perimeter.h b/src/cpp/fs/Perimeter.h index db58d56f6a5..321de752942 100644 --- a/src/cpp/fs/Perimeter.h +++ b/src/cpp/fs/Perimeter.h @@ -41,21 +41,15 @@ class Perimeter * \param env Environment to apply Perimeter to */ Perimeter(const LazyPath& perim, const Point& point, const Environment& env); - /** - * \brief Create a Perimeter of the given size at the given Location - * \param location Location to center Perimeter on - * \param size Size of Perimeter to create - * \param env Environment to apply Perimeter to - */ - Perimeter(const Location& location, const size_t size, const Environment& env); + Perimeter(const XYIdx& location, const size_t size, const Environment& env); /** * \brief List of all burned Locations */ - const list burned; + const list burned; /** * \brief List of all Locations along the edge of this Perimeter */ - const list edge; + const list edge; private: Perimeter(const BurnedMap& burned_map); diff --git a/src/cpp/fs/ProbabilityMap.h b/src/cpp/fs/ProbabilityMap.h index f7387d186fa..6b11f942925 100644 --- a/src/cpp/fs/ProbabilityMap.h +++ b/src/cpp/fs/ProbabilityMap.h @@ -31,9 +31,9 @@ class ProbabilityMap ProbabilityMap() = delete; ~ProbabilityMap(); ProbabilityMap(const ProbabilityMap& rhs) noexcept = delete; - ProbabilityMap(ProbabilityMap&& rhs) noexcept = default; + ProbabilityMap(ProbabilityMap&& rhs) noexcept = delete; ProbabilityMap& operator=(const ProbabilityMap& rhs) noexcept = delete; - ProbabilityMap& operator=(ProbabilityMap&& rhs) noexcept = default; + ProbabilityMap& operator=(ProbabilityMap&& rhs) noexcept = delete; /** * \brief Constructor * \param time Time in simulation this ProbabilityMap represents diff --git a/src/cpp/fs/RangeIterator.h b/src/cpp/fs/RangeIterator.h index c8ac6227797..678617333a2 100644 --- a/src/cpp/fs/RangeIterator.h +++ b/src/cpp/fs/RangeIterator.h @@ -163,7 +163,7 @@ class RangeIterator { return static_cast(step_ - rhs.step_); } - inline auto operator<=>(const RangeIterator& rhs) const = default; + auto operator<=>(const RangeIterator& rhs) const = default; // inline bool operator==(const RangeIterator& rhs) const { return *(*this) == rhs; } // inline bool operator!=(const RangeIterator& rhs) const { return !(*(*this) == rhs); } // inline bool operator>(const RangeIterator& rhs) const { return *(*this) < *rhs; } diff --git a/src/cpp/fs/Scenario.cpp b/src/cpp/fs/Scenario.cpp index a7ba2fd1ec9..ffd15815c2f 100644 --- a/src/cpp/fs/Scenario.cpp +++ b/src/cpp/fs/Scenario.cpp @@ -16,7 +16,6 @@ namespace fs { using std::cout; -constexpr auto CELL_CENTER = static_cast(0.5); // constexpr auto PRECISION = static_cast(0.001); static atomic COUNT = 0; static atomic COMPLETED = 0; @@ -27,7 +26,7 @@ template void do_each(T& for_list, F fct) { std::for_each( -#ifndef __APPLE__ +#if !defined(__APPLE__) || !defined(__clang__) // apple clang doesn't support this? std::execution::seq, #endif @@ -40,7 +39,7 @@ template void do_par(T& for_list, F fct) { std::for_each( -#ifndef __APPLE__ +#if !defined(__APPLE__) || !defined(__clang__) // apple clang doesn't support this? std::execution::par_unseq, #endif @@ -155,87 +154,11 @@ static void make_threshold( { make_threshold(thresholds, mt, start_day, last_date, &same); } -Scenario::Scenario( - Model* model, - const size_t id, - const ptr weather, - const DurationSize start_time, - const shared_ptr& perimeter, - const StartPoint& start_point, - const Day start_day, - const Day last_date -) - : Scenario(model, id, weather, weather, start_time, perimeter, start_point, start_day, last_date) -{ } -Scenario::Scenario( - Model* model, - const size_t id, - ptr weather, - const DurationSize start_time, - const shared_ptr& start_cell, - const StartPoint& start_point, - const Day start_day, - const Day last_date -) - : Scenario(model, id, weather, weather, start_time, start_cell, start_point, start_day, last_date) -{ } -Scenario::Scenario( - Model* model, - const size_t id, - ptr weather, - ptr weather_daily, - const DurationSize start_time, - const shared_ptr& perimeter, - const StartPoint& start_point, - const Day start_day, - const Day last_date -) - : Scenario( - model, - id, - weather, - weather_daily, - start_time, - perimeter, - nullptr, - start_point, - start_day, - last_date - ) -{ } -Scenario::Scenario( - Model* model, - const size_t id, - const ptr weather, - const ptr weather_daily, - const DurationSize start_time, - const shared_ptr& start_cell, - const StartPoint& start_point, - const Day start_day, - const Day last_date -) - : Scenario( - model, - id, - weather, - weather_daily, - start_time, - // make_unique(*model, nullptr), - nullptr, - start_cell, - start_point, - start_day, - last_date - ) -{ } // HACK: just set next start point here for surface right now -Scenario* Scenario::reset_with_new_start( - const shared_ptr& start_cell, - ptr final_sizes -) +Scenario* Scenario::reset_with_new_start(const XYIdx& start_xy, ptr final_sizes) { unburnable_.clear(); - start_cell_ = start_cell; + start_xy_ = start_xy; cancelled_ = false; current_time_ = start_time_; intensity_ = nullptr; @@ -334,75 +257,81 @@ void Scenario::evaluate(const Event& event) event.time ); #endif - const auto& p = event.cell; - const auto x = p.column() + CELL_CENTER; - const auto y = p.row() + CELL_CENTER; - const XYPos p0{x, y}; - switch (event.type) + // HACK: handle NewFire separately since can't create variables in switch + if (Event::Type::NewFire == event.type) { - case Event::Type::FireSpread: - ++step_; -#ifdef DEBUG_POINTS - { - const auto ymd = fs::make_timestamp(model().year(), event.time); - } -#endif - scheduleFireSpread(event); - break; - case Event::Type::Save: - std::ignore = saveObservers(model_->outputDirectory(), event.time); - saveStats(event.time); - break; - case Event::Type::NewFire: - points_log_.log( - step_, STAGE_NEW, event.time, p.column() + CELL_CENTER, p.row() + CELL_CENTER - ); - // HACK: don't do this in constructor because scenario creates this in its constructor - // HACK: insert point as originating from itself - points_.insert( - p0, - SpreadData(event.time, NO_INTENSITY, NO_ROS, Direction::Invalid(), Direction::Invalid()), - x, - y - ); - if (is_null_fuel(event.cell)) + const auto p0 = event.xy.center(); + const auto& x = p0.x; + const auto& y = p0.y; + const auto for_cell = model_->cell(event.xy); + points_log_.log(step_, STAGE_NEW, event.time, x.value, y.value); + // HACK: don't do this in constructor because scenario creates this in its constructor + // HACK: insert point as originating from itself + insert( + points_, + p0, + SpreadData{event.time, NO_INTENSITY, NO_ROS, Direction::Invalid(), Direction::Invalid()}, + p0 + ); + if (is_null_fuel(for_cell)) + { + exit(logging::fatal("{:s} Trying to start a fire in non-fuel", log_prefix_)); + } + logging::verbose( + "{:s} Starting fire at point ({:f}, {:f}) in fuel type {:s} at time {:f}", + log_prefix_, + x.value, + y.value, + FuelType::safeName(check_fuel(for_cell)), + event.time + ); + if (!survives(event.time, for_cell, event.time_at_location)) + { + if (should_log(logging::level::info)) { - exit(logging::fatal("{:s} Trying to start a fire in non-fuel", log_prefix_)); + // HACK: show daily values since that's what survival uses + const auto wx = weather_daily(event.time); + logging::info( + "{:s} Didn't survive ignition in {:s} with weather {:f}, {:f}", + log_prefix_, + FuelType::safeName(check_fuel(for_cell)), + wx->ffmc.value, + wx->dmc.value + ); } - logging::verbose( - "{:s} Starting fire at point ({:f}, {:f}) in fuel type {:s} at time {:f}", - log_prefix_, - x, - y, - FuelType::safeName(check_fuel(event.cell)), - event.time - ); - if (!survives(event.time, event.cell, event.time_at_location)) - { - if (should_log(logging::level::info)) + // HACK: we still want the fire to have existed, so set the intensity of the origin + } + // fires start with intensity of 1 + burn(event); + scheduleFireSpread(event); + } + else + { + switch (event.type) + { + case Event::Type::FireSpread: + ++step_; +#ifdef DEBUG_POINTS { - // HACK: show daily values since that's what survival uses - const auto wx = weather_daily(event.time); - logging::info( - "{:s} Didn't survive ignition in {:s} with weather {:f}, {:f}", - log_prefix_, - FuelType::safeName(check_fuel(event.cell)), - wx->ffmc.value, - wx->dmc.value - ); + const auto ymd = fs::make_timestamp(model().year(), event.time); } - // HACK: we still want the fire to have existed, so set the intensity of the origin - } - // fires start with intensity of 1 - burn(event); - scheduleFireSpread(event); - break; - case Event::Type::EndSimulation: - logging::verbose("{:s} End simulation event reached at {:f}", log_prefix_, event.time); - endSimulation(); - break; - default: - throw runtime_error("Invalid event type"); +#endif + scheduleFireSpread(event); + break; + case Event::Type::Save: + std::ignore = saveObservers(model_->outputDirectory(), event.time); + saveStats(event.time); + break; + case Event::Type::NewFire: + throw runtime_error("Error handling event"); + break; + case Event::Type::EndSimulation: + logging::verbose("{:s} End simulation event reached at {:f}", log_prefix_, event.time); + endSimulation(); + break; + default: + throw runtime_error("Invalid event type"); + } } } Scenario::Scenario( @@ -412,51 +341,26 @@ Scenario::Scenario( const ptr weather_daily, const DurationSize start_time, const shared_ptr& perimeter, - const shared_ptr& start_cell, + const XYIdx& start_cell, StartPoint start_point, const Day start_day, const Day last_date -) - : Scenario( - model, - id, - weather, - weather_daily, - start_time, - perimeter, - start_cell, - start_point, - start_day, - last_date, - settings::instance() - ) -{ } -Scenario::Scenario( - Model* model, - const size_t id, - const ptr weather, - const ptr weather_daily, - const DurationSize start_time, - const shared_ptr& perimeter, - const shared_ptr& start_cell, - StartPoint start_point, - const Day start_day, - const Day last_date, - const Settings& settings ) : current_time_(start_time), unburnable_{}, intensity_(nullptr), perimeter_(perimeter), - max_ros_(0), start_cell_(start_cell), weather_(weather), weather_daily_(weather_daily), + max_ros_(0), start_xy_(start_cell), weather_(weather), weather_daily_(weather_daily), model_(model), probabilities_(nullptr), final_sizes_(nullptr), start_point_(std::move(start_point)), id_(id), start_time_(start_time), last_save_(weather_->minDate()), simulation_(-1), start_day_(start_day), last_date_(last_date), ran_(false), step_(0), - points_log_(LogPoints{model_->outputDirectory(), settings.save_points, id_, start_time_}) + points_log_( + LogPoints{model_->outputDirectory(), settings::instance().save_points, id_, start_time_} + ) { const auto wx = weather_->at(start_time_); logging::check_fatal( nullptr == wx, "No weather for start time {:s}", make_timestamp(model->year(), start_time_) ); - const auto saves = settings.output_date_offsets.offsets(); + const auto saves = settings::instance().output_date_offsets.offsets(); const auto last_save = start_day_ + saves[saves.size() - 1]; logging::check_fatal( last_save > weather_->maxDate(), @@ -514,7 +418,7 @@ Scenario::Scenario(Scenario&& rhs) noexcept unburnable_(std::move(rhs.unburnable_)), scheduler_(std::move(rhs.scheduler_)), intensity_(std::move(rhs.intensity_)), perimeter_(std::move(rhs.perimeter_)), spread_info_(std::move(rhs.spread_info_)), arrival_(std::move(rhs.arrival_)), - max_ros_(rhs.max_ros_), start_cell_(std::move(rhs.start_cell_)), weather_(rhs.weather_), + max_ros_(rhs.max_ros_), start_xy_(std::move(rhs.start_xy_)), weather_(rhs.weather_), weather_daily_(rhs.weather_daily_), model_(rhs.model_), probabilities_(rhs.probabilities_), final_sizes_(rhs.final_sizes_), start_point_(std::move(rhs.start_point_)), id_(rhs.id_), start_time_(rhs.start_time_), last_save_(rhs.last_save_), simulation_(rhs.simulation_), @@ -533,7 +437,7 @@ Scenario& Scenario::operator=(Scenario&& rhs) noexcept scheduler_ = std::move(rhs.scheduler_); intensity_ = std::move(rhs.intensity_); perimeter_ = std::move(rhs.perimeter_); - start_cell_ = std::move(rhs.start_cell_); + start_xy_ = std::move(rhs.start_xy_); weather_ = rhs.weather_; weather_daily_ = rhs.weather_daily_; model_ = rhs.model_; @@ -554,38 +458,34 @@ void Scenario::burn(const Event& event) { #ifdef DEBUG_SIMULATION logging::check_fatal( - intensity_->hasBurned(event.cell), + intensity_->hasBurned(event.xy), "{:s} Re-burning cell ({:d}, {:d})", log_prefix_, - event.cell.column(), - event.cell.row() + event.xy.x_value(), + event.xy.y_value() ); #endif // #ifdef DEBUG_POINTS - // logging::check_fatal((*unburnable_)[event.cell().hash()], [&]() { + // logging::check_fatal((*unburnable_)[event.xy.hash()], [&]() { // return std::format( - // "{:s} Burning unburnable cell ({:d}, {:d})", - // log_prefix_, - // event.cell().column(), - // event.cell().row() + // "{:s} Burning unburnable cell ({:d}, {:d})", log_prefix_, event.xy.x(), event.xy.y() // ); // }); // #endif // Observers only care about cells burning so do it here notify(event); - intensity_->burn(event.cell, event.intensity, event.ros, event.raz); + intensity_->burn(event.xy, event.intensity, event.ros, event.raz); #ifdef DEBUG_GRIDS logging::check_fatal( - !intensity_->hasBurned(event.cell), "{:s} Wasn't marked as burned after burn", log_prefix_ + !intensity_->hasBurned(event.xy), "{:s} Wasn't marked as burned after burn", log_prefix_ ); #endif - arrival_[event.cell] = event.time; + arrival_[event.xy] = event.time; } -bool Scenario::isSurrounded(const Location& location) const +bool Scenario::isSurrounded(const XYIdx& location) const { return intensity_->isSurrounded(location); } -Cell Scenario::cell(const InnerPos& p) const noexcept { return cell(p.second, p.first); } #ifdef DEBUG_PROBABILITY void saveProbabilities( const string_view dir, @@ -626,31 +526,30 @@ Scenario* Scenario::run(map>* probabili } if (nullptr == perimeter_) { - addEvent(Event{.time = start_time_, .type = Event::Type::NewFire, .cell = cell(*start_cell_)}); + addEvent(Event{.time = start_time_, .type = Event::Type::NewFire, .xy = start_xy_}); } else { logging::verbose("{:s} Applying perimeter", log_prefix_); intensity_->applyPerimeter(*perimeter_); logging::verbose("{:s} Perimeter applied", log_prefix_); - const auto& env = model().environment(); logging::verbose("{:s} Igniting points", log_prefix_); for (const auto& location : perimeter_->edge) { - const auto cell = env.cell(location); #ifdef DEBUG_SIMULATION - logging::check_fatal(is_null_fuel(cell), "{:s} Null fuel in perimeter", log_prefix_); + const auto for_cell = model_->cell(location); + logging::check_fatal(is_null_fuel(for_cell), "{:s} Null fuel in perimeter", log_prefix_); #endif - const auto x = cell.column() + CELL_CENTER; - const auto y = cell.row() + CELL_CENTER; - const XYPos p0{x, y}; + const auto p0 = location.center(); + const auto& x = p0.x; + const auto& y = p0.y; // log_verbose(*this, "Adding point ({:d}, {:d})", - logging::extensive("{:s} Adding point ({:f}, {:f})", log_prefix_, x, y); - points_.insert( + logging::extensive("{:s} Adding point ({:f}, {:f})", log_prefix_, x.value, y.value); + insert( + points_, p0, - SpreadData(start_time_, NO_INTENSITY, NO_ROS, Direction::Invalid(), Direction::Invalid()), - x, - y + SpreadData{start_time_, NO_INTENSITY, NO_ROS, Direction::Invalid(), Direction::Invalid()}, + p0 ); } addEvent(Event{.time = start_time_, .type = Event::Type::FireSpread}); @@ -661,13 +560,13 @@ Scenario* Scenario::run(map>* probabili logging::verbose("{:s} Creating simulation end event for {:f}", log_prefix_, last_save_); addEvent(Event{.time = last_save_, .type = Event::Type::EndSimulation}); // mark all original points as burned at start - for (auto& kv : points_.map_) + for (auto& [loc, pts] : points_.cells_) { - const auto& location = cell(kv.first); + assert(loc == pts.pos()); // would be burned already if perimeter applied - if (canBurn(location)) + if (canBurn(loc)) { - const Event fake_event{.time = start_time_, .cell = location, .ros = 0.0}; + const Event fake_event{.time = start_time_, .xy = loc, .ros = 0.0}; burn(fake_event); } } @@ -767,12 +666,10 @@ CellPointsMap apply_offsets_spreadkey( offsets.cbegin(), offsets.cend(), offsets_after_duration.begin(), - [&](const ROSOffset& r_p) { - const auto& intensity = std::get<0>(r_p); - const auto& ros = std::get<1>(r_p); - const auto& raz = std::get<2>(r_p); - const auto& p = std::get<3>(r_p); - return ROSOffset(intensity, ros, raz, Offset(p.first * duration, p.second * duration)); + [&](const ROSOffset& r) { + return ROSOffset{ + r.intensity, r.ros, r.raz, Offset{r.offset.x * duration, r.offset.y * duration} + }; } ); logging::verbose( @@ -781,61 +678,47 @@ CellPointsMap apply_offsets_spreadkey( logging::verbose("cell_pts_map has {:d} items", cell_pts_map.size()); for (auto& pts_for_cell : cell_pts_map) { - const Location& location = std::get<0>(pts_for_cell); - CellPoints& cell_pts = std::get<1>(pts_for_cell); + auto& [location, cell_pts] = pts_for_cell; #ifdef DEBUG_CELLPOINTS - logging::note( - "cell_pts for ({:d}, {:d}) has {:d} items", src.column(), src.row(), cell_pts.size() - ); + logging::note("cell_pts for ({:d}, {:d}) has {:d} items", src.x(), src.y(), cell_pts.size()); #endif if (cell_pts.empty()) { #ifdef DEBUG_CELLPOINTS - logging::note("Cell ({:d}, {:d}) ignored because empty", src.column(), src.row()); + logging::note("Cell ({:d}, {:d}) ignored because empty", src.x(), src.y()); #endif continue; } - auto& pts = cell_pts.pts_.points(); - auto& dirs = cell_pts.pts_.directions(); - std::array, NUM_DIRECTIONS> pt_dirs{}; - for (size_t i = 0; i < pts.size(); ++i) - { - pt_dirs[i] = {pts[i], dirs[i]}; - } + ////////////////////////////////////////////////////////////////////////////////// + auto pt_dirs = cell_pts.point_directions(); std::sort(pt_dirs.begin(), pt_dirs.end()); const auto it_pt_dirs_last = std::unique(pt_dirs.begin(), pt_dirs.end()); auto it_pt_dirs = pt_dirs.cbegin(); while (it_pt_dirs != it_pt_dirs_last) { const auto& pt_dir = *it_pt_dirs; - const auto& pt = std::get<0>(pt_dir); - const auto& dir = std::get<1>(pt_dir); - const auto& cell_x = cell_pts.cell_x_y_.first; - const auto& cell_y = cell_pts.cell_x_y_.second; - const XYPos src{pt.first + cell_x, pt.second + cell_y}; + const auto& [pt, dir] = pt_dir; + const XYIdx src{pt}; + const auto [cell_x, cell_y] = hash_to_xy_value(cell_pts.pos()); // apply offsets to point // should be quicker to loop over offsets in inner loop - for (const ROSOffset& r_p : offsets_after_duration) + for (const ROSOffset& r : offsets_after_duration) { - const auto& intensity = std::get<0>(r_p); - const auto& ros = std::get<1>(r_p); - const auto& raz = std::get<2>(r_p); - const auto& out = std::get<3>(r_p); - const auto& x_o = out.first; - const auto& y_o = out.second; - const auto dir_diff = abs(raz.asDegrees() - dir); + const auto& x_o = r.offset.x; + const auto& y_o = r.offset.y; + const auto dir_diff = abs(r.raz.asDegrees() - dir); // #ifdef DEBUG_CELLPOINTS logging::verbose( "location.x {:d}; location.y {:d};" "cell_x {:d}; cell_y {:d};" " ros {:f}; x {:f}; y {:f}; duration {:f};\n", - location.column(), - location.row(), + location.x_value(), + location.y_value(), cell_x, cell_y, - ros, - pt.first, - pt.second, + r.ros, + pt.x.value, + pt.y.value, duration ); // // NOTE: there should be no change in the extent of the fire if we exclude things @@ -843,8 +726,17 @@ CellPointsMap apply_offsets_spreadkey( // // - but if we exclude too much then it can change how things spread, even if it // is a more representative angle for the grids if (is_initial || MAX_DEGREES >= dir_diff) { - const auto new_x = x_o + pt.first + cell_x; - const auto new_y = y_o + pt.second + cell_y; + logging::check_fatal( + -1 >= pt.x.value || MAX_WIDTH <= pt.x.value, + "x out of bounds when applying offsets: {}", + pt.x.value + ); + logging::check_fatal( + -1 >= pt.y.value || MAX_HEIGHT <= pt.y.value, + "y out of bounds when applying offsets: {}", + pt.y.value + ); + const XYPos pt_new{XPos{x_o + pt.x.value}, YPos{y_o + pt.y.value}}; logging::verbose( "({:d}, {:d}): {:f}: [{:f} => ({:f}, {:f})] + [{:f} => ({:f}, {:f})] = [{:f} => ({:f}, {:f})]", cell_x, @@ -853,18 +745,18 @@ CellPointsMap apply_offsets_spreadkey( dir, x_o, y_o, - raz.asDegrees(), - pt.first, - pt.second, - raz.asDegrees(), - new_x, - new_y + r.raz.asDegrees(), + pt.x.value, + pt.y.value, + r.raz.asDegrees(), + pt_new.x.value, + pt_new.y.value ); - r1.insert( - src, - SpreadData(arrival_time, intensity, ros, raz, Direction(Degrees{dir})), - new_x, - new_y + std::ignore = insert( + r1, + pt, + SpreadData{arrival_time, r.intensity, r.ros, r.raz, Direction{Degrees{dir}}}, + pt_new ); #ifdef DEBUG_CELLPOINTS logging::note("r1 is now {:d} items", r1.size()); @@ -873,6 +765,75 @@ CellPointsMap apply_offsets_spreadkey( } ++it_pt_dirs; } + // ////////////////////////////////////////////////////////////////////////////////// + // auto& pts = cell_pts.pts_.points; + // std::array, NUM_DIRECTIONS> pt_dirs{}; + // for (size_t i = 0; i < pts_inner.size(); ++i) + // { + // pt_dirs[i] = {pts[i], dirs[i]}; + // } + // std::sort(pt_dirs.begin(), pt_dirs.end()); + // const auto it_pt_dirs_last = std::unique(pt_dirs.begin(), pt_dirs.end()); + // auto it_pt_dirs = pt_dirs.cbegin(); + // while (it_pt_dirs != it_pt_dirs_last) + // { + // const auto& pt_dir = *it_pt_dirs; + // const auto& [src, dir] = pt_dir; + // const auto [cell_x, cell_y] = hash_to_xy_value(cell_pts.cell_x_y_); + // // apply offsets to point + // // should be quicker to loop over offsets in inner loop + // for (const ROSOffset& r : offsets_after_duration) + // { + // const auto& x_o = r.offset.x; + // const auto& y_o = r.offset.y; + // const auto dir_diff = abs(r.raz.asDegrees() - dir); + // // #ifdef DEBUG_CELLPOINTS + // logging::verbose( + // "location.x {:d}; location.y {:d};" + // "cell_x {:d}; cell_y {:d};" + // " ros {:f}; x {:f}; y {:f}; duration {:f};\n", + // location.x_value(), + // location.y_value(), + // cell_x, + // cell_y, + // r.ros, + // src.x.value, + // src.y.value, + // duration + // ); + // // // NOTE: there should be no change in the extent of the fire if we exclude things + // // behind the normal to the direction it came from + // // // - but if we exclude too much then it can change how things spread, even + // if it + // // is a more representative angle for the grids if (is_initial || MAX_DEGREES >= + // dir_diff) + // { + // const XPos new_x{x_o + src.x.value}; + // const YPos new_y{y_o + src.y.value}; + // logging::verbose( + // "({:d}, {:d}): {:f}: [{:f} => ({:f}, {:f})] + [{:f} => ({:f}, {:f})] = [{:f} => + // ({:f}, {:f})]", cell_x, cell_y, dir_diff, dir, x_o, y_o, r.raz.asDegrees(), + // src.x.value, + // src.y.value, + // r.raz.asDegrees(), + // new_x.value, + // new_y.value + // ); + // // TODO: switch to this version and confirm identical results + // // insert( + // // r1, + // // src, + // // SpreadData{arrival_time, r.intensity, r.ros, r.raz, Direction{Degrees{dir}}}, + // // new_x, + // // new_y + // // ); + // #ifdef DEBUG_CELLPOINTS + // logging::note("r1 is now {:d} items", r1.size()); + // #endif + // } + // } + // ++it_pt_dirs; + // ////////////////////////////////////////////////////////////////////////////////// } return r1; } @@ -915,10 +876,11 @@ void Scenario::scheduleFireSpread(const Event& event) // make block to prevent it being visible beyond use { // if we use an iterator this way we don't need to copy keys to erase things - auto it = points_.map_.begin(); - while (it != points_.map_.end()) + auto& lhs = points_.cells_; + auto it = lhs.begin(); + while (it != lhs.end()) { - const Location& loc = it->first; + auto& [loc, pts] = *it; const Cell for_cell = cell(loc); const auto key = for_cell.key(); { @@ -932,18 +894,14 @@ void Scenario::scheduleFireSpread(const Event& event) { max_ros_ = max(max_ros_, ros); // NOTE: shouldn't be Cell if we're looking up by just Location later - to_spread[key].emplace_back(loc, std::move(it->second)); - it = points_.map_.erase(it); + to_spread[key].emplace_back(std::move(*it)); + it = lhs.erase(it); #ifdef DEBUG_CELLPOINTS auto& v = to_spread[key]; const auto n = v.size(); const auto& p = v[n - 1].second; logging::note( - "added {:d} items to to_spread[{:d}][({:d}, {:d})]", - p.size(), - key, - loc.column(), - loc.row() + "added {:d} items to to_spread[{:d}][({:d}, {:d})]", p.size(), key, loc.x(), loc.y() ); #endif } @@ -988,11 +946,10 @@ void Scenario::scheduleFireSpread(const Event& event) #ifdef DEBUG_CELLPOINTS const auto n_c = cell_pts.size(); #endif - cell_pts.remove_if([this](const pair& kv) { - const auto& location = kv.first; - const auto h = location.hash(); + cell_pts.remove_if([this](const CellPointsMap::map_value& kv) { + auto& [location, pts] = kv; // clear out if unburnable - const auto do_clear = unburnable_.at(h); + const auto do_clear = unburnable_.at(location); return do_clear; }); #ifdef DEBUG_CELLPOINTS @@ -1001,9 +958,9 @@ void Scenario::scheduleFireSpread(const Event& event) // need to merge new points back into cells that didn't spread points_.merge(unburnable_, cell_pts); // if we move everything out of points_ we can parallelize this check? - do_each(points_.map_, [&](pair& kv) { - const auto for_cell = cell(kv.first); - CellPoints& pts = kv.second; + do_each(points_.cells_, [&](CellPointsMap::map_value& kv) { + auto& [loc, pts] = kv; + const auto for_cell = cell(loc); // ******************* CHECK THIS BECAUSE IF SOMETHING IS IN HERE SHOULD IT ALWAYS HAVE // SPREAD????? *****************8 const auto& seek_spread = spread_info_.find(for_cell.key()); @@ -1011,46 +968,43 @@ void Scenario::scheduleFireSpread(const Event& event) (spread_info_.end() == seek_spread) ? 0 : seek_spread->second.maxIntensity(); // HACK: just use side-effect to log and check bounds points_log_.log(step_, STAGE_SPREAD, new_time, pts); - if (canBurn(for_cell) && max_intensity > 0) + if (canBurn(loc) && max_intensity > 0) { - const auto& spread = pts.spread_arrival_; + const auto& spread = pts.spread_arrival(); const Event fake_event{ .time = new_time, - .cell = for_cell, - .ros = spread.ros(), - .intensity = spread.intensity(), - .raz = spread.direction(), + .xy = loc, + .ros = spread.ros, + .intensity = spread.intensity, + .raz = spread.direction, .source = pts.sources() }; burn(fake_event); } - if (!unburnable_.at(for_cell.hash()) + if (!unburnable_.at(loc) // && canBurn(for_cell) - && ((survives(new_time, for_cell, new_time - arrival_[for_cell]) && !isSurrounded(for_cell)) - )) + && ((survives(new_time, for_cell, new_time - arrival_[loc]) && !isSurrounded(loc)))) { points_log_.log(step_, STAGE_CONDENSE, new_time, pts); - const auto r = for_cell.row(); - const auto c = for_cell.column(); - const Location loc{r, c}; - std::swap(points_.map_[loc], pts); + // CHECK: this is already pointed at the right thing, no? + // std::swap(pts0.second, pts); } else { // just inserted false, so make sure unburnable gets updated // whether it went out or is surrounded just mark it as unburnable - unburnable_.set(for_cell.hash()); + unburnable_.set(loc); // not swapping means these points get dropped } }); logging::extensive( - "{:s} Spreading {:d} cells until {:f}", log_prefix_, points_.map_.size(), new_time + "{:s} Spreading {:d} cells until {:f}", log_prefix_, points_.cells_.size(), new_time ); addEvent(Event{.time = new_time, .type = Event::Type::FireSpread}); } MathSize Scenario::currentFireSize() const { return intensity_->fireSize(); } -bool Scenario::canBurn(const Cell& location) const { return intensity_->canBurn(location); } -bool Scenario::hasBurned(const Location& location) const { return intensity_->hasBurned(location); } +bool Scenario::canBurn(const XYIdx& location) const { return intensity_->canBurn(location); } +bool Scenario::hasBurned(const XYIdx& location) const { return intensity_->hasBurned(location); } void Scenario::endSimulation() noexcept { logging::verbose("{:s} Ending simulation", log_prefix_); diff --git a/src/cpp/fs/Scenario.h b/src/cpp/fs/Scenario.h index 1b4828caed7..f691e3aeca8 100644 --- a/src/cpp/fs/Scenario.h +++ b/src/cpp/fs/Scenario.h @@ -5,8 +5,8 @@ #include "CellPoints.h" #include "FireSpread.h" #include "FireWeather.h" -#include "InnerPos.h" #include "IntensityMap.h" +#include "Location.h" #include "LogPoints.h" #include "Model.h" #include "Settings.h" @@ -15,7 +15,6 @@ namespace fs { class IObserver; struct Event; -using PointSet = vector; /** * \brief Deleter for IObserver to get around incomplete class with unique_ptr */ @@ -45,95 +44,6 @@ class Scenario */ [[nodiscard]] static size_t total_steps() noexcept; virtual ~Scenario(); - /** - * \brief Constructor - * \param model Model running this Scenario - * \param id Identifier - * \param weather Wather stream to use - * \param start_time Start time for simulation - * \param perimeter Perimeter to initialize with - * \param start_point StartPoint to use sunrise/sunset times from - * \param start_day First day of simulation - * \param last_date Last day of simulation - */ - Scenario( - Model* model, - size_t id, - const ptr weather, - DurationSize start_time, - const shared_ptr& perimeter, - const StartPoint& start_point, - Day start_day, - Day last_date - ); - /** - * \brief Constructor - * \param model Model running this Scenario - * \param id Identifier - * \param weather Hourly weather stream to use - * \param weather Weather stream to use for spread and extinction probability - * \param start_time Start time for simulation - * \param start_cell Cell to start ignition in - * \param start_point StartPoint to use sunrise/sunset times from - * \param start_day First day of simulation - * \param last_date Last day of simulation - */ - Scenario( - Model* model, - size_t id, - ptr weather, - DurationSize start_time, - const shared_ptr& start_cell, - const StartPoint& start_point, - Day start_day, - Day last_date - ); - /** - * \brief Constructor - * \param model Model running this Scenario - * \param id Identifier - * \param weather Hourly weather stream to use - * \param weather Weather stream to use for spread and extinction probability - * \param start_time Start time for simulation - * \param perimeter Perimeter to initialize with - * \param start_point StartPoint to use sunrise/sunset times from - * \param start_day First day of simulation - * \param last_date Last day of simulation - */ - Scenario( - Model* model, - size_t id, - ptr weather, - ptr weather_daily, - DurationSize start_time, - const shared_ptr& perimeter, - const StartPoint& start_point, - Day start_day, - Day last_date - ); - /** - * \brief Constructor - * \param model Model running this Scenario - * \param id Identifier - * \param weather Hourly weather stream to use - * \param weather Weather stream to use for spread and extinction probability - * \param start_time Start time for simulation - * \param start_cell Cell to start ignition in - * \param start_point StartPoint to use sunrise/sunset times from - * \param start_day First day of simulation - * \param last_date Last day of simulation - */ - Scenario( - Model* model, - size_t id, - const ptr weather, - const ptr weather_daily, - DurationSize start_time, - const shared_ptr& start_cell, - const StartPoint& start_point, - Day start_day, - Day last_date - ); Scenario(Scenario&& rhs) noexcept; Scenario(const Scenario& rhs) = delete; Scenario& operator=(Scenario&& rhs) noexcept; @@ -146,7 +56,7 @@ class Scenario * \return This */ [[nodiscard]] Scenario* reset_with_new_start( - const shared_ptr& start_cell, + const XYIdx& start_cell, ptr final_sizes ); /** @@ -171,37 +81,13 @@ class Scenario * \param Whether to log a warning about this being cancelled */ void cancel(bool show_warning) noexcept; - /** - * \brief Get Cell for given row and column - * \param row Row - * \param column Column - * \return Cell for given row and column - */ [[nodiscard]] - Cell cell(const Idx row, const Idx column) const - { - return model_->cell(row, column); - } - /** - * \brief Get Cell for given Location - * \param location Location - * \return Cell for given Location - */ - template - [[nodiscard]] constexpr Cell cell(const Position

& position) const + Cell cell(const XYIdx xy) const { - return model_->cell(position); + return model_->cell(xy); } - /** - * \brief Number of rows - * \return Number of rows - */ - [[nodiscard]] constexpr Idx rows() const { return model_->rows(); } - /** - * \brief Number of columns - * \return Number of columns - */ - [[nodiscard]] constexpr Idx columns() const { return model_->columns(); } + [[nodiscard]] constexpr Idx height() const { return model_->height(); } + [[nodiscard]] constexpr Idx width() const { return model_->width(); } /** * \brief Cell width and height (m) * \return Cell width and height (m) @@ -315,18 +201,7 @@ class Scenario * \param location Location to check if is surrounded * \return Whether or not the given Location is surrounded by cells that are burnt */ - [[nodiscard]] bool isSurrounded(const Location& location) const; - template - [[nodiscard]] bool isSurrounded(const Position

& position) const - { - return isSurrounded(Location{position.hash()}); - } - /** - * \brief Cell that InnerPos falls within - * \param p InnerPos - * \return Cell that InnerPos falls within - */ - [[nodiscard]] Cell cell(const InnerPos& p) const noexcept; + [[nodiscard]] bool isSurrounded(const XYIdx& location) const; /** * \brief Run the Scenario * \param probabilities map to update ProbabilityMap for times base on Scenario results @@ -348,18 +223,13 @@ class Scenario * \param location Cell * \return Whether or not a Cell can burn */ - [[nodiscard]] bool canBurn(const Cell& location) const; + [[nodiscard]] bool canBurn(const XYIdx& location) const; /** * \brief Whether or not Location has burned already * \param location Location to check * \return Whether or not Location has burned already */ - [[nodiscard]] bool hasBurned(const Location& location) const; - template - [[nodiscard]] bool hasBurned(const Position

& position) const - { - return hasBurned(Location{position.hash()}); - } + [[nodiscard]] bool hasBurned(const XYIdx& location) const; /** * \brief Add an Event to the queue * \param event Event to add @@ -496,6 +366,8 @@ class Scenario protected: string log_prefix_{}; + +public: /** * \brief Constructor * \param model Model running this Scenario @@ -514,24 +386,13 @@ class Scenario const ptr weather_daily, DurationSize start_time, const shared_ptr& perimeter, - const shared_ptr& start_cell, + const XYIdx& start_xy, StartPoint start_point, Day start_day, Day last_date ); - Scenario( - Model* model, - size_t id, - const ptr weather, - const ptr weather_daily, - DurationSize start_time, - const shared_ptr& perimeter, - const shared_ptr& start_cell, - StartPoint start_point, - Day start_day, - Day last_date, - const Settings& settings - ); + +protected: /** * \brief Observers to be notified when cells burn */ @@ -552,9 +413,6 @@ class Scenario * \brief Current time for this Scenario */ DurationSize current_time_; - /** - * \brief Map of Cells to the PointSets within them - */ CellPointsMap points_; /** * \brief Contains information on cells that are not burnable @@ -579,7 +437,7 @@ class Scenario /** * \brief Map of when Cell had first Point arrive in it */ - map arrival_{}; + map arrival_{}; /** * \brief Maximum rate of spread for current time */ @@ -587,7 +445,7 @@ class Scenario /** * \brief Cell that the Scenario starts from if no Perimeter */ - shared_ptr start_cell_{nullptr}; + XYIdx start_xy_{}; /** * \brief Hourly weather to use for this Scenario */ diff --git a/src/cpp/fs/Settings.cpp b/src/cpp/fs/Settings.cpp index 383b871c6eb..25def2048ac 100644 --- a/src/cpp/fs/Settings.cpp +++ b/src/cpp/fs/Settings.cpp @@ -387,7 +387,7 @@ Settings::Settings(const string dir_binary, const string dir_root) noexcept } } } - catch (const std::exception& ex) + catch (const std::exception&) { // failed to read settings but just use defaults } diff --git a/src/cpp/fs/SortedVector.h b/src/cpp/fs/SortedVector.h new file mode 100644 index 00000000000..115aaec2b35 --- /dev/null +++ b/src/cpp/fs/SortedVector.h @@ -0,0 +1,164 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +#ifndef FS_SORTEDVECTOR_H +#define FS_SORTEDVECTOR_H +#include "stdafx.h" +namespace fs +{ +template +vector merge(const vector& lhs, const vector& rhs) noexcept +{ + vector out{}; + // maximum size is if no overlap + out.reserve(lhs.size() + rhs.size()); + auto it_lhs = lhs.begin(); + auto it_rhs = rhs.begin(); + auto end_lhs = lhs.end(); + auto end_rhs = rhs.end(); + while (true) + { + if (end_lhs == it_lhs) + { + while (end_rhs != it_rhs) + { + out.emplace_back(*it_rhs); + ++it_rhs; + } + break; + } + if (end_rhs == it_rhs) + { + while (end_lhs != it_lhs) + { + out.emplace_back(*it_lhs); + ++it_lhs; + } + break; + } + // at end of neither so pick lower value + const T& v0 = *it_lhs; + const T& v1 = *it_rhs; + if (v0 < v1) + { + out.emplace_back(v0); + ++it_lhs; + } + else if (v0 > v1) + { + out.emplace_back(v1); + ++it_rhs; + } + else + { + out.emplace_back(v0); + ++it_lhs; + ++it_rhs; + } + } +} +template +class SortedVector +{ + std::vector values_{}; + +public: + ~SortedVector() = default; + /** + * \brief Construct empty SortedVector + */ + SortedVector() = default; + SortedVector(const SortedVector& rhs); + SortedVector(SortedVector&& rhs) noexcept; + SortedVector& operator=(const SortedVector& rhs) noexcept; + SortedVector& operator=(SortedVector&& rhs) noexcept; + auto insert(const T& v) noexcept + { + auto it = std::lower_bound(values_.begin(), values_.end(), v); + return values_.insert(it, v); + } + auto contains(const T& v) const noexcept + { + auto it = std::lower_bound(values_.begin(), values_.end(), v); + return it != values_.end(); + } + [[nodiscard]] size_t size() const noexcept { return values_.size(); } + void merge(const SortedVector& rhs) noexcept { values_ = merge(values_, rhs.values_); } +}; +template +class LinearMap +{ + using value_type = std::pair; + // FIX: duplicate SortedVector for now + std::vector values_{}; + +public: + static auto compare_function(const value_type& kv0, const value_type& kv1) + { + return kv0.first <=> kv1.first; + }; + ~LinearMap() = default; + /** + * \brief Construct empty LinearMap + */ + LinearMap() = default; + LinearMap(const LinearMap& rhs); + LinearMap(LinearMap&& rhs) noexcept; + LinearMap& operator=(const LinearMap& rhs) noexcept; + LinearMap& operator=(LinearMap&& rhs) noexcept; + template + auto try_emplace( + const K& k1, + std::function fct_merge, + Args... args + ) noexcept + { + // find lower bound based on key only + auto it = std::lower_bound(values_.begin(), values_.end(), compare_function); + if (values_.end() != it) + { + auto& [k0, v0] = *it; + if (k0 == k1) + { + // if keys match then merge values and keep key + fct_merge(k0, v0, V{args...}); + // key existed + return std::pair{it, true}; + } + // keys don't match + } + // insert at key position + it = values_.insert(it, V{args...}); + // key did not exist + return std::pair{it, false}; + } + auto insert( + const value_type& kv, + std::function fct_merge + ) noexcept + { + // find lower bound based on key only + auto it = std::lower_bound(values_.begin(), values_.end(), compare_function); + if (values_.end() != it) + { + auto& [k0, v0] = *it; + auto& [k1, v1] = kv; + if (k0 == k1) + { + // if keys match then merge values and keep key + fct_merge(k0, v0, v1); + return it; + } + // keys don't match + } + // insert at key position + return values_.insert(it, kv); + } + auto contains(const value_type& v) const noexcept + { + auto it = std::lower_bound(values_.begin(), values_.end(), compare_function); + return it != values_.end(); + } + [[nodiscard]] size_t size() const noexcept { return values_.size(); } + // void merge(const LinearMap& rhs) noexcept { values_ = merge(values_, rhs.values_); } +}; +} +#endif diff --git a/src/cpp/fs/SpreadAlgorithm.cpp b/src/cpp/fs/SpreadAlgorithm.cpp index 07733f4e34a..d8c03df6aa0 100644 --- a/src/cpp/fs/SpreadAlgorithm.cpp +++ b/src/cpp/fs/SpreadAlgorithm.cpp @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ #include "SpreadAlgorithm.h" +#include "FireSpread.h" #include "FuelType.h" #include "unstable.h" #include "Util.h" @@ -52,17 +53,17 @@ HorizontalAdjustment horizontal_adjustment(const AspectSize slope_azimuth, const return false; } const auto ros_cell = ros / cell_size_; - const auto intensity = fire_intensity(tfc, ros); + const auto intensity = static_cast(fire_intensity(tfc, ros)); // spreading, so figure out offset from current point - offsets.emplace_back( + offsets.push_back(ROSOffset{ intensity, ros, direction.asDegrees(), Offset{ - static_cast(ros_cell * sin(direction)), - static_cast(ros_cell * cos(direction)) + static_cast(ros_cell * sin(direction)), + static_cast(ros_cell * cos(direction)) } - ); + }); return true; }; // if not over spread threshold then don't spread @@ -158,17 +159,17 @@ HorizontalAdjustment horizontal_adjustment(const AspectSize slope_azimuth, const return false; } const auto ros_cell = ros / cell_size_; - const auto intensity = fire_intensity(tfc, ros); + const auto intensity = static_cast(fire_intensity(tfc, ros)); // spreading, so figure out offset from current point - offsets.emplace_back( + offsets.push_back(ROSOffset{ intensity, ros, Direction{direction}, Offset{ - static_cast(ros_cell * sin(direction)), - static_cast(ros_cell * cos(direction)) + static_cast(ros_cell * sin(direction)), + static_cast(ros_cell * cos(direction)) } - ); + }); #ifdef DEBUG_POINTS const auto s1 = offsets.size(); logging::check_equal(s0 + 1, s1, "offsets.size()"); diff --git a/src/cpp/fs/SpreadAlgorithm.h b/src/cpp/fs/SpreadAlgorithm.h index 2d59ff8a9aa..a301a5c8347 100644 --- a/src/cpp/fs/SpreadAlgorithm.h +++ b/src/cpp/fs/SpreadAlgorithm.h @@ -2,7 +2,7 @@ #ifndef FS_SPREADALGORITHM_H #define FS_SPREADALGORITHM_H #include "stdafx.h" -#include "InnerPos.h" +#include "FireSpread.h" namespace fs { using HorizontalAdjustment = std::function; diff --git a/src/cpp/fs/StartPoint.h b/src/cpp/fs/StartPoint.h index 0abb83e59eb..ec3d1fd21ae 100644 --- a/src/cpp/fs/StartPoint.h +++ b/src/cpp/fs/StartPoint.h @@ -47,12 +47,11 @@ class StartPoint : public Point { return get<1>(days_.at(day)); } - -private: - /** - * \brief Offset from UTC (hours) - */ - MathSize utc_offset_; + // private: + // /** + // * \brief Offset from UTC (hours) + // */ + // MathSize utc_offset_; }; } #endif diff --git a/src/cpp/fs/Statistics.h b/src/cpp/fs/Statistics.h index 5578ce6dcca..7713c535883 100644 --- a/src/cpp/fs/Statistics.h +++ b/src/cpp/fs/Statistics.h @@ -107,14 +107,20 @@ class Statistics */ explicit Statistics(vector values) // values should already be sorted - : n_(values.size()), min_(values[0]), max_(values[n_ - 1]), mean_([&]() { + : n_(values.size()), min_(values.empty() ? std::numeric_limits::max() : values[0]), + max_(values.empty() ? std::numeric_limits::min() : values[n_ - 1]), mean_([&]() { return std::accumulate( values.begin(), values.end(), 0.0, [](const auto t, const auto x) { return t + x; } ) / n_; }()), - median_(values[n_ / 2]) + median_(values.empty() ? std::numeric_limits::min() : values[n_ / 2]) { + if (values.empty()) + { + // avoiding null reference above, but don't want this to work + throw std::runtime_error("Trying to create Statistics with no input values"); + }; for (size_t i = 0; i < percentiles_.size(); ++i) { const auto pos = std::min( diff --git a/src/cpp/fs/StrictType.h b/src/cpp/fs/StrictType.h index 6b4116402db..45c0c82cad9 100644 --- a/src/cpp/fs/StrictType.h +++ b/src/cpp/fs/StrictType.h @@ -42,7 +42,7 @@ struct StrictType { return ConcreteType{static_cast(InvalidValue)}; }; - ValueType value{0.0}; + ValueType value{static_cast(0.0)}; constexpr StrictType() = default; constexpr explicit StrictType(const ValueType value) noexcept : value(value) { @@ -64,14 +64,17 @@ struct StrictType constexpr StrictType(const T& rhs) noexcept = default; T& operator=(T&& rhs) noexcept = default; T& operator=(const T& rhs) noexcept = default; - auto operator<=>(const T& rhs) const = default; + constexpr auto operator==(const T& rhs) const { return value == rhs.value; } + constexpr auto operator<=>(const T& rhs) const { return value <=> rhs.value; } [[nodiscard]] constexpr ConcreteType operator+(const T& rhs) const noexcept { - return ConcreteType{value + rhs.value}; + // HACK: avoid narrowing conversion warning + return ConcreteType{static_cast(value + rhs.value)}; } [[nodiscard]] constexpr ConcreteType operator-(const T& rhs) const noexcept { - return ConcreteType{value - rhs.value}; + // HACK: avoid narrowing conversion warning + return ConcreteType{static_cast(value - rhs.value)}; } // NOTE: should dividing by same type remove units? [[nodiscard]] constexpr ConcreteType operator*(const ValueType& rhs) const noexcept diff --git a/src/cpp/fs/Test.cpp b/src/cpp/fs/Test.cpp index 21a02ed60f3..1c338e9befd 100644 --- a/src/cpp/fs/Test.cpp +++ b/src/cpp/fs/Test.cpp @@ -4,6 +4,7 @@ #include "FireSpread.h" #include "FireWeather.h" #include "FuelLookup.h" +#include "Location.h" #include "Log.h" #include "Model.h" #include "Observer.h" @@ -49,7 +50,7 @@ class TestScenario final : public Scenario */ TestScenario( Model* model, - const shared_ptr& start_cell, + const XYIdx& start_xy, const StartPoint& start_point, const int start_date, const DurationSize end_date, @@ -60,8 +61,10 @@ class TestScenario final : public Scenario model, 1, weather, + weather, start_date, - start_cell, + nullptr, + start_xy, start_point, static_cast(start_date), static_cast(end_date) @@ -79,39 +82,39 @@ class TestScenario final : public Scenario void showSpread(const SpreadInfo& spread, ptr w, const FuelType* fuel) { // HACK: make two rows and then print so columns are aligned - std::stringstream ROW_HEADER{}; - std::stringstream ROW_DATA{}; - auto add_col = [&](const char* col, const string value) { - ROW_DATA << " " << value; - ROW_HEADER << std::format(" {:>{}s}", col, value.size()); + std::stringstream line_header{}; + std::stringstream line_data{}; + auto add_value = [&](const char* col, const string value) { + line_data << " " << value; + line_header << std::format(" {:>{}s}", col, value.size()); }; - add_col("PREC", std::format("{:5.2f}", w->prec.value)); - add_col("TEMP", std::format("{:5.1f}", w->temperature.value)); - add_col("RH", std::format("{:3g}", w->rh.value)); - add_col("WS", std::format("{:5.1f}", w->wind.speed.value)); - add_col("WD", std::format("{:3g}", w->wind.direction.value)); - add_col("FFMC", std::format("{:5.1f}", w->ffmc.value)); - add_col("DMC", std::format("{:5.1f}", w->dmc.value)); - add_col("DC", std::format("{:5g}", w->dc.value)); - add_col("ISI", std::format("{:5.1f}", w->isi.value)); - add_col("BUI", std::format("{:5.1f}", w->bui.value)); - add_col("FWI", std::format("{:5.1f}", w->fwi.value)); - add_col("GS", std::format("{:3d}", spread.percentSlope())); - add_col("SAZ", std::format("{:3d}", spread.slopeAzimuth())); + add_value("PREC", std::format("{:5.2f}", w->prec.value)); + add_value("TEMP", std::format("{:5.1f}", w->temperature.value)); + add_value("RH", std::format("{:3g}", w->rh.value)); + add_value("WS", std::format("{:5.1f}", w->wind.speed.value)); + add_value("WD", std::format("{:3g}", w->wind.direction.value)); + add_value("FFMC", std::format("{:5.1f}", w->ffmc.value)); + add_value("DMC", std::format("{:5.1f}", w->dmc.value)); + add_value("DC", std::format("{:5g}", w->dc.value)); + add_value("ISI", std::format("{:5.1f}", w->isi.value)); + add_value("BUI", std::format("{:5.1f}", w->bui.value)); + add_value("FWI", std::format("{:5.1f}", w->fwi.value)); + add_value("GS", std::format("{:3d}", spread.percentSlope())); + add_value("SAZ", std::format("{:3d}", spread.slopeAzimuth())); const auto simple_fuel = simplify_fuel_name(fuel->name()); - add_col("FUEL", std::format("{:>7s}", simple_fuel)); - add_col("GC", std::format("{:3.0g}", fuel->grass_curing(spread.nd(), *w))); - add_col("L:B", std::format("{:5.2f}", spread.lengthToBreadth())); - add_col("CBH", std::format("{:4.1f}", fuel->cbh())); - add_col("CFB", std::format("{:6.3f}", spread.crownFractionBurned())); - add_col("CFC", std::format("{:6.3f}", spread.crownFuelConsumption())); - add_col("FD", std::format("{:2c}", spread.fireDescription())); - add_col("HFI", std::format("{:6d}", static_cast(spread.maxIntensity()))); - add_col("RAZ", std::format("{:3d}", spread.headDirection().asDegreesSize())); - add_col("ROS", std::format("{:6.4g}", spread.headRos())); - add_col("SFC", std::format("{:6.4g}", spread.surfaceFuelConsumption())); - add_col("TFC", std::format("{:6.4g}", spread.totalFuelConsumption())); - cout << std::format("Calculated spread is:\n{:s}\n{:s}\n", ROW_HEADER.str(), ROW_DATA.str()); + add_value("FUEL", std::format("{:>7s}", simple_fuel)); + add_value("GC", std::format("{:3.0g}", fuel->grass_curing(spread.nd(), *w))); + add_value("L:B", std::format("{:5.2f}", spread.lengthToBreadth())); + add_value("CBH", std::format("{:4.1f}", fuel->cbh())); + add_value("CFB", std::format("{:6.3f}", spread.crownFractionBurned())); + add_value("CFC", std::format("{:6.3f}", spread.crownFuelConsumption())); + add_value("FD", std::format("{:2c}", spread.fireDescription())); + add_value("HFI", std::format("{:6d}", static_cast(spread.maxIntensity()))); + add_value("RAZ", std::format("{:3d}", spread.headDirection().asDegreesSize())); + add_value("ROS", std::format("{:6.4g}", spread.headRos())); + add_value("SFC", std::format("{:6.4g}", spread.surfaceFuelConsumption())); + add_value("TFC", std::format("{:6.4g}", spread.totalFuelConsumption())); + cout << std::format("Calculated spread is:\n{:s}\n{:s}\n", line_header.str(), line_data.str()); } string generate_test_name( const auto& fuel, @@ -167,34 +170,34 @@ string run_test( make_directory_recursive(output_directory); const auto fuel = lookup.bySimplifiedName(simplify_fuel_name(fuel_name)); auto values = vector(); - for (Idx r = 0; r < MAX_ROWS; ++r) + for (Idx y = 0; y < MAX_HEIGHT; ++y) { - for (Idx c = 0; c < MAX_COLUMNS; ++c) + for (Idx x = 0; x < MAX_WIDTH; ++x) { - values.emplace_back(r, c, slope, aspect, FuelType::safeCode(fuel)); + values.emplace_back(slope, aspect, FuelType::safeCode(fuel)); } } const Cell cell_nodata{}; TestEnvironment env{CellGrid{ TEST_GRID_SIZE, - MAX_ROWS, - MAX_COLUMNS, + MAX_WIDTH, + MAX_HEIGHT, cell_nodata.fullHash(), cell_nodata, TEST_XLLCORNER, TEST_YLLCORNER, - TEST_XLLCORNER + TEST_GRID_SIZE * MAX_COLUMNS, - TEST_YLLCORNER + TEST_GRID_SIZE * MAX_ROWS, + TEST_XLLCORNER + TEST_GRID_SIZE * MAX_WIDTH, + TEST_YLLCORNER + TEST_GRID_SIZE * MAX_HEIGHT, TEST_PROJ4, std::move(values) }}; - const Location start_location(static_cast(MAX_ROWS / 2), static_cast(MAX_COLUMNS / 2)); + const XYIdx start_xy{static_cast(MAX_WIDTH / 2), static_cast(MAX_HEIGHT / 2)}; Model model(settings.start_date.value(), output_directory, ForPoint, &env); - const auto start_cell = make_shared(model.cell(start_location)); + const auto start_cell = model.cell(start_xy); FireWeather weather(fuel, start_date, dc, dmc, ffmc, wind); - TestScenario scenario(&model, start_cell, ForPoint, start_date, end_date, &weather, final_sizes); + TestScenario scenario(&model, start_xy, ForPoint, start_date, end_date, &weather, final_sizes); const auto w = weather.at(start_date); - auto info = SpreadInfo(scenario, start_date, start_cell->key(), model.nd(start_date), w); + auto info = SpreadInfo(scenario, start_date, start_cell.key(), model.nd(start_date), w); showSpread(info, w, fuel); map> probabilities{}; logging::debug("Starting simulation"); diff --git a/src/cpp/fs/UTM.cpp b/src/cpp/fs/UTM.cpp index 99f50911fc2..7042af42780 100644 --- a/src/cpp/fs/UTM.cpp +++ b/src/cpp/fs/UTM.cpp @@ -20,7 +20,7 @@ PJ_CONTEXT* get_context() return true; }(); std::ignore = showed_once; - if (!proj_context_set_database_path(pjc, db_path.c_str(), NULL, NULL)) + if (!proj_context_set_database_path(pjc, db_path.c_str(), nullptr, nullptr)) { exit(logging::fatal("Can't set proj.db path")); } diff --git a/src/cpp/fs/debug_settings.cpp b/src/cpp/fs/debug_settings.cpp index 1d2514dbbee..278d78e4926 100644 --- a/src/cpp/fs/debug_settings.cpp +++ b/src/cpp/fs/debug_settings.cpp @@ -25,9 +25,6 @@ void show_debug_settings() #ifndef NDEBUG print_centered("DEBUG"); #endif -#ifdef DEBUG_DIRECTIONS - print_centered("DEBUG_DIRECTIONS"); -#endif #ifdef DEBUG_FUEL_VARIABLE print_centered("DEBUG_FUEL_VARIABLE"); #endif diff --git a/src/cpp/fs/debug_settings.h b/src/cpp/fs/debug_settings.h index 3876ee0f451..53fe2ed08c8 100644 --- a/src/cpp/fs/debug_settings.h +++ b/src/cpp/fs/debug_settings.h @@ -3,7 +3,6 @@ #define FS_DEBUG_SETTINGS_H // if in debug mode then set everything, otherwise uncomment turning things off if trying to debug // specific things -#define DEBUG_DIRECTIONS #define DEBUG_FUEL_VARIABLE #define DEBUG_FWI_WEATHER #define DEBUG_GRIDS @@ -18,7 +17,6 @@ #define DEBUG_ITERATOR // #define DEBUG_WEATHER #ifdef NDEBUG -#undef DEBUG_DIRECTIONS #undef DEBUG_FUEL_VARIABLE #undef DEBUG_FWI_WEATHER #undef DEBUG_GRIDS @@ -43,10 +41,9 @@ #undef DEBUG_NEW_SPREAD_CHECK #undef DEBUG_NEW_SPREAD_VERBOSE #endif -#if !defined(NDEBUG) || defined(DEBUG_DIRECTIONS) || defined(DEBUG_FUEL_VARIABLE) \ - || defined(DEBUG_FWI_WEATHER) || defined(DEBUG_GRIDS) || defined(DEBUG_POINTS) \ - || defined(DEBUG_PROBABILITY) || defined(DEBUG_SIMULATION) || defined(DEBUG_STATISTICS) \ - || defined(DEBUG_WEATHER) +#if !defined(NDEBUG) || defined(DEBUG_FUEL_VARIABLE) || defined(DEBUG_FWI_WEATHER) \ + || defined(DEBUG_GRIDS) || defined(DEBUG_POINTS) || defined(DEBUG_PROBABILITY) \ + || defined(DEBUG_SIMULATION) || defined(DEBUG_STATISTICS) || defined(DEBUG_WEATHER) #define DEBUG_ANY #endif namespace fs::debug diff --git a/src/cpp/fs/project.cpp b/src/cpp/fs/project.cpp index 4cc5c79751f..719bc7243ac 100644 --- a/src/cpp/fs/project.cpp +++ b/src/cpp/fs/project.cpp @@ -3,7 +3,7 @@ #include namespace fs { -unique_ptr to_proj4( +std::optional to_proj4( const string& proj4, const fs::Point& point, MathSize* x, diff --git a/src/cpp/fs/project.h b/src/cpp/fs/project.h index 246be4e209b..f6699d1f336 100644 --- a/src/cpp/fs/project.h +++ b/src/cpp/fs/project.h @@ -7,7 +7,7 @@ namespace fs { using fs::FullCoordinates; using fs::MathSize; -unique_ptr to_proj4( +std::optional to_proj4( const string& proj4, const fs::Point& point, MathSize* x, diff --git a/src/cpp/fs/stdafx.h b/src/cpp/fs/stdafx.h index ae68f09ad67..3e86fb67799 100644 --- a/src/cpp/fs/stdafx.h +++ b/src/cpp/fs/stdafx.h @@ -139,14 +139,10 @@ static constexpr CellIndex DIRECTION_SW = 0b00010000; static constexpr CellIndex DIRECTION_NE = 0b00100000; static constexpr CellIndex DIRECTION_NW = 0b01000000; static constexpr CellIndex DIRECTION_SE = 0b10000000; -/** - * \brief A row or column index for a grid - */ +// x or y index for a grid using Idx = int16_t; constexpr auto INVALID_INDEX = std::numeric_limits::min(); -/** - * \brief A row or column index for a grid not in memory yet - */ +// x or y index for a grid not in memory yet using FullIdx = int64_t; /** * \brief Type used for perimeter raster values (uses [0, 1]) @@ -181,10 +177,6 @@ using SlopeSize = uint16_t; * \brief Type used for storing intensities */ using IntensitySize = uint32_t; -/** - * \brief Type used for storing distances within cells - */ -using DistanceSize = double; /** * \brief Type used for storing locations within cells */ @@ -210,16 +202,10 @@ constexpr auto NO_ROS = static_cast(0.0); */ using Day = uint16_t; static constexpr Day MAX_DAYS = 366; -/** - * \brief Maximum number of columns for an Environment - */ -static constexpr Idx MAX_COLUMNS = 4096; -/** - * \brief Maximum number of rows for an Environment - */ -static constexpr Idx MAX_ROWS = MAX_COLUMNS; +static constexpr Idx MAX_WIDTH = 4096; +static constexpr Idx MAX_HEIGHT = MAX_WIDTH; static constexpr Idx PREFERRED_TILE_WIDTH = 256; -static constexpr Idx TILE_WIDTH = min(MAX_COLUMNS, static_cast(PREFERRED_TILE_WIDTH)); +static constexpr Idx TILE_WIDTH = min(MAX_WIDTH, static_cast(PREFERRED_TILE_WIDTH)); /** * \brief Maximum aspect value (360 == 0) */ @@ -292,25 +278,34 @@ using DegreesSize = uint16_t; * \brief Array of results of a function for all possible integer angles in degrees */ using AngleTableArray = array; -/** - * \brief Size to use for representing the data in a Cell - */ -using Topo = uint64_t; /** * \brief Size to use for representing sub-coordinates for location within a Cell */ using SubSize = uint16_t; /** - * \brief Coordinates (row, column, sub-row, sub-column) + * \brief Coordinates ((x, y), (sub-x, sub-y)) */ -using Coordinates = tuple; +struct Coordinates +{ + Idx x; + Idx y; + SubSize x_sub; + SubSize y_sub; +}; /** - * \brief FullCoordinates (row, column, sub-row, sub-column) + * \brief FullCoordinates ((x, y), (sub-x, sub-y)) */ -using FullCoordinates = tuple; +struct FullCoordinates +{ + FullIdx x; + FullIdx y; + SubSize x_sub; + SubSize y_sub; +}; /** * \brief Type of clock to use for times */ using Clock = std::chrono::steady_clock; +using namespace std::chrono_literals; } #endif diff --git a/src/cpp/test_fwi.cpp b/src/cpp/test_fwi.cpp index b3bc66211b7..89d64453837 100644 --- a/src/cpp/test_fwi.cpp +++ b/src/cpp/test_fwi.cpp @@ -13,6 +13,15 @@ using settings::Settings; using namespace std; using namespace fs::fwireference; constexpr auto DEFAULT_LATITUDE = 46.0; +struct line_type +{ + int month; + int day; + MathSize temp; + MathSize rhum; + MathSize wind; + MathSize prcp; +}; int test_fwi_file( const char* file_in, const char* file_out, @@ -30,8 +39,7 @@ int test_fwi_file( Dmc dmc0_{dmc0}; Dc dc0_{dc0}; // HACK: load file once and buffer - using row_type = tuple; - static map> buffered_files{}; + static map> buffered_files{}; auto buffer = buffered_files[file_in]; if (buffer.empty()) { @@ -46,7 +54,7 @@ int test_fwi_file( { istringstream ss(line); ss >> month >> day >> temp >> rhum >> wind >> prcp; - buffer.emplace_back(month, day, temp, rhum, wind, prcp); + buffer.push_back(line_type{month, day, temp, rhum, wind, prcp}); } inputFile.close(); } @@ -65,14 +73,14 @@ int test_fwi_file( logging::debug("Testing FWI generated from {:s} for latitude {:g}", file_in, latitude); const Latitude latitude_{latitude}; /* Main loop for calculating indices */ - for (auto row : buffer) + for (auto line : buffer) { - month = std::get<0>(row); - day = std::get<1>(row); - temp = std::get<2>(row); - rhum = std::get<3>(row); - wind = std::get<4>(row); - prcp = std::get<5>(row); + month = line.month; + day = line.day; + temp = line.temp; + rhum = line.rhum; + wind = line.wind; + prcp = line.prcp; const auto month_{Month::from_ordinal(month)}; static constexpr MathSize EPSILON{std::numeric_limits::epsilon()}; Temperature temp_{temp};