diff --git a/.containerignore b/.containerignore new file mode 100644 index 00000000..f1ec957e --- /dev/null +++ b/.containerignore @@ -0,0 +1,8 @@ +* +!Cell2Fire/*.cpp +!Cell2Fire/*.h +!Cell2Fire/makefile +!test/unit_tests/*.cpp +!test/model +!test/target_results.zip +!test/test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cb5cbe3..310472a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y g++ make libboost-dev libtiff-dev catch2 + sudo apt-get install -y g++ make libboost-random-dev libtiff-dev catch2 - name: Build run: | diff --git a/.github/workflows/mt19937_boost.yml b/.github/workflows/mt19937_boost.yml new file mode 100644 index 00000000..356a8170 --- /dev/null +++ b/.github/workflows/mt19937_boost.yml @@ -0,0 +1,89 @@ +--- +name: rng boost::mt19937 cross-os congruence +on: + workflow_dispatch: +jobs: + build-and-run: + name: Build and run (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update -q + sudo apt-get install -y build-essential libboost-random-dev + - name: Install dependencies (macOS) + if: startsWith(matrix.os, 'macos') + run: | + brew update + brew install llvm boost + echo "Using clang++ from Homebrew" + echo "CXX=$(brew --prefix)/opt/llvm/bin/clang++" >> $GITHUB_ENV + - name: Set up Visual Studio shell + if: startsWith(matrix.os, 'windows') + uses: egor-tensin/vs-shell@v2 + with: + arch: x64 + - name: Set up Vcpkg (Windows) + if: startsWith(matrix.os, 'windows') + shell: powershell + run: | + # Use preinstalled vcpkg from runner images + & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install boost-random:x64-windows + Write-Host "VCPKG include: $env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\include" + "$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\include" | Out-File -FilePath include_path.txt -Encoding ascii + - name: Build (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: | + c++ -std=c++17 test/tech/mt19937_boost.cpp -o mt19937_boost + - name: Build (macOS) + if: startsWith(matrix.os, 'macos') + run: | + "$CXX" -std=c++17 -I"$(brew --prefix)/include" -L"$(brew --prefix)/lib" test/tech/mt19937_boost.cpp -o mt19937_boost + - name: Build (Windows) + if: startsWith(matrix.os, 'windows') + shell: powershell + run: | + # Add include path and compile + $include = Join-Path $env:VCPKG_INSTALLATION_ROOT 'installed\x64-windows\include' + $env:INCLUDE = "$include;$env:INCLUDE" + cmd /c "cl /std:c++17 /EHsc /I `"$include`" test\tech\mt19937_boost.cpp /Fe:mt19937_boost.exe" + - name: Run emitter + shell: bash + run: | + if [[ "$RUNNER_OS" == "Windows" ]]; then + ./mt19937_boost.exe > mt19937_${{ runner.os }}.txt + else + ./mt19937_boost > mt19937_${{ runner.os }}.txt + fi + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mt19937_${{ runner.os }} + path: mt19937_${{ runner.os }}.txt + compare: + name: Compare outputs + runs-on: ubuntu-latest + needs: build-and-run + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Diff Ubuntu vs macOS + run: | + diff -u artifacts/mt19937_Linux/mt19937_Linux.txt artifacts/mt19937_macOS/mt19937_macOS.txt + - name: Diff Ubuntu vs Windows (normalize CRLF) + run: |- + sudo apt-get install -y dos2unix + dos2unix artifacts/mt19937_Windows/mt19937_Windows.txt + diff -u artifacts/mt19937_Windows/mt19937_Windows.txt artifacts/mt19937_Linux/mt19937_Linux.txt diff --git a/.github/workflows/mt19937_std.yml b/.github/workflows/mt19937_std.yml new file mode 100644 index 00000000..f15d934d --- /dev/null +++ b/.github/workflows/mt19937_std.yml @@ -0,0 +1,79 @@ +--- +name: rng std::mt19937 cross-os congruence +on: + workflow_dispatch: +jobs: + build-and-run: + name: Build and run (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup MSBuild (Windows) + if: startsWith(matrix.os, 'windows') + uses: microsoft/setup-msbuild@v2 + - name: Ensure compiler (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update -q + sudo apt-get install -y build-essential + shell: bash + - name: Ensure compiler (macOS) + if: startsWith(matrix.os, 'macos') + run: | + xcode-select --install || true + shell: bash + - name: Build (Linux/macOS) + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + run: | + c++ -std=c++17 test/tech/mt19937_std.cpp -o mt19937_std + shell: bash + - name: Set up Visual Studio shell + if: startsWith(matrix.os, 'windows') + uses: egor-tensin/vs-shell@v2 + with: + arch: x64 + - name: Build (Windows) + if: startsWith(matrix.os, 'windows') + run: | + cl /std:c++17 test\tech\mt19937_std.cpp /Fe:mt19937_std.exe + shell: pwsh + - name: Run and capture (Linux/macOS) + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + run: | + ./mt19937_std > mt19937_${{ runner.os }}.txt + shell: bash + - name: Run and capture (Windows) + if: startsWith(matrix.os, 'windows') + run: | + .\mt19937_std.exe | Out-File -FilePath mt19937_${{ runner.os }}.txt -Encoding ascii + shell: pwsh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mt19937_${{ runner.os }} + path: mt19937_${{ runner.os }}.txt + compare: + name: Compare outputs + runs-on: ubuntu-latest + needs: build-and-run + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Diff Ubuntu vs macOS + run: | + diff -u artifacts/mt19937_Linux/mt19937_Linux.txt artifacts/mt19937_macOS/mt19937_macOS.txt + - name: Diff Ubuntu vs Windows (normalize CRLF) + run: |- + sudo apt-get install -y dos2unix + dos2unix artifacts/mt19937_Windows/mt19937_Windows.txt + diff -u artifacts/mt19937_Windows/mt19937_Windows.txt artifacts/mt19937_Linux/mt19937_Linux.txt diff --git a/Cell2Fire/ReadCSV.cpp b/Cell2Fire/ReadCSV.cpp index 8ede4cdf..98bee05b 100644 --- a/Cell2Fire/ReadCSV.cpp +++ b/Cell2Fire/ReadCSV.cpp @@ -5,16 +5,41 @@ #include "tiffio.h" #include -#include #include #include #include #include +#include #include #include #include #include +// Local minimal replacement for boost::algorithm::split with is_any_of semantics. +// C++14-friendly (uses std::string, not std::string_view). +// When compress == false, preserves empty tokens (equivalent to token_compress_off). +// When compress == true, collapses consecutive delimiters. +static std::vector +split_any_of(const std::string& s, const std::string& delims, bool compress) +{ + std::vector out; + const size_t n = s.size(); + size_t i = 0; + while (i <= n) { + size_t j = s.find_first_of(delims, i); + if (j == std::string::npos) j = n; + out.emplace_back(s.substr(i, j - i)); + if (j == n) break; + if (compress) { + i = j + 1; + while (i < n && delims.find(s[i]) != std::string::npos) ++i; + } else { + i = j + 1; + } + } + return out; +} + /** * Creates an instance of CSVReader. * @param filename name of file to read @@ -104,8 +129,7 @@ CSVReader::getData(string filename) } else { - std::vector vec; - boost::algorithm::split(vec, line, boost::is_any_of(this->delimeter)); + std::vector vec = split_any_of(line, this->delimeter, false); dataList.push_back(vec); } } @@ -206,8 +230,7 @@ CSVReader::getData(string filename) { while (getline(file, line)) { - std::vector vec; - boost::algorithm::split(vec, line, boost::is_any_of(this->delimeter)); + std::vector vec = split_any_of(line, this->delimeter, false); dataList.push_back(vec); } } diff --git a/Cell2Fire/ReadCSV.h b/Cell2Fire/ReadCSV.h index 7fe5ebe1..27c0a08a 100644 --- a/Cell2Fire/ReadCSV.h +++ b/Cell2Fire/ReadCSV.h @@ -7,7 +7,6 @@ #include "tiffio.h" #include -#include #include #include #include diff --git a/Cell2Fire/WriteCSV.cpp b/Cell2Fire/WriteCSV.cpp index 46b980e2..743a6286 100644 --- a/Cell2Fire/WriteCSV.cpp +++ b/Cell2Fire/WriteCSV.cpp @@ -1,7 +1,6 @@ #include "WriteCSV.h" #include -#include #include #include #include diff --git a/Cell2Fire/WriteCSV.h b/Cell2Fire/WriteCSV.h index 356ffd81..43f75512 100644 --- a/Cell2Fire/WriteCSV.h +++ b/Cell2Fire/WriteCSV.h @@ -2,7 +2,6 @@ #define WRITECSV #include -#include #include #include #include diff --git a/README.md b/README.md index e61ceb36..0bb67b8f 100644 --- a/README.md +++ b/README.md @@ -105,34 +105,30 @@ Cell2Fire --final-grid --output-messages --out-ros --sim S --nsims 2 --seed 123 # check the results: to convert to tiff or see the results in QGIS, use the plugin ``` -### Get the container - -![Tutorial here](container/README.md), TL;DR: - +### Containerized +TL;DR: ```bash -# have or install podman (or docker) -sudo apt install podman - -# download the container [Dockerfile](https://github.com/fire2a/C2F-W/raw/refs/heads/feature-containerize/container/Dockerfile) -wget https://github.com/fire2a/C2F-W/raw/refs/heads/feature-containerize/container/Dockerfile - -# build -podman build -t c2f -f Dockerfile . - -# Done! Usage mounting the instance and results directories into the container +podman build -t cell2fire -f container/Containerfile . mkdir results -podman run -v $(pwd):/mnt c2f --input-instance-folder /mnt/data/Kitral/Portillo-tif --output-folder /mnt/results --nsims 3 --sim K --grids | tee results/log.txt +podman run -v $(pwd):/mnt cell2fire \ + --input-instance-folder /mnt/data/ScottAndBurgan/Vilopriu_2013-tif \ + --output-folder /mnt/results \ + --nsims 3 --sim S \ + --output-messages --ignitionsLog | tee results/log.txt +rm -r results/* ``` +[More options and tutorial here](container/README.md) + ## Collaborative Compile it ```bash # dependencies -sudo apt install g++-12 libboost-all-dev libeigen3-dev libtiff-dev +sudo apt install g++ libboost-random-dev libtiff-dev # or brew -brew install gcc@12 libomp eigen boost libtiff # llvm ? +brew install gcc@12 libomp boost libtiff # llvm ? # fork & clone git clone git@github.com:/C2F-W.git diff --git a/container/Containerfile b/container/Containerfile new file mode 100644 index 00000000..0c892c31 --- /dev/null +++ b/container/Containerfile @@ -0,0 +1,21 @@ +FROM debian:stable-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + g++ \ + libboost-random-dev \ + libtiff-dev \ + make && \ + rm -rf /var/lib/apt/lists/* + +COPY Cell2Fire Cell2Fire + +WORKDIR /Cell2Fire + +RUN make clean && \ + make + +ENTRYPOINT ["/Cell2Fire/Cell2Fire"] + diff --git a/container/Containerfile+tests b/container/Containerfile+tests new file mode 100644 index 00000000..b6a772dd --- /dev/null +++ b/container/Containerfile+tests @@ -0,0 +1,28 @@ +FROM debian:stable-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + catch2 \ + g++ \ + libboost-random-dev \ + libtiff-dev \ + make \ + unzip && \ + rm -rf /var/lib/apt/lists/* + +COPY Cell2Fire Cell2Fire + +COPY test /test + +WORKDIR /Cell2Fire + +RUN make clean && \ + make tests && \ + make + +RUN cd /test && ./test.sh + +ENTRYPOINT ["/Cell2Fire/Cell2Fire"] + diff --git a/container/Dockerfile b/container/Dockerfile index 95addc43..bc5ff3ec 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -2,7 +2,7 @@ FROM debian:stable LABEL authors="matilde" ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y g++-12 libboost-all-dev libeigen3-dev libtiff-dev git make unzip lsb-release catch2 +RUN apt-get update && apt-get install -y g++ libboost-random-dev libtiff-dev git make unzip catch2 RUN git clone https://github.com/fire2a/C2F-W.git diff --git a/container/README.md b/container/README.md index 2d09fc84..6f85892f 100644 --- a/container/README.md +++ b/container/README.md @@ -1,31 +1,72 @@ -This folder contains a containerized version of Cell2Fire. The container runs the latest version of Cell2Fire without having to manually build and configure the application on your system. The container is compatible with both Podman and Docker. We recommend using Podman, but the instructions found here are easily translated into Docker. +# Cell2Fire Containers -## TL;DR +This folder contains a containerized versions of Cell2Fire. The containers runs the latest version of Cell2Fire without having to manually build and configure the application on your system. + + +### Requirement +You only need Podman installed on your system to build and run the container. Replace `podman` with `docker` if needed. +```bash +sudo apt install podman ``` -sudo apt install podman git -git clone git@github.com:Cell2Fire/C2F-W.git -cd C2F-W/container -podman build -t c2f . -cd ../test + +## Usage +After building the container image (named `cell2fire` in the next example), you can run Cell2Fire simulations using the container. The container accepts the same parameters as if you were running the compiled binary directly. + +The simplest use is by mounting the instance and results directories into the container using volumes (`-v $(pwd):/mnt`) and deleting the container after the run (`--rm`). +```bash mkdir results -~/C2F-W/test$ podman run -v $(pwd):/mnt c2f --input-instance-folder /mnt/model/fbp-asc --output-folder /mnt/results --nsims 3 --sim C --grids +podman run --rm -v $(pwd):/mnt cell2fire \ + --input-instance-folder /mnt/data/Kitral/Portezuelo-tif \ + --output-folder /mnt/results \ + --output-messages \ + --ignitionsLog \ + --nsims 3 --sim K --grids | tee results/log.txt ls results +rm -r results/* +``` + +## Select your preferred build -# unnecesary if correctly addressing the output folder: -# enter the container interactively, overriding the entrypoint: -podman run -it --entrypoint /bin/bash c2f +### 1. Lightweight +``` +git clone git@github.com:Cell2Fire/C2F-W.git +cd C2F-W +podman build -t cell2fire -f container/Containerfile . ``` -## Folder Structure +### 2. Run tests too +``` +git clone git@github.com:Cell2Fire/C2F-W.git +cd C2F-W +podman build -t cell2firetests -f container/Containerfile+tests . +``` + +### 3. Minimal dependencies but heavier +Also wget and unzip is needed to get the Dockerfile and a test instance. +```bash +# get the Dockerfile +wget https://github.com/fire2a/C2F-W/raw/main/container/Dockerfile + +# build +podman build -t cell2fire -f Dockerfile . + +# get an instance +wget https://github.com/fire2a/C2F-W/releases/download/v1.0.1/Kitral-tif.zip +unzip Kitral-tif.zip -d data +``` + +## Learn More + +### Folder Structure - `container/`: Contains the files used to build and run the container. - `Dockerfile`: Defines the build instructions for the container image. -## Prerequisites +### Prerequisites Ensure you have Podman installed on your system. You can find installation instructions in the [official Podman documentation](https://podman.io/docs/installation). -## Building the Image +### Building the Image To build the container image using Podman, navigate to the `container/` directory and run the following command: @@ -35,7 +76,7 @@ podman build -t -f Dockerfile . ``` This command builds the container image and tags it as . -## Running Cell2Fire +### Running Cell2Fire Once the image is built, you can run the simulation using Podman. It accepts the same parameters as if you were running the compiled binary directly. In order for the container to have access to the input data files, we must use volumes. Simply put the `-v` or `--volume` tag followed by the path to your input files diff --git a/test/tech/mt19937.md b/test/tech/mt19937.md new file mode 100644 index 00000000..42e4b984 --- /dev/null +++ b/test/tech/mt19937.md @@ -0,0 +1,32 @@ +## mt19937 random number generator + +The purpose of these tests is to assure that random generated numbers are cross platform consistent. + +Two tests are included, running the same distributions used in Cell2Fire, but changing the origin of the random library. + +One uses boost::random the other std::random. + +These tests can be manually run on + +- https://github.com/fdobad/C2F-W/actions/workflows/mt19937_std.yml + +- https://github.com/fdobad/C2F-W/actions/workflows/mt19937_boost.yml + +as they have a workflow_dispatch directive. + +### example of running locally + +```bash +cd test/tech +g++ -std=c++11 -O2 mt19937_std.cpp -o mt19937_std +./mt19937_std | tee mt19937_std.txt +g++ -std=c++11 -O2 mt19937_boost.cpp -o mt19937_boost -lboost_random -lboost_system +./mt19937_boost | tee mt19937_boost.txt +diff mt19937_std.txt mt19937_boost.txt # not expected to be the same!! +``` + +## Results + +As of 2025-12-02 Only boost is cross platform consistent! + +When std::random get's consistent then boost dependency can be dropped easing the building times and dependencies! diff --git a/test/tech/mt19937_boost.cpp b/test/tech/mt19937_boost.cpp new file mode 100644 index 00000000..9a24c976 --- /dev/null +++ b/test/tech/mt19937_boost.cpp @@ -0,0 +1,30 @@ +#include +#include +#include + +int +main() +{ + const uint32_t seed = 123456789u; + boost::random::mt19937 eng(seed); + + boost::random::uniform_int_distribution udist(1, 1000000); + boost::random::normal_distribution ndist(0.0, 1.0); + + std::cout << "# boost::mt19937 uniform_int [1,1000000]" << std::endl; + for (int i = 0; i < 1000; ++i) + { + std::cout << udist(eng) << '\n'; + } + + // Reset engine for normal distribution to ensure same sequence base + eng.seed(seed); + std::cout << "# boost::mt19937 normal mean=0 stddev=1" << std::endl; + std::cout << std::setprecision(17); + for (int i = 0; i < 1000; ++i) + { + std::cout << ndist(eng) << '\n'; + } + + return 0; +} diff --git a/test/tech/mt19937_std.cpp b/test/tech/mt19937_std.cpp new file mode 100644 index 00000000..fedb2aaa --- /dev/null +++ b/test/tech/mt19937_std.cpp @@ -0,0 +1,30 @@ +#include +#include +#include + +int +main() +{ + const uint32_t seed = 123456789u; + std::mt19937 eng(seed); + + std::uniform_int_distribution udist(1, 1000000); + std::normal_distribution ndist(0.0, 1.0); + + std::cout << "# std::mt19937 uniform_int [1,1000000]" << std::endl; + for (int i = 0; i < 1000; ++i) + { + std::cout << udist(eng) << '\n'; + } + + // Reset engine for normal distribution to ensure same sequence base + eng.seed(seed); + std::cout << "# std::mt19937 normal mean=0 stddev=1" << std::endl; + std::cout << std::setprecision(17); + for (int i = 0; i < 1000; ++i) + { + std::cout << ndist(eng) << '\n'; + } + + return 0; +} diff --git a/test/tech/split_any_of_compare.Containerfile b/test/tech/split_any_of_compare.Containerfile new file mode 100644 index 00000000..d69ac7fc --- /dev/null +++ b/test/tech/split_any_of_compare.Containerfile @@ -0,0 +1,14 @@ +FROM debian:stable-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + g++ \ + make \ + libboost-all-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +COPY split_any_of_compare.cpp . + +CMD bash -c "g++ -std=c++17 -O2 split_any_of_compare.cpp -o split_any_of_compare && ./split_any_of_compare" diff --git a/test/tech/split_any_of_compare.cpp b/test/tech/split_any_of_compare.cpp new file mode 100644 index 00000000..ba1bfd1c --- /dev/null +++ b/test/tech/split_any_of_compare.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include + +#include + +static std::vector split_any_of(std::string_view s, + std::string_view delims, + bool compress) +{ + std::vector out; + const size_t n = s.size(); + size_t i = 0; + while (i <= n) { + size_t j = s.find_first_of(delims, i); + if (j == std::string_view::npos) j = n; + out.emplace_back(s.substr(i, j - i)); + if (j == n) break; + if (compress) { + i = j + 1; + while (i < n && delims.find(s[i]) != std::string_view::npos) ++i; + } else { + i = j + 1; + } + } + return out; +} + +static bool equal_vectors(const std::vector& a, + const std::vector& b) +{ + if (a.size() != b.size()) return false; + for (size_t i = 0; i < a.size(); ++i) { + if (a[i] != b[i]) return false; + } + return true; +} + +int main() +{ + struct Case { std::string line; std::string delims; bool compress; }; + std::vector cases = { + {"a,b,c", ",", false}, + {"a,,b,,,c", ",", false}, + {"a,,b,,,c", ",", true}, + {"a;b,c|d", ",;|", false}, + {"a;;b,,|c", ",;|", true}, + {"", ",;|", false}, + {",|;", ",;|", false}, + {",|;", ",;|", true}, + {" a b c ", " ", false}, + {" a b c ", " ", true}, + }; + + int failures = 0; + + for (const auto& cs : cases) { + // Boost split + std::vector boost_out; + if (cs.compress) { + boost::algorithm::split(boost_out, cs.line, + boost::is_any_of(cs.delims), + boost::token_compress_on); + } else { + boost::algorithm::split(boost_out, cs.line, + boost::is_any_of(cs.delims), + boost::token_compress_off); + } + + // std replacement + auto std_out = split_any_of(cs.line, cs.delims, cs.compress); + + const bool ok = equal_vectors(boost_out, std_out); + if (!ok) { + ++failures; + std::cout << "Mismatch for line='" << cs.line << "' delims='" << cs.delims + << "' compress=" << (cs.compress ? "on" : "off") << "\n"; + std::cout << " boost: ["; + for (size_t i = 0; i < boost_out.size(); ++i) { + if (i) std::cout << ", "; + std::cout << '"' << boost_out[i] << '"'; + } + std::cout << "]\n std : ["; + for (size_t i = 0; i < std_out.size(); ++i) { + if (i) std::cout << ", "; + std::cout << '"' << std_out[i] << '"'; + } + std::cout << "]\n"; + } + } + + if (failures == 0) { + std::cout << "All cases matched." << std::endl; + return 0; + } else { + std::cout << failures << " mismatches." << std::endl; + return 1; + } +} diff --git a/test/tech/split_any_of_compare.md b/test/tech/split_any_of_compare.md new file mode 100644 index 00000000..d4ff7709 --- /dev/null +++ b/test/tech/split_any_of_compare.md @@ -0,0 +1,15 @@ +# shrinking boost dependency proof + +`split_any_of_compare.cpp` implements a comparison of boost::split_any_of with a custom split_any_of function. +The code tests both functions on the same input and compares their outputs to ensure they behave identically. +Then we can remove boost::algorithm from our codebase with confidence, lightening our dependencies and build times. + +To run in isolation: + + podman build -f split_any_of_compare.Containerfile -t fire2a-split-any-of-test . + podman run --rm fire2a-split-any-of-test + +Not isolated: + + g++ -std=c++17 -I./include split_any_of_compare.cpp -o split_any_of_compare + ./split_any_of_compare