From e7544c6f5d62d14195f58e01b5a7012d5544e9e4 Mon Sep 17 00:00:00 2001 From: JessicaGarrett-NOAA <30940444+madMatchstick@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:25:55 -0400 Subject: [PATCH 1/8] Create add_to_project.yml --- .github/workflows/add_to_project.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/add_to_project.yml diff --git a/.github/workflows/add_to_project.yml b/.github/workflows/add_to_project.yml new file mode 100644 index 0000000..830b888 --- /dev/null +++ b/.github/workflows/add_to_project.yml @@ -0,0 +1,19 @@ +name: Add to Project + +on: + pull_request: + types: + - opened + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issues and PRs to formulation project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.6.1 + with: + project-url: https://github.com/orgs/NOAA-OWP/projects/30 + github-token: ${{ secrets.FORMULATION_PROJECT_ADD_TOKEN }} From 5803d94e26e65d44c983514f781de60072b2a297 Mon Sep 17 00:00:00 2001 From: Robert Bartel Date: Tue, 15 Oct 2024 11:09:14 -0400 Subject: [PATCH 2/8] Add Git, branching strategy, and release info. Updating contributing docs with more detail on Git usage, adding more detailed Git branching and info page, and adding page detailing steps for release management. --- CONTRIBUTING.md | 224 ++++++++++++++++++++++++++++++++++++-- doc/GIT_USAGE.md | 143 ++++++++++++++++++++++++ doc/RELEASE_MANAGEMENT.md | 42 +++++++ 3 files changed, 398 insertions(+), 11 deletions(-) create mode 100644 doc/GIT_USAGE.md create mode 100644 doc/RELEASE_MANAGEMENT.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f109b2..bd69ad6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Guidance on how to contribute +# Guidance on How to Contribute > All contributions to this project will be released to the public domain. > By submitting a pull request or filing a bug, issue, or @@ -7,11 +7,11 @@ There are two primary ways to help: - - Using the issue tracker, and - - Changing the code-base. +- Using the issue tracker, and +- Changing the code-base. -## Using the issue tracker +# Using the Issue Tracker Use the issue tracker to suggest feature requests, report bugs, and ask questions. This is also a great way to connect with the developers of the project as well @@ -22,11 +22,213 @@ the issue that you will take on that effort, then follow the _Changing the code- guidance below. -## Changing the code-base +# Changing the Code-Base + +* [Summary](#summary) +* [Getting Started](#getting-started) +* [Developing Changes](#developing-changes) +* [Keeping Forks Up to Date](#keeping-forks-up-to-date) + +Note that this document only discusses aspects of Git usage that should be needed for day-to-day development and contributions. For a more detailed overview of LGAR-C's use of Git, see the [GIT_USAGE](doc/GIT_USAGE.md) doc. + +## Summary + +To work with the repo and contribute changes, the basic process is as follows: + +- Create your own fork in Github +- Clone the repo locally (conventionally from your fork) and [setup your repo on your local development machine](#getting-started) +- Make sure to [keep your fork and your local clone(s) up to date](#keeping-forks-up-to-date) with the official OWP LGAR-C repo, ensuring histories remain consistent by performing [rebasing](#rebasing-development-branches) +- Create feature/fix branches from `master` when you want to contribute +- Write changes you want to contribute, commit to your local feature/fix branch, and push these commits to a branch in your personal Github fork +- Submit pull requests to the official OWP repo's `master` branch from a feature/fix branch your fork when the latter branch has a collection of changes ready to be incorporated + +## Getting Started + +In order to be able to contribute code changes, you will first need to [create a Github fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) of the official OWP repo. + +Next, set up your authentication mechanism with Github for your command line (or IDE). You can either [create an SSH key pair](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) and [add the public key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) to your Github account, or you can set up a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#using-a-personal-access-token-on-the-command-line) if you plan to clone the repo locally via HTTPS. + +After that, [clone a local development repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) from your fork, using a command similar to one of the following: + + # SSH-based clone command. Change URL to match your fork as appropriate + git clone git@github.com:your_user/LGAR-C.git + + # HTTPS-based clone command. Change URL to match your fork as appropriate + git clone https://github.com/your_user/LGAR-C.git + +You can now change directories into the local repo, which will have the _default_ branch - `master` for this repository - checked out. + + # Move into the repo directory "LGAR-C" + cd LGAR-C + + # You can verify the branch by examining the output of ... + git status + +> [!IMPORTANT] +> Git will add a [Git remote](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) named `origin` to the clone's Git configuration that points to the cloned-from repo. Because of this, the recommended convention is to clone your local repo(s) from your personal fork, thus making `origin` point to your fork. This is assumed to be the case in other parts of the documentation. + +Next, add the OWP LGAR-C repo as a second [Git remote](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) for the local clone. Doing the addition will look something like: + + # Add the remote, here using the HTTPS URL (the SSH URL would be fine also) + git remote add upstream https://github.com/NOAA-OWP/LGAR-C.git + + # Verify + git remote -v + +> [!IMPORTANT] +> The standard convention used in this doc and elsewhere is to name the Git remote for the official Github OWP repository `upstream`. In regular text, that Git remote will always be denoted like this. +> +> Git also has the more general concept of an "upstream branch" associated with "tracking branches", as discussed on the [Git Remote Branches](https://git-scm.com/book/en/v2/Git-Branching-Remote-Branches) documentation. We will use "upstream branch" when discussing these. + +Now set up the user and email in the local repo's configuration. + + git config user.name "John Doe" + git config user.email "john@doe.org" + +Alternatively, one could also set these in the machine's global Git config (or rely upon the global settings if already configured). + + git config --global user.name "John Doe" + git config --global user.email "john@doe.org" + +### Optional: Git Hooks + +While optional, Git hooks are a useful feature for helping maintain code quality in your local development repo. See the repo's [_Git Usage_](doc/GIT_USAGE.md#optional-setting-up-hook-scripts) document for more discussion on these. + +[//]: # (TODO: add section/document on code style) + + +## Developing Changes + +* [Work in a Dedicated Branch](#work-in-a-dedicated-branch) +* [Pushing Incremental Commits](#pushing-incremental-commits) +* [Submitting Pull Requests](#submitting-pull-requests) + * [Guidelines for Pull Requests](#guidelines-for-pull-requests) + +### Work in a Dedicated Branch + +When you want to contribute a fix or new feature, start by creating and checking out a local branch (e.g., `new_branch`) to contain your work. This should be based on `master` (you may need to [sync remote changes](#getting-remote-changes) first): + + # Create the new branch "new_branch" based on "master" + git branch new_branch master + + # Check out "new_branch" locally to work in it + git checkout new_branch + +Go ahead and push this new branch to your fork so it exists there as well. Use `-u` so that the local branch adds a tracking reference to the branch in your fork: + + # Assuming the convention of your fork's remote being `origin` + git push -u origin new_branch + +> [!IMPORTANT] +> While `-u` isn't strictly required, including it causes `origin/new_branch` to be set as the "upstream branch" for the local `new_branch` (with the latter being referred to as a "tracking branch"). Tracking branch relationships are described in the _Tracking Branches_ section of [Git's Remote Branches doc](https://git-scm.com/book/en/v2/Git-Branching-Remote-Branches). +> +> While it is totally optional, parts our documentation - especially example commands - may assume this tracking branch relationship has been set. + +From there begin writing and committing your changes to the branch. While up to you, it is suggested that development work be committed frequently when changes are complete and meaningful. If work requires modifying more than one file in the source, it is recommended to commit the changes independently to help avoid too large of conflicts if/when they occur. + +### Pushing Incremental Commits + +Especially if making more frequent, smaller commits as suggested above, it is a good practice to regularly push these smaller commits to your fork. If the `-u` option was used when initially pushing the branch, it is simple to check if there are local, unpushed commits. + + # The fetch is probably unnecesssary unless you work from multiple local repos + git fetch + + # Assuming your branch of interest is still checked out: + git status + + # And if there are some newer, local changes that haven't been push yet: + git push + +### Submitting Pull Requests + +Once a code contribution is finished, make sure all changes have been pushed to the branch in your fork. Then you can navigate to the OWP repo via Github's web interface and [submit a PR](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) to pull this branch into the OWP `master` branch. Verify that `master` is selected as the recipient branch. You will also need to make sure you are comparing across forks, choosing your appropriate fork and branch to pull from. Complete details on the process can be found in Github's documentation. + +[//]: # (TODO: consider moving this to its own doc and expanding on details for PRs there some) + +#### Guidelines for Pull Requests + +The following guidelines are recommended for pull requests. Following these increases the likelihood of a PR being reviewed quickly and minimizes the amount of changes likely to be needed before approval: + +* Make sure the PR has an informative and human-readable title +* Limit the scope of changes within a PR to a single goal (no scope creep) +* Ensure the resulting code can be automatically rebased into `master` without conflicts +* Make sure project coding standards, if/when defined, are followed +* Ensure resulting code passes all existing automated tests +* Ensure any changes or additions to functionality are tested +* Document new functions with a description, list of inputs, and expected output +* Flag any placeholder code is flagged and note future TODO items in comments +* Update repo-level documentation appropriately +* Select one or more appropriate reviewers when creating the PR + +#### PR Review and Requested Revisions + +Once the PR is submitted, it will be reviewed by one or more other repo contributors. Often conversations will be had within the Github PR if reviewers have questions or request revisions be made to the proposed changes. If revisions are requested, you will need to make those in your locally copy of the feature/fix branch, and then re-push that branch (and the updates) to your personal fork. Then, use the PR page in Github to re-request review. + +## Keeping Forks Up to Date + +- [A Rebase Strategy](#a-rebase-strategy) +- [Getting Remote Changes](#getting-Remote-changes) +- [Rebasing Development Branches](#rebasing-development-branches) +- [Fixing Diverging Development Branches](#fixing-diverging-development-branches) + +### A Rebase Strategy + +Development for this repo uses a *rebase* strategy for integrating code changes, rather than a *merge* strategy. More information on rebasing is [available here](https://git-scm.com/book/en/v2/Git-Branching-Rebasing), but the main takeaway is that it is important all changes are integrated into branches using this approach. Deviation from this will likely cause a bit of mess with branch commit histories, which could force rejection of otherwise-good PRs. + +### Getting Remote Changes + +When it is time to check for or apply updates from the official OWP repo to a personal fork and/or a local repo, check out the `master` branch locally and do fetch-and-rebase, which can be done with `pull` and the `--rebase` option: + + # Checkout local master branch + git checkout master + + # Fetch and rebase changes + git pull --rebase upstream master + +Then, make sure these get pushed to your personal fork. Assuming [the above-described setup](#getting-started-with-your-fork) where the local repo was cloned from the fork, and assuming the local `master` branch is currently checked out, the command for that is just: + + # Note the assumptions mentioned above that are required for this syntax + git push + +Alternatively, you can use the more explicit form: + +`git push :` + +The previous example command is effectively equivalent to running: + + # Cloning a repo from a fork created a remote for the fork named "origin"; see above assumptions + git push origin master:master + +You also can omit `:` (including the colon) and supply just the remote branch name if the appropriate local branch is still checked out. + +#### For `production` Too + +Note that the above steps to get remote changes from the official OWP repo can be applied to the `production` branch also (just swap `production` in place of `master`). `production` should not be used as the basis for feature/fix branches, but there are other reasons why one might want the latest `production` locally or in a personal fork. + +### Rebasing Development Branches + +When the steps in [Getting Remote Changes](#getting-remote-changes) do bring in new commits that update `master`, it is usually a good idea (and often necessary) to rebase any local feature/fix branches were previously created. E.g., + + # If using a development branch named 'faster_dataset_writes' + git checkout faster_dataset_writes + git rebase master + +See documentation on [the "git rebase" command](https://git-scm.com/docs/git-rebase) for more details. + +#### Interactive Rebasing + +It is possible to have more control over rebasing by doing an interactive rebase. E.g.: + + git rebase -i master + +This will open up a text editor allowing for reordering, squashing, dropping, etc., development branch commits prior to rebasing them onto the new base commit from `master`. See the [**Interactive Mode**](https://git-scm.com/docs/git-rebase#_interactive_mode) section on the rebase command for more details. + +### Fixing Diverging Development Branches + +If a local feature/fix branch is already pushed to a remote fork, and then later rebasing the local branch is necessary, doing so will cause the histories to diverge. For simple cases, the fix is to just force-push the rebased local branch. + + # To force-push to fix a divergent branch + git push -f origin feature_branch + +However, extra care is needed if multiple developers may be using the branch in the fork (e.g., a developer is collaborating with someone else on a large set of changes for some new feature). The particular considerations and best ways to go about things in such cases are outside the scope of this document. Consult Git's documentation and Google, or contact another contributor for advice. -Generally speaking, you should fork this repository, make changes in your -own fork, and then submit a pull request. All new code should have associated -unit tests that validate implemented features and the presence or lack of defects. -Additionally, the code should follow any stylistic and architectural guidelines -prescribed by the project. In the absence of such guidelines, mimic the styles -and patterns in the existing code-base. diff --git a/doc/GIT_USAGE.md b/doc/GIT_USAGE.md new file mode 100644 index 0000000..312b14c --- /dev/null +++ b/doc/GIT_USAGE.md @@ -0,0 +1,143 @@ +# Git Strategy + +Note that this document goes into detail on the Git strategy and branching model for the official OWP repository. It is here for openness and transparency, but most contributors and users will not need to be concerned with this level of detail. For information geared toward day-to-day development contributions and Git, see the [CONTRIBUTING](../CONTRIBUTING.md) doc. + +- [Branching Model](#branching-model) + - [Feature Branches from `master`](#feature-branches-from-master) + - [Relating `production`, `master`, and Release Branches](#relating-production-master-and-release-branches) +- [Contributing](#contributing) +- [Optional: Setting Up Hook Scripts](#optional-setting-up-hook-scripts) + +## Branching Model + +- This repo uses a branching model based on [Gitflow](https://nvie.com/posts/a-successful-git-branching-model/) that has two primary long-term branches: + - **master**: the main development and integration branch containing the latest completed development work intended for the next released version + - **production**: the branch representing the latest code verified as production-ready and pointing to the most recently release, official version +- Rebasing is used to integrate changes across branches, rather than merge commits + - This allows the repo to maintain a more robust and complete history +- Most interaction with the official OWP repo is done via pull requests (PRs) to the `master` branch + - Independent branches for features or bug fixes are created off `master` to contain development work that is in progress + - Once work in a feature/fix branch is complete (or at least thought complete), it is used to create a PR + - PRs and their linked branches are reviewed and, once approved, have their changes integrated back into `master` + - Typically feature/fix branches exist in personal clones and personal Github forks, but not in the official OWP repo +- Release branches (e.g., `release-X` for pending version `X`) will be created whenever it is time to officially release a new version + - These effectively are release candidates, with branches created from `master` + - The release branches are managed by the core OWP contributors team + - They do exist in the official OWP repo + - But they are short-lived and removed once the release becomes official + - See the [Release Management](RELEASE_MANAGEMENT.md) doc for more details on the release process + +### Feature Branches from `master` +This illustrates the relationship between feature branches and `master`. They should be created from `master` and independently contain commits from their feature. Once done, the changes will be reintegrated back into `master` via rebasing. + +```mermaid + %%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': { 'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}}}%% + gitGraph + commit id:"feature1.1" + commit id:"feature1.2" + branch feature-2 + branch feature-3 + checkout feature-2 + commit id:"feature2.1" + commit id:"feature2.2" + checkout master + merge feature-2 + checkout feature-3 + commit id:"feature3.1" + commit id:"feature3.2" + commit id:"feature3.3" + checkout master + merge feature-3 +``` + +The resulting state of `master` after rebasing the two new feature branches would be: + +```mermaid + %%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': { 'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}}}%% + gitGraph + commit id:"feature1.1" + commit id:"feature1.2" + commit id:"feature2.1" + commit id:"feature2.2" + commit id:"feature3.1" + commit id:"feature3.2" + commit id:"feature3.3" +``` + +### Relating `production`, `master`, and Release Branches + +This illustrates the relationship between `production`, `master`, and `release-v2`. Notice that `production` has already been tagged with version `v1` at the start. Commits for `feature1` and `feature2` at some point are integrated into `master`. When it is time to prepare to release version `v2`, `release-v2` is created. A few bug fix commits were needed in `release-v2`. After that, all the changes in `release-v2` are integrated into `production`, and `production` is tagged `v2`. All the changes are also integrated back into `master`. + + +```mermaid + %%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': { 'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}}}%% + gitGraph + commit id:"v1-commit" + branch production + checkout production + commit id:"v1-commit" tag: "v1" + checkout master + commit id:"feature1.1" + commit id:"feature1.2" + commit id:"feature2.1" + commit id:"feature2.2" + commit id:"feature2.3" + branch release-v2 + checkout release-v2 + commit id:"fix2.1" + commit id:"fix2.2" + checkout production + merge release-v2 tag:"v2" + checkout master + merge release-v2 + +``` + +The resulting state of `production` is: + +```mermaid + %%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': { 'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'production'}}}%% + gitGraph + commit id:"v1-commit" tag:"v1" + commit id:"feature1.1" + commit id:"feature1.2" + commit id:"feature2.1" + commit id:"feature2.2" + commit id:"feature2.3" + commit id:"fix2.1" + commit id:"fix2.2" tag:"v2" +``` + +The resulting state of `master` is essentially the same: + +```mermaid + %%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': { 'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}}}%% + gitGraph + commit id:"v1-commit" + commit id:"feature1.1" + commit id:"feature1.2" + commit id:"feature2.1" + commit id:"feature2.2" + commit id:"feature2.3" + commit id:"fix2.1" + commit id:"fix2.2" +``` + +## Contributing + +More details on the practical processes and requirements for contributing code changes can be found in the [CONTRIBUTING](../CONTRIBUTING.md) doc. In summary: + +- Github Pull Requests (PRs) are required to incorporate changes into the official OWP repo + - Contributors should generally not be pushing changes directly to branches in the OWP repo +- PRs should be submitted using a feature/fix branch contained in a personal Github fork +- Rebasing is used, rather than merge commits, to integrate changes across branches and keep branches from different repos in sync +- PRs should be configured to pull changes into the `master` branch +- Feature/fix branches should be created from `master` +- Personal forks and local clone(s) should be kept up to date with the official OWP repo regularly to minimize the introduction of merge conflict in PRs + + +## Optional: Setting Up Hook Scripts + +_Git_ supports the capability to automatically run various scripts when certain events happen. These are referred to as [_Git_ hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). See Git's documentation for full details on what can be done with hooks and how they are configured. + +Use of client-side hooks is optional but can be very useful for things like automating some kind of quality check before committing a change. However, client-side hooks are not copied as part of cloning a repository. They must be set up locally for each clone. This can be done manually or using available helper tools: e.g., [_pre-commit_](https://pre-commit.com/). Once again, see Git's docs for available options and setup details. \ No newline at end of file diff --git a/doc/RELEASE_MANAGEMENT.md b/doc/RELEASE_MANAGEMENT.md new file mode 100644 index 0000000..2b91263 --- /dev/null +++ b/doc/RELEASE_MANAGEMENT.md @@ -0,0 +1,42 @@ +# Release Management + +The page discusses the release process for official versions of _LGAR-C_. This process is very much interrelated to the repo branching management model, as discussed in detail on the [GIT_USAGE](./GIT_USAGE.md) doc. + +# The Release Process + +## TL;DR + +The release process can be summarized fairly simply: +- A version name is finalized +- A release candidate branch is created +- Testing, QA, fixes are done on the release candidate branch +- Once release candidate is ready, changes are integrated into `production` and `master`, and the new version is tagged + +## Process Steps + + +[comment]: <> (TODO: Document release manual testing and QA procedures) +[//]: # (TODO: document testing and quality checks/process for release candidate prior to release) +[//]: # (TODO: document peer review and integration process for bug fixes, doc updates, etc., into release candidate branch prior to release (i.e, regular PR?) + +1. The next version number is decided/finalized + - Version numbering should follow [Semantic Versioning](https://semver.org/) and its typical `MAJOR.MINOR.PATCH` pattern +2. A release candidate branch, based on `master`, is created in the official OWP repo + - The name of this branch will be `release-X` for version `X` +3. The version is incremented in the main [CMakeLists.txt](../CMakeLists.txt) + - Update the line setting the version, which will look something like `project(lasambmi VERSION 1.0.0 DESCRIPTION "OWP LASAM BMI Module Shared Library")` + - Then committed and pushed this change to the `release-X` branch +4. All necessary testing and quality pre-release tasks are performed using this release candidate branch + - **TODO**: to be documented in more detail +4. (If necessary) Bug fixes, documentation updates, and other acceptable, non-feature changes are applied to the release branch + - Such changes should go through some peer review process before inclusion in the official OWP branch (e.g., PRs, out-of-band code reviews, etc.) + - **TODO**: process to be decided upon and documented +5. Steps 3. and 4. are repeated as needed until testing, quality checks, etc. in Step 3. do not require another iteration of Step 4. + - At this point, the branch is ready for official release +6. All changes in the release candidate branch are incorporated into `production` in the official OWP repo + - Note that **rebasing** should be used to reconcile changes ([see here](../CONTRIBUTING.md#a-rebase-strategy) for more info) +7. The subsequent `HEAD` commit of `production` is tagged with the new version in the official OWP repo +8. All changes in the release candidate branch are incorporated back into `master` in the official OWP repo + - This will include things like bug fixes committed to `release-X` after it was branched from `master` + - As with `production` in Step 6., this should be [done using rebasing](../CONTRIBUTING.md#a-rebase-strategy) +9. The release candidate branch is deleted from the OWP repo (and, ideally, other clones and forks) \ No newline at end of file From 007f8a10f52de2a8e37a645ad9cf4e59067deda7 Mon Sep 17 00:00:00 2001 From: Brian-Cosgrove <131808837+Brian-Cosgrove@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:13:49 -0500 Subject: [PATCH 3/8] Created new OWP government license file for LGAR-C repo Added Apache license information for LGAR-C repo via creation of new LICENSE file --- LICENSE | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 211 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index a023033..6c62c71 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,211 @@ -“Software code created by U.S. Government employees is not subject to copyright -in the United States (17 U.S.C. §105). The United States/Department of Commerce -reserve all rights to seek and obtain copyright protection in countries other -than the United States for Software authored in its entirety by the Department -of Commerce. To this end, the Department of Commerce hereby grants to Recipient -a royalty-free, nonexclusive license to use, copy, and create derivative works -of the Software outside of the United States.” + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +___________________________________________________________________________ + +Software code created by U.S. Government employees is not subject to copyright in the +United States (17 U.S.C. §105). The United States/Department of Commerce reserves +all rights to seek and obtain copyright protection in countries other than the United +States for Software authored in its entirety by the Department of Commerce. To this end, +the Department of Commerce hereby grants to Recipient a royalty-free, nonexclusive +license to use, copy, and create derivative works of the Software outside of the United +States. From 494356b7d1f72b9159ba39d3ed90e8ed259cd26e Mon Sep 17 00:00:00 2001 From: Ian Todd Date: Thu, 31 Jul 2025 13:59:06 +0000 Subject: [PATCH 4/8] Add serialization methods for BmiLGAR --- CMakeLists.txt | 9 ++ include/all.hxx | 13 +++ include/bmi_lgar.hxx | 23 +++- include/vecbuf.hpp | 115 +++++++++++++++++++ src/bmi_lgar.cxx | 262 +++++++++++++++++++++++++++++++++++++++---- src/linked_list.cxx | 19 ++-- 6 files changed, 407 insertions(+), 34 deletions(-) create mode 100644 include/vecbuf.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b6ab03f..a30c740 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,12 +88,21 @@ else() ./src/util_funcs.cxx ./src/aet.cxx ./src/Logger.cpp ./include/Logger.hpp ./giuh/giuh.c include/all.hxx ./giuh/giuh.h) endif() +# ----------------------------------------------------------------------------- +# Find the Boost library and configure usage +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +find_package(Boost 1.79.0 REQUIRED) + target_include_directories(lasambmi PRIVATE include) set_target_properties(lasambmi PROPERTIES VERSION ${PROJECT_VERSION}) set_target_properties(lasambmi PROPERTIES PUBLIC_HEADER ./include/bmi_lgar.hxx) +target_link_libraries(lasambmi PRIVATE Boost::serialization Boost::archive) + include(GNUInstallDirs) install(TARGETS lasambmi diff --git a/include/all.hxx b/include/all.hxx index a3c3fd2..cc64d3b 100755 --- a/include/all.hxx +++ b/include/all.hxx @@ -24,6 +24,7 @@ #include #include #include +#include using namespace std; @@ -53,6 +54,18 @@ struct wetting_front bool to_bottom; // TRUE iff this wetting front is in contact with the layer bottom double dzdt_cm_per_h; // use to store the calculated wetting front speed struct wetting_front *next; // pointer to the next wetting front. + + template + void serialize(Archive &ar, const unsigned int version) { + ar & this->depth_cm; + ar & this->theta; + ar & this->psi_cm; + ar & this->K_cm_per_h; + ar & this->layer_num; + ar & this->front_num; + ar & this->to_bottom; + ar & this->dzdt_cm_per_h; + } }; /* head is a GLOBALLY defined pointer to the first link in the wetting front list. diff --git a/include/bmi_lgar.hxx b/include/bmi_lgar.hxx index b29339a..7c843c5 100644 --- a/include/bmi_lgar.hxx +++ b/include/bmi_lgar.hxx @@ -14,6 +14,8 @@ using namespace std; #include "../bmi/bmi.hxx" #include "all.hxx" #include +#include "vecbuf.hpp" +#include extern "C" { #include "../giuh/giuh.h" @@ -30,7 +32,11 @@ class NotImplemented : public std::logic_error { class BmiLGAR : public bmi::Bmi { public: ~BmiLGAR(); - BmiLGAR():giuh_ordinates(nullptr), giuh_runoff_queue(nullptr) { + BmiLGAR() : + giuh_ordinates(nullptr), + giuh_runoff_queue(nullptr), + m_serialized{} + { this->input_var_names[0] = "precipitation_rate"; this->input_var_names[1] = "potential_evapotranspiration_rate"; this->input_var_names[2] = "soil_temperature_profile"; @@ -130,8 +136,9 @@ public: void global_mass_balance(); double update_calibratable_parameters(); struct model_state* get_model(); - + private: + friend class boost::serialization::access; void realloc_soil(); struct model_state* state; static const int input_var_name_count = 3; @@ -146,6 +153,9 @@ private: double *giuh_ordinates; double *giuh_runoff_queue; + vecbuf m_serialized; + uint64_t m_serialized_length; // needs a stable anchor for GetValuePtr + // unit conversion //struct unit_conversion units; struct bmi_unit_conversion { @@ -163,6 +173,15 @@ private: }; struct bmi_unit_conversion bmi_unit_conv; + + template + void serialize(Archive& ar, const unsigned int version); + template + void serialize_wetting_front_list(Archive &ar, wetting_front **head, int &count); + + void new_serialized(); + void load_serialized(const char* data); + void free_serialized(); }; diff --git a/include/vecbuf.hpp b/include/vecbuf.hpp new file mode 100644 index 0000000..b9b5b86 --- /dev/null +++ b/include/vecbuf.hpp @@ -0,0 +1,115 @@ +#ifndef HPP_STRING_VECBUF +#define HPP_STRING_VECBUF +// https://gist.github.com/stephanlachnit/4a06f8475afd144e73235e2a2584b000 +// SPDX-FileCopyrightText: 2023 Stephan Lachnit +// SPDX-License-Identifier: MIT + +#include +#include +#include + +template> +class vecbuf : public std::basic_streambuf { +public: + using streambuf = std::basic_streambuf; + using char_type = typename streambuf::char_type; + using int_type = typename streambuf::int_type; + using traits_type = typename streambuf::traits_type; + using vector = std::vector; + using value_type = typename vector::value_type; + using size_type = typename vector::size_type; + + // Constructor for vecbuf with optional initial capacity + vecbuf(size_type capacity = 0) : vector_() { reserve(capacity); } + + // Forwarder for std::vector::shrink_to_fit() + constexpr void shrink_to_fit() { vector_.shrink_to_fit(); } + + // Forwarder for std::vector::clear() + constexpr bool clear() const { return vector_.clear(); } + + // Forwarder for std::vector::reserve + constexpr void reserve(size_type capacity) { vector_.reserve(capacity); setp_from_vector(); } + + // Increase the capacity of the buffer by reserving the current_size + additional_capacity + constexpr void reserve_additional(size_type additional_capacity) { reserve(size() + additional_capacity); } + + // Forwarder for std::vector::data + constexpr const value_type* data() const { return vector_.data(); } + + // Forwarder for std::vector::size + constexpr size_type size() const { return vector_.size(); } + + // Forwarder for std::vector::capacity + constexpr size_type capacity() const { return vector_.capacity(); } + + // Implements std::basic_streambuf::xsputn + std::streamsize xsputn(const char_type* s, std::streamsize count) override { + try { + reserve_additional(count); + } + catch (const std::bad_alloc& error) { + // reserve did not work, use slow algorithm + return xsputn_slow(s, count); + } + // reserve worked, use fast algorithm + return xsputn_fast(s, count); + } + +protected: + // Calculates value to std::basic_streambuf::pbase from vector + constexpr value_type* pbase_from_vector() const { return const_cast(vector_.data()); } + + // Calculates value to std::basic_streambuf::pptr from vector + constexpr value_type* pptr_from_vector() const { return const_cast(vector_.data() + vector_.size()); } + + // Calculates value to std::basic_streambuf::epptr from vector + constexpr value_type* epptr_from_vector() const { return const_cast(vector_.data()) + vector_.capacity(); } + + // Sets the values for std::basic_streambuf::pbase, std::basic_streambuf::pptr and std::basic_streambuf::epptr from vector + constexpr void setp_from_vector() { streambuf::setp(pbase_from_vector(), epptr_from_vector()); streambuf::pbump(size()); } + +private: + // std::vector containing the data + vector vector_; + + // Fast implementation of std::basic_streambuf::xsputn if reserve_additional(count) succeeded + std::streamsize xsputn_fast(const char_type* s, std::streamsize count) { + // store current pptr (end of vector location) + auto* old_pptr = pptr_from_vector(); + // resize the vector, does not move since space already reserved + vector_.resize(vector_.size() + count); + // directly memcpy new content to old pptr (end of vector before it was resized) + traits_type::copy(old_pptr, s, count); + // reserve() already calls setp_from_vector(), only adjust pptr to new epptr + streambuf::pbump(count); + + return count; + } + + // Slow implementation of std::basic_streambuf::xsputn if reserve_additional(count) did not succeed, might calls std::basic_streambuf::overflow() + std::streamsize xsputn_slow(const char_type* s, std::streamsize count) { + // reserving entire vector failed, emplace char for char + std::streamsize written = 0; + while (written < count) { + try { + // copy one char, should throw eventually std::bad_alloc + vector_.emplace_back(s[written]); + } + catch (const std::bad_alloc& error) { + // try overflow(), if eof return, else continue + int_type c = this->overflow(traits_type::to_int_type(s[written])); + if (traits_type::eq_int_type(c, traits_type::eof())) { + return written; + } + } + // update pbase, pptr and epptr + setp_from_vector(); + written++; + } + return written; + } + +}; + +#endif diff --git a/src/bmi_lgar.cxx b/src/bmi_lgar.cxx index 0241bcd..e10dd41 100644 --- a/src/bmi_lgar.cxx +++ b/src/bmi_lgar.cxx @@ -14,6 +14,9 @@ #include "../include/all.hxx" #include "../include/Logger.hpp" +#include +#include + std::stringstream bmilgar_ss(""); // default verbosity is set to 'none' other option 'high' or 'low' needs to be specified in the config file @@ -703,36 +706,64 @@ Finalize() delete [] state->lgar_bmi_params.frozen_factor; delete state->lgar_bmi_input_params; delete state; + this->state = NULL; } int BmiLGAR:: GetVarGrid(std::string name) { - if (name.compare("soil_storage_model") == 0 || name.compare("soil_num_wetting_fronts") == 0) // int + if ( + name.compare("soil_storage_model") == 0 + || name.compare("soil_num_wetting_fronts") == 0 + || name.compare("serialization_free") == 0 + ) // int return 0; - else if (name.compare("precipitation_rate") == 0 || name.compare("precipitation") == 0) - return 1; - else if (name.compare("potential_evapotranspiration_rate") == 0 - || name.compare("potential_evapotranspiration") == 0 - || name.compare("actual_evapotranspiration") == 0) // double + else if ( + name.compare("precipitation_rate") == 0 + || name.compare("precipitation") == 0 + || name.compare("potential_evapotranspiration_rate") == 0 + || name.compare("potential_evapotranspiration") == 0 + || name.compare("actual_evapotranspiration") == 0 + || name.compare("surface_runoff") == 0 + || name.compare("giuh_runoff") == 0 + || name.compare("soil_storage") == 0 + || name.compare("field_capacity") == 0 + || name.compare("ponded_depth_max") == 0 + || name.compare("total_discharge") == 0 + || name.compare("infiltration") == 0 + || name.compare("percolation") == 0 + || name.compare("groundwater_to_stream_recharge") == 0 + || name.compare("mass_balance") == 0 + ) // double return 1; - else if (name.compare("surface_runoff") == 0 || name.compare("giuh_runoff") == 0 - || name.compare("soil_storage") == 0 || name.compare("field_capacity") == 0 || name.compare("ponded_depth_max") == 0)// double - return 1; - else if (name.compare("total_discharge") == 0 || name.compare("infiltration") == 0 - || name.compare("percolation") == 0 || name.compare("groundwater_to_stream_recharge") == 0) // double - return 1; - else if (name.compare("mass_balance") == 0) - return 1; - else if (name.compare("soil_depth_layers") == 0 || name.compare("smcmax") == 0 || name.compare("smcmin") == 0 - || name.compare("van_genuchten_m") == 0 || name.compare("van_genuchten_alpha") == 0 || name.compare("van_genuchten_n") == 0 - || name.compare("hydraulic_conductivity") == 0) // array of doubles (fixed length) + else if ( + name.compare("soil_depth_layers") == 0 + || name.compare("smcmax") == 0 + || name.compare("smcmin") == 0 + || name.compare("van_genuchten_m") == 0 + || name.compare("van_genuchten_alpha") == 0 + || name.compare("van_genuchten_n") == 0 + || name.compare("hydraulic_conductivity") == 0 + ) // array of doubles (fixed length) return 2; - else if (name.compare("soil_moisture_wetting_fronts") == 0 || name.compare("soil_depth_wetting_fronts") == 0) // array of doubles (dynamic length) + else if ( + name.compare("soil_moisture_wetting_fronts") == 0 + || name.compare("soil_depth_wetting_fronts") == 0 + ) // array of doubles (dynamic length) return 3; - else if (name.compare("soil_temperature_profile") == 0) // array of doubles (fixed and of the size of soil temperature profile) + else if ( + name.compare("soil_temperature_profile") == 0 + ) // array of doubles (fixed and of the size of soil temperature profile) return 4; + else if ( + name.compare("serialization_state") == 0 + ) // char + return 5; + else if ( + name.compare("serialization_create") == 0 + ) // uint64_t + return 6; else return -1; } @@ -747,6 +778,10 @@ GetVarType(std::string name) return "int"; else if (var_grid == 1 || var_grid == 2 || var_grid == 3 || var_grid == 4) return "double"; + else if (var_grid == 5) + return "char"; + else if (var_grid == 6) + return "uint64_t"; else return "none"; } @@ -761,6 +796,10 @@ GetVarItemsize(std::string name) return sizeof(int); else if (var_grid == 1 || var_grid == 2 || var_grid == 3 || var_grid == 4) return sizeof(double); + else if (var_grid == 5) + return sizeof(char); + else if (var_grid == 6) + return sizeof(uint64_t); else return 0; } @@ -882,6 +921,8 @@ GetGridSize(const int grid) return this->state->lgar_bmi_params.num_wetting_fronts; else if (grid == 4) // number of cells (discretized temperature profile, input from SFT) return this->state->lgar_bmi_params.num_cells_temp; + else if (grid == 5) + return this->m_serialized_length; // serialized state length else return -1; } @@ -895,8 +936,13 @@ GetValue (std::string name, void *dest) int nbytes = 0; src = this->GetValuePtr(name); - nbytes = this->GetVarNbytes(name); - memcpy (dest, src, nbytes); + + if (name.compare("serialization_state") == 0) { + memcpy(dest, src, this->m_serialized_length); + } else { + nbytes = this->GetVarNbytes(name); + memcpy (dest, src, nbytes); + } } @@ -953,7 +999,12 @@ GetValuePtr (std::string name) return (void*)&this->state->lgar_calib_params.ponded_depth_max; else if (name.compare("field_capacity") == 0) return (void*)&this->state->lgar_calib_params.field_capacity_psi; - else { + else if (name.compare("serialization_state") == 0) + return (void*)(this->m_serialized.data()); + else if (name.compare("serialization_create") == 0) { + this->new_serialized(); + return (void*)(&this->m_serialized_length); + } else { std::stringstream errMsg; errMsg << "variable "<< name << " does not exist"; throw std::runtime_error(errMsg.str()); @@ -990,6 +1041,18 @@ GetValueAtIndices (std::string name, void *dest, int *inds, int len) void BmiLGAR:: SetValue (std::string name, void *src) { + // state serialization exceptions + if (name == "serialization_state") { + this->load_serialized((char*)src); + return; + } else if (name == "serialization_free") { + this->free_serialized(); + return; + } else if (name.compare("serialization_create") == 0) { + auto msg = "Cannot set values with \"serialization_create\"."; + Logger::Log(LogLevel::WARNING, msg); + throw std::runtime_error(msg); + } void * dest = NULL; dest = this->GetValuePtr(name); @@ -1208,4 +1271,159 @@ GetGridNodesPerFace(const int grid, int *nodes_per_face) throw bmi_lgar::NotImplemented(); } + +// low-impoact serialization. This will only archive mutable state that is not recalculated during Update or is accessable through BMI getters/setters +template +void BmiLGAR:: +serialize(Archive& ar, const unsigned int version) { + model_state* state = this->state; + + // GetValuePtr properties + ar & state->lgar_bmi_input_params->precipitation_mm_per_h; + ar & this->bmi_unit_conv.volprecip_timestep_m; + ar & state->lgar_bmi_input_params->PET_mm_per_h; + ar & this->bmi_unit_conv.volPET_timestep_m; + ar & this->bmi_unit_conv.volAET_timestep_m; + ar & this->bmi_unit_conv.volrunoff_giuh_timestep_m; + ar & this->bmi_unit_conv.volend_timestep_m; + ar & this->bmi_unit_conv.volQ_timestep_m; + ar & this->bmi_unit_conv.volin_timestep_m; + ar & this->bmi_unit_conv.volrech_timestep_m; + ar & this->bmi_unit_conv.volQ_gw_timestep_m; + ar & this->bmi_unit_conv.mass_balance_m; + ar & this->bmi_unit_conv.volrunoff_timestep_m; + ar & state->lgar_bmi_params.num_wetting_fronts; + ar & state->lgar_calib_params.ponded_depth_max; + ar & state->lgar_calib_params.field_capacity_psi; + + // end of update + ar & state->lgar_mass_balance.volprecip_timestep_cm; + ar & state->lgar_mass_balance.volin_timestep_cm; + ar & state->lgar_mass_balance.volon_timestep_cm; + ar & state->lgar_mass_balance.volend_timestep_cm; + ar & state->lgar_mass_balance.volAET_timestep_cm; + ar & state->lgar_mass_balance.volrech_timestep_cm; + ar & state->lgar_mass_balance.volrunoff_timestep_cm; + ar & state->lgar_mass_balance.volQ_timestep_cm; + ar & state->lgar_mass_balance.volQ_gw_timestep_cm; + ar & state->lgar_mass_balance.volPET_timestep_cm; + ar & state->lgar_mass_balance.volrunoff_giuh_timestep_cm; + + ar & state->lgar_mass_balance.volprecip_cm; + ar & state->lgar_mass_balance.volin_cm; + ar & state->lgar_mass_balance.volon_cm; + ar & state->lgar_mass_balance.volend_cm; + ar & state->lgar_mass_balance.volAET_cm; + ar & state->lgar_mass_balance.volrech_cm; + ar & state->lgar_mass_balance.volrunoff_cm; + ar & state->lgar_mass_balance.volQ_cm; + ar & state->lgar_mass_balance.volQ_gw_cm; + ar & state->lgar_mass_balance.volPET_cm; + ar & state->lgar_mass_balance.volrunoff_giuh_cm; + ar & state->lgar_mass_balance.volchange_calib_cm; + + ar & boost::serialization::make_array(state->lgar_bmi_params.cum_layer_thickness_cm, state->lgar_bmi_params.num_layers); + + // giuh state + ar & boost::serialization::make_array(this->giuh_runoff_queue, state->lgar_bmi_params.num_giuh_ordinates); + + // in frozen_factor_hydraulic_conductivity + if (state->lgar_bmi_params.sft_coupled) { + ar & boost::serialization::make_array(state->lgar_bmi_params.frozen_factor, state->lgar_bmi_params.num_layers); + } + + // update_calibratable_parameters + ar & state->lgar_bmi_params.field_capacity_psi_cm; + ar & state->lgar_bmi_params.calib_params_flag; + + // may be set in adapative timesteps + if (state->lgar_bmi_params.adaptive_timestep){ + ar & state->lgar_bmi_params.timestep_h; + } + + // how much time has passed since instantiation + ar & state->lgar_bmi_params.time_s; + ar & state->lgar_bmi_params.timesteps; + + // serialization of arbitrarily-lengthed linked-lists + int num_fronts = state->lgar_bmi_params.num_wetting_fronts; + this->serialize_wetting_front_list(ar, &state->head, state->lgar_bmi_params.num_wetting_fronts); + int num_previous = 0; + this->serialize_wetting_front_list(ar, &state->state_previous, num_previous); + + if (ar.is_loading() && num_fronts != state->lgar_bmi_params.num_wetting_fronts) { + // reallocate arrays based on new num_wetting_fronts + this->realloc_soil(); + } + ar & boost::serialization::make_array( + state->lgar_bmi_params.soil_moisture_wetting_fronts, state->lgar_bmi_params.num_wetting_fronts + ); + ar & boost::serialization::make_array( + state->lgar_bmi_params.soil_depth_wetting_fronts, state->lgar_bmi_params.num_wetting_fronts + ); + +} + +template +void BmiLGAR::serialize_wetting_front_list(Archive &ar, wetting_front **head, int &count) { + wetting_front *current; + if (ar.is_saving()) { + if (count == 0) // assume recalculation needed if 0 + count = listLength(*head); + ar & count; + current = *head; + while (current != NULL) { + ar & (*current); + current = current->next; + } + } else { // loading + ar & count; + listDelete(*head); + *head = NULL; + wetting_front *prior; + for (int i = 0; i < count; ++i) { + current = new wetting_front(); + ar & (*current); + if (i == 0) { + *head = current; + } else { + prior->next = current; + } + prior = current; + } + } +} + +void BmiLGAR::new_serialized() { + this->m_serialized.clear(); + boost::archive::binary_oarchive archive(this->m_serialized); + try { + archive << (*this); + this->m_serialized_length = this->m_serialized.size(); + } catch (const std::exception &e) { + Logger::Log(LogLevel::SEVERE, "Serializing LASAM encountered an error: %s", e.what()); + this->free_serialized(); + throw; + } +} + +void BmiLGAR::load_serialized(const char* data) { + std::stringstream stream(data); + boost::archive::binary_iarchive archive(stream); + try { + archive >> (*this); + } catch (const std::exception &e) { + Logger::Log(LogLevel::SEVERE, "Deserializing LASAM encountered an error: %s", e.what()); + throw; + } + this->free_serialized(); +} + +void BmiLGAR::free_serialized() { + this->m_serialized.clear(); + this->m_serialized.shrink_to_fit(); + this->m_serialized_length = 0; +} + + #endif diff --git a/src/linked_list.cxx b/src/linked_list.cxx index 1390ef9..90483c6 100755 --- a/src/linked_list.cxx +++ b/src/linked_list.cxx @@ -37,7 +37,7 @@ extern void listDelete(struct wetting_front* head) { while (head != NULL) { struct wetting_front *next = head->next; - free( head ); + delete head; head = next; } } @@ -73,7 +73,7 @@ extern struct wetting_front* listCopy(struct wetting_front* current, struct wett } else { - struct wetting_front* wf = (struct wetting_front*)malloc(sizeof(struct wetting_front)); + struct wetting_front* wf = new wetting_front(); wf->depth_cm = current->depth_cm; wf->theta = current->theta; @@ -99,7 +99,7 @@ extern void listInsertFirst(double depth, double theta, int front_num, int layer { //create a link - struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + struct wetting_front *link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -255,7 +255,7 @@ extern struct wetting_front* listDeleteFront(int front_num, struct wetting_front previous = current->next; } - if( current != NULL ) free( current ); + delete current; current = previous; while(previous != NULL) { // decrement all front numbers @@ -283,8 +283,7 @@ extern struct wetting_front* listInsertFront(double depth, double theta, int new if(new_front_num==1) { // create it //create a link - struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); - + struct wetting_front *link = new wetting_front(); link->depth_cm = depth; link->theta = theta; link->front_num = new_front_num; @@ -305,7 +304,7 @@ extern struct wetting_front* listInsertFront(double depth, double theta, int new do { if (previous->front_num == new_front_num-1) { // this is where we want to insert it //create a new link - struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + struct wetting_front *link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -353,7 +352,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ if(head == NULL) { // list is empty // Kinda weird. Shouldn't call this function to start a list. Create link in the first position - link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -378,7 +377,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ // yep. It's first //create a link and put it at the beginning of the list - link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -407,7 +406,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ // it is in this interval // create a link - link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link = new wetting_front(); link->depth_cm = depth; link->theta = theta; From 1753966bb47a75094760062e1fec058f6a543558 Mon Sep 17 00:00:00 2001 From: Carolyn Maynard Date: Wed, 6 Aug 2025 18:24:40 +0000 Subject: [PATCH 5/8] Revert "Merge branch 'idt/bmi-serialization' into 'development'" This reverts merge request !29 --- CMakeLists.txt | 9 -- include/all.hxx | 13 --- include/bmi_lgar.hxx | 23 +--- include/vecbuf.hpp | 115 ------------------- src/bmi_lgar.cxx | 262 ++++--------------------------------------- src/linked_list.cxx | 19 ++-- 6 files changed, 34 insertions(+), 407 deletions(-) delete mode 100644 include/vecbuf.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a30c740..b6ab03f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,21 +88,12 @@ else() ./src/util_funcs.cxx ./src/aet.cxx ./src/Logger.cpp ./include/Logger.hpp ./giuh/giuh.c include/all.hxx ./giuh/giuh.h) endif() -# ----------------------------------------------------------------------------- -# Find the Boost library and configure usage -set(Boost_USE_STATIC_LIBS OFF) -set(Boost_USE_MULTITHREADED ON) -set(Boost_USE_STATIC_RUNTIME OFF) -find_package(Boost 1.79.0 REQUIRED) - target_include_directories(lasambmi PRIVATE include) set_target_properties(lasambmi PROPERTIES VERSION ${PROJECT_VERSION}) set_target_properties(lasambmi PROPERTIES PUBLIC_HEADER ./include/bmi_lgar.hxx) -target_link_libraries(lasambmi PRIVATE Boost::serialization Boost::archive) - include(GNUInstallDirs) install(TARGETS lasambmi diff --git a/include/all.hxx b/include/all.hxx index cc64d3b..a3c3fd2 100755 --- a/include/all.hxx +++ b/include/all.hxx @@ -24,7 +24,6 @@ #include #include #include -#include using namespace std; @@ -54,18 +53,6 @@ struct wetting_front bool to_bottom; // TRUE iff this wetting front is in contact with the layer bottom double dzdt_cm_per_h; // use to store the calculated wetting front speed struct wetting_front *next; // pointer to the next wetting front. - - template - void serialize(Archive &ar, const unsigned int version) { - ar & this->depth_cm; - ar & this->theta; - ar & this->psi_cm; - ar & this->K_cm_per_h; - ar & this->layer_num; - ar & this->front_num; - ar & this->to_bottom; - ar & this->dzdt_cm_per_h; - } }; /* head is a GLOBALLY defined pointer to the first link in the wetting front list. diff --git a/include/bmi_lgar.hxx b/include/bmi_lgar.hxx index 7c843c5..b29339a 100644 --- a/include/bmi_lgar.hxx +++ b/include/bmi_lgar.hxx @@ -14,8 +14,6 @@ using namespace std; #include "../bmi/bmi.hxx" #include "all.hxx" #include -#include "vecbuf.hpp" -#include extern "C" { #include "../giuh/giuh.h" @@ -32,11 +30,7 @@ class NotImplemented : public std::logic_error { class BmiLGAR : public bmi::Bmi { public: ~BmiLGAR(); - BmiLGAR() : - giuh_ordinates(nullptr), - giuh_runoff_queue(nullptr), - m_serialized{} - { + BmiLGAR():giuh_ordinates(nullptr), giuh_runoff_queue(nullptr) { this->input_var_names[0] = "precipitation_rate"; this->input_var_names[1] = "potential_evapotranspiration_rate"; this->input_var_names[2] = "soil_temperature_profile"; @@ -136,9 +130,8 @@ public: void global_mass_balance(); double update_calibratable_parameters(); struct model_state* get_model(); - + private: - friend class boost::serialization::access; void realloc_soil(); struct model_state* state; static const int input_var_name_count = 3; @@ -153,9 +146,6 @@ private: double *giuh_ordinates; double *giuh_runoff_queue; - vecbuf m_serialized; - uint64_t m_serialized_length; // needs a stable anchor for GetValuePtr - // unit conversion //struct unit_conversion units; struct bmi_unit_conversion { @@ -173,15 +163,6 @@ private: }; struct bmi_unit_conversion bmi_unit_conv; - - template - void serialize(Archive& ar, const unsigned int version); - template - void serialize_wetting_front_list(Archive &ar, wetting_front **head, int &count); - - void new_serialized(); - void load_serialized(const char* data); - void free_serialized(); }; diff --git a/include/vecbuf.hpp b/include/vecbuf.hpp deleted file mode 100644 index b9b5b86..0000000 --- a/include/vecbuf.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#ifndef HPP_STRING_VECBUF -#define HPP_STRING_VECBUF -// https://gist.github.com/stephanlachnit/4a06f8475afd144e73235e2a2584b000 -// SPDX-FileCopyrightText: 2023 Stephan Lachnit -// SPDX-License-Identifier: MIT - -#include -#include -#include - -template> -class vecbuf : public std::basic_streambuf { -public: - using streambuf = std::basic_streambuf; - using char_type = typename streambuf::char_type; - using int_type = typename streambuf::int_type; - using traits_type = typename streambuf::traits_type; - using vector = std::vector; - using value_type = typename vector::value_type; - using size_type = typename vector::size_type; - - // Constructor for vecbuf with optional initial capacity - vecbuf(size_type capacity = 0) : vector_() { reserve(capacity); } - - // Forwarder for std::vector::shrink_to_fit() - constexpr void shrink_to_fit() { vector_.shrink_to_fit(); } - - // Forwarder for std::vector::clear() - constexpr bool clear() const { return vector_.clear(); } - - // Forwarder for std::vector::reserve - constexpr void reserve(size_type capacity) { vector_.reserve(capacity); setp_from_vector(); } - - // Increase the capacity of the buffer by reserving the current_size + additional_capacity - constexpr void reserve_additional(size_type additional_capacity) { reserve(size() + additional_capacity); } - - // Forwarder for std::vector::data - constexpr const value_type* data() const { return vector_.data(); } - - // Forwarder for std::vector::size - constexpr size_type size() const { return vector_.size(); } - - // Forwarder for std::vector::capacity - constexpr size_type capacity() const { return vector_.capacity(); } - - // Implements std::basic_streambuf::xsputn - std::streamsize xsputn(const char_type* s, std::streamsize count) override { - try { - reserve_additional(count); - } - catch (const std::bad_alloc& error) { - // reserve did not work, use slow algorithm - return xsputn_slow(s, count); - } - // reserve worked, use fast algorithm - return xsputn_fast(s, count); - } - -protected: - // Calculates value to std::basic_streambuf::pbase from vector - constexpr value_type* pbase_from_vector() const { return const_cast(vector_.data()); } - - // Calculates value to std::basic_streambuf::pptr from vector - constexpr value_type* pptr_from_vector() const { return const_cast(vector_.data() + vector_.size()); } - - // Calculates value to std::basic_streambuf::epptr from vector - constexpr value_type* epptr_from_vector() const { return const_cast(vector_.data()) + vector_.capacity(); } - - // Sets the values for std::basic_streambuf::pbase, std::basic_streambuf::pptr and std::basic_streambuf::epptr from vector - constexpr void setp_from_vector() { streambuf::setp(pbase_from_vector(), epptr_from_vector()); streambuf::pbump(size()); } - -private: - // std::vector containing the data - vector vector_; - - // Fast implementation of std::basic_streambuf::xsputn if reserve_additional(count) succeeded - std::streamsize xsputn_fast(const char_type* s, std::streamsize count) { - // store current pptr (end of vector location) - auto* old_pptr = pptr_from_vector(); - // resize the vector, does not move since space already reserved - vector_.resize(vector_.size() + count); - // directly memcpy new content to old pptr (end of vector before it was resized) - traits_type::copy(old_pptr, s, count); - // reserve() already calls setp_from_vector(), only adjust pptr to new epptr - streambuf::pbump(count); - - return count; - } - - // Slow implementation of std::basic_streambuf::xsputn if reserve_additional(count) did not succeed, might calls std::basic_streambuf::overflow() - std::streamsize xsputn_slow(const char_type* s, std::streamsize count) { - // reserving entire vector failed, emplace char for char - std::streamsize written = 0; - while (written < count) { - try { - // copy one char, should throw eventually std::bad_alloc - vector_.emplace_back(s[written]); - } - catch (const std::bad_alloc& error) { - // try overflow(), if eof return, else continue - int_type c = this->overflow(traits_type::to_int_type(s[written])); - if (traits_type::eq_int_type(c, traits_type::eof())) { - return written; - } - } - // update pbase, pptr and epptr - setp_from_vector(); - written++; - } - return written; - } - -}; - -#endif diff --git a/src/bmi_lgar.cxx b/src/bmi_lgar.cxx index e10dd41..0241bcd 100644 --- a/src/bmi_lgar.cxx +++ b/src/bmi_lgar.cxx @@ -14,9 +14,6 @@ #include "../include/all.hxx" #include "../include/Logger.hpp" -#include -#include - std::stringstream bmilgar_ss(""); // default verbosity is set to 'none' other option 'high' or 'low' needs to be specified in the config file @@ -706,64 +703,36 @@ Finalize() delete [] state->lgar_bmi_params.frozen_factor; delete state->lgar_bmi_input_params; delete state; - this->state = NULL; } int BmiLGAR:: GetVarGrid(std::string name) { - if ( - name.compare("soil_storage_model") == 0 - || name.compare("soil_num_wetting_fronts") == 0 - || name.compare("serialization_free") == 0 - ) // int + if (name.compare("soil_storage_model") == 0 || name.compare("soil_num_wetting_fronts") == 0) // int return 0; - else if ( - name.compare("precipitation_rate") == 0 - || name.compare("precipitation") == 0 - || name.compare("potential_evapotranspiration_rate") == 0 - || name.compare("potential_evapotranspiration") == 0 - || name.compare("actual_evapotranspiration") == 0 - || name.compare("surface_runoff") == 0 - || name.compare("giuh_runoff") == 0 - || name.compare("soil_storage") == 0 - || name.compare("field_capacity") == 0 - || name.compare("ponded_depth_max") == 0 - || name.compare("total_discharge") == 0 - || name.compare("infiltration") == 0 - || name.compare("percolation") == 0 - || name.compare("groundwater_to_stream_recharge") == 0 - || name.compare("mass_balance") == 0 - ) // double + else if (name.compare("precipitation_rate") == 0 || name.compare("precipitation") == 0) + return 1; + else if (name.compare("potential_evapotranspiration_rate") == 0 + || name.compare("potential_evapotranspiration") == 0 + || name.compare("actual_evapotranspiration") == 0) // double return 1; - else if ( - name.compare("soil_depth_layers") == 0 - || name.compare("smcmax") == 0 - || name.compare("smcmin") == 0 - || name.compare("van_genuchten_m") == 0 - || name.compare("van_genuchten_alpha") == 0 - || name.compare("van_genuchten_n") == 0 - || name.compare("hydraulic_conductivity") == 0 - ) // array of doubles (fixed length) + else if (name.compare("surface_runoff") == 0 || name.compare("giuh_runoff") == 0 + || name.compare("soil_storage") == 0 || name.compare("field_capacity") == 0 || name.compare("ponded_depth_max") == 0)// double + return 1; + else if (name.compare("total_discharge") == 0 || name.compare("infiltration") == 0 + || name.compare("percolation") == 0 || name.compare("groundwater_to_stream_recharge") == 0) // double + return 1; + else if (name.compare("mass_balance") == 0) + return 1; + else if (name.compare("soil_depth_layers") == 0 || name.compare("smcmax") == 0 || name.compare("smcmin") == 0 + || name.compare("van_genuchten_m") == 0 || name.compare("van_genuchten_alpha") == 0 || name.compare("van_genuchten_n") == 0 + || name.compare("hydraulic_conductivity") == 0) // array of doubles (fixed length) return 2; - else if ( - name.compare("soil_moisture_wetting_fronts") == 0 - || name.compare("soil_depth_wetting_fronts") == 0 - ) // array of doubles (dynamic length) + else if (name.compare("soil_moisture_wetting_fronts") == 0 || name.compare("soil_depth_wetting_fronts") == 0) // array of doubles (dynamic length) return 3; - else if ( - name.compare("soil_temperature_profile") == 0 - ) // array of doubles (fixed and of the size of soil temperature profile) + else if (name.compare("soil_temperature_profile") == 0) // array of doubles (fixed and of the size of soil temperature profile) return 4; - else if ( - name.compare("serialization_state") == 0 - ) // char - return 5; - else if ( - name.compare("serialization_create") == 0 - ) // uint64_t - return 6; else return -1; } @@ -778,10 +747,6 @@ GetVarType(std::string name) return "int"; else if (var_grid == 1 || var_grid == 2 || var_grid == 3 || var_grid == 4) return "double"; - else if (var_grid == 5) - return "char"; - else if (var_grid == 6) - return "uint64_t"; else return "none"; } @@ -796,10 +761,6 @@ GetVarItemsize(std::string name) return sizeof(int); else if (var_grid == 1 || var_grid == 2 || var_grid == 3 || var_grid == 4) return sizeof(double); - else if (var_grid == 5) - return sizeof(char); - else if (var_grid == 6) - return sizeof(uint64_t); else return 0; } @@ -921,8 +882,6 @@ GetGridSize(const int grid) return this->state->lgar_bmi_params.num_wetting_fronts; else if (grid == 4) // number of cells (discretized temperature profile, input from SFT) return this->state->lgar_bmi_params.num_cells_temp; - else if (grid == 5) - return this->m_serialized_length; // serialized state length else return -1; } @@ -936,13 +895,8 @@ GetValue (std::string name, void *dest) int nbytes = 0; src = this->GetValuePtr(name); - - if (name.compare("serialization_state") == 0) { - memcpy(dest, src, this->m_serialized_length); - } else { - nbytes = this->GetVarNbytes(name); - memcpy (dest, src, nbytes); - } + nbytes = this->GetVarNbytes(name); + memcpy (dest, src, nbytes); } @@ -999,12 +953,7 @@ GetValuePtr (std::string name) return (void*)&this->state->lgar_calib_params.ponded_depth_max; else if (name.compare("field_capacity") == 0) return (void*)&this->state->lgar_calib_params.field_capacity_psi; - else if (name.compare("serialization_state") == 0) - return (void*)(this->m_serialized.data()); - else if (name.compare("serialization_create") == 0) { - this->new_serialized(); - return (void*)(&this->m_serialized_length); - } else { + else { std::stringstream errMsg; errMsg << "variable "<< name << " does not exist"; throw std::runtime_error(errMsg.str()); @@ -1041,18 +990,6 @@ GetValueAtIndices (std::string name, void *dest, int *inds, int len) void BmiLGAR:: SetValue (std::string name, void *src) { - // state serialization exceptions - if (name == "serialization_state") { - this->load_serialized((char*)src); - return; - } else if (name == "serialization_free") { - this->free_serialized(); - return; - } else if (name.compare("serialization_create") == 0) { - auto msg = "Cannot set values with \"serialization_create\"."; - Logger::Log(LogLevel::WARNING, msg); - throw std::runtime_error(msg); - } void * dest = NULL; dest = this->GetValuePtr(name); @@ -1271,159 +1208,4 @@ GetGridNodesPerFace(const int grid, int *nodes_per_face) throw bmi_lgar::NotImplemented(); } - -// low-impoact serialization. This will only archive mutable state that is not recalculated during Update or is accessable through BMI getters/setters -template -void BmiLGAR:: -serialize(Archive& ar, const unsigned int version) { - model_state* state = this->state; - - // GetValuePtr properties - ar & state->lgar_bmi_input_params->precipitation_mm_per_h; - ar & this->bmi_unit_conv.volprecip_timestep_m; - ar & state->lgar_bmi_input_params->PET_mm_per_h; - ar & this->bmi_unit_conv.volPET_timestep_m; - ar & this->bmi_unit_conv.volAET_timestep_m; - ar & this->bmi_unit_conv.volrunoff_giuh_timestep_m; - ar & this->bmi_unit_conv.volend_timestep_m; - ar & this->bmi_unit_conv.volQ_timestep_m; - ar & this->bmi_unit_conv.volin_timestep_m; - ar & this->bmi_unit_conv.volrech_timestep_m; - ar & this->bmi_unit_conv.volQ_gw_timestep_m; - ar & this->bmi_unit_conv.mass_balance_m; - ar & this->bmi_unit_conv.volrunoff_timestep_m; - ar & state->lgar_bmi_params.num_wetting_fronts; - ar & state->lgar_calib_params.ponded_depth_max; - ar & state->lgar_calib_params.field_capacity_psi; - - // end of update - ar & state->lgar_mass_balance.volprecip_timestep_cm; - ar & state->lgar_mass_balance.volin_timestep_cm; - ar & state->lgar_mass_balance.volon_timestep_cm; - ar & state->lgar_mass_balance.volend_timestep_cm; - ar & state->lgar_mass_balance.volAET_timestep_cm; - ar & state->lgar_mass_balance.volrech_timestep_cm; - ar & state->lgar_mass_balance.volrunoff_timestep_cm; - ar & state->lgar_mass_balance.volQ_timestep_cm; - ar & state->lgar_mass_balance.volQ_gw_timestep_cm; - ar & state->lgar_mass_balance.volPET_timestep_cm; - ar & state->lgar_mass_balance.volrunoff_giuh_timestep_cm; - - ar & state->lgar_mass_balance.volprecip_cm; - ar & state->lgar_mass_balance.volin_cm; - ar & state->lgar_mass_balance.volon_cm; - ar & state->lgar_mass_balance.volend_cm; - ar & state->lgar_mass_balance.volAET_cm; - ar & state->lgar_mass_balance.volrech_cm; - ar & state->lgar_mass_balance.volrunoff_cm; - ar & state->lgar_mass_balance.volQ_cm; - ar & state->lgar_mass_balance.volQ_gw_cm; - ar & state->lgar_mass_balance.volPET_cm; - ar & state->lgar_mass_balance.volrunoff_giuh_cm; - ar & state->lgar_mass_balance.volchange_calib_cm; - - ar & boost::serialization::make_array(state->lgar_bmi_params.cum_layer_thickness_cm, state->lgar_bmi_params.num_layers); - - // giuh state - ar & boost::serialization::make_array(this->giuh_runoff_queue, state->lgar_bmi_params.num_giuh_ordinates); - - // in frozen_factor_hydraulic_conductivity - if (state->lgar_bmi_params.sft_coupled) { - ar & boost::serialization::make_array(state->lgar_bmi_params.frozen_factor, state->lgar_bmi_params.num_layers); - } - - // update_calibratable_parameters - ar & state->lgar_bmi_params.field_capacity_psi_cm; - ar & state->lgar_bmi_params.calib_params_flag; - - // may be set in adapative timesteps - if (state->lgar_bmi_params.adaptive_timestep){ - ar & state->lgar_bmi_params.timestep_h; - } - - // how much time has passed since instantiation - ar & state->lgar_bmi_params.time_s; - ar & state->lgar_bmi_params.timesteps; - - // serialization of arbitrarily-lengthed linked-lists - int num_fronts = state->lgar_bmi_params.num_wetting_fronts; - this->serialize_wetting_front_list(ar, &state->head, state->lgar_bmi_params.num_wetting_fronts); - int num_previous = 0; - this->serialize_wetting_front_list(ar, &state->state_previous, num_previous); - - if (ar.is_loading() && num_fronts != state->lgar_bmi_params.num_wetting_fronts) { - // reallocate arrays based on new num_wetting_fronts - this->realloc_soil(); - } - ar & boost::serialization::make_array( - state->lgar_bmi_params.soil_moisture_wetting_fronts, state->lgar_bmi_params.num_wetting_fronts - ); - ar & boost::serialization::make_array( - state->lgar_bmi_params.soil_depth_wetting_fronts, state->lgar_bmi_params.num_wetting_fronts - ); - -} - -template -void BmiLGAR::serialize_wetting_front_list(Archive &ar, wetting_front **head, int &count) { - wetting_front *current; - if (ar.is_saving()) { - if (count == 0) // assume recalculation needed if 0 - count = listLength(*head); - ar & count; - current = *head; - while (current != NULL) { - ar & (*current); - current = current->next; - } - } else { // loading - ar & count; - listDelete(*head); - *head = NULL; - wetting_front *prior; - for (int i = 0; i < count; ++i) { - current = new wetting_front(); - ar & (*current); - if (i == 0) { - *head = current; - } else { - prior->next = current; - } - prior = current; - } - } -} - -void BmiLGAR::new_serialized() { - this->m_serialized.clear(); - boost::archive::binary_oarchive archive(this->m_serialized); - try { - archive << (*this); - this->m_serialized_length = this->m_serialized.size(); - } catch (const std::exception &e) { - Logger::Log(LogLevel::SEVERE, "Serializing LASAM encountered an error: %s", e.what()); - this->free_serialized(); - throw; - } -} - -void BmiLGAR::load_serialized(const char* data) { - std::stringstream stream(data); - boost::archive::binary_iarchive archive(stream); - try { - archive >> (*this); - } catch (const std::exception &e) { - Logger::Log(LogLevel::SEVERE, "Deserializing LASAM encountered an error: %s", e.what()); - throw; - } - this->free_serialized(); -} - -void BmiLGAR::free_serialized() { - this->m_serialized.clear(); - this->m_serialized.shrink_to_fit(); - this->m_serialized_length = 0; -} - - #endif diff --git a/src/linked_list.cxx b/src/linked_list.cxx index 90483c6..1390ef9 100755 --- a/src/linked_list.cxx +++ b/src/linked_list.cxx @@ -37,7 +37,7 @@ extern void listDelete(struct wetting_front* head) { while (head != NULL) { struct wetting_front *next = head->next; - delete head; + free( head ); head = next; } } @@ -73,7 +73,7 @@ extern struct wetting_front* listCopy(struct wetting_front* current, struct wett } else { - struct wetting_front* wf = new wetting_front(); + struct wetting_front* wf = (struct wetting_front*)malloc(sizeof(struct wetting_front)); wf->depth_cm = current->depth_cm; wf->theta = current->theta; @@ -99,7 +99,7 @@ extern void listInsertFirst(double depth, double theta, int front_num, int layer { //create a link - struct wetting_front *link = new wetting_front(); + struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); link->depth_cm = depth; link->theta = theta; @@ -255,7 +255,7 @@ extern struct wetting_front* listDeleteFront(int front_num, struct wetting_front previous = current->next; } - delete current; + if( current != NULL ) free( current ); current = previous; while(previous != NULL) { // decrement all front numbers @@ -283,7 +283,8 @@ extern struct wetting_front* listInsertFront(double depth, double theta, int new if(new_front_num==1) { // create it //create a link - struct wetting_front *link = new wetting_front(); + struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link->depth_cm = depth; link->theta = theta; link->front_num = new_front_num; @@ -304,7 +305,7 @@ extern struct wetting_front* listInsertFront(double depth, double theta, int new do { if (previous->front_num == new_front_num-1) { // this is where we want to insert it //create a new link - struct wetting_front *link = new wetting_front(); + struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); link->depth_cm = depth; link->theta = theta; @@ -352,7 +353,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ if(head == NULL) { // list is empty // Kinda weird. Shouldn't call this function to start a list. Create link in the first position - link = new wetting_front(); + link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); link->depth_cm = depth; link->theta = theta; @@ -377,7 +378,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ // yep. It's first //create a link and put it at the beginning of the list - link = new wetting_front(); + link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); link->depth_cm = depth; link->theta = theta; @@ -406,7 +407,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ // it is in this interval // create a link - link = new wetting_front(); + link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); link->depth_cm = depth; link->theta = theta; From 9d4e0090ea0a22104516fca9eafbc19b7ad90530 Mon Sep 17 00:00:00 2001 From: Ian Todd Date: Fri, 15 Aug 2025 13:37:44 +0000 Subject: [PATCH 6/8] State serialization for BMI --- CMakeLists.txt | 12 ++ include/all.hxx | 13 +++ include/bmi_lgar.hxx | 23 +++- include/vecbuf.hpp | 115 +++++++++++++++++++ src/Logger.cpp | 2 +- src/bmi_lgar.cxx | 262 +++++++++++++++++++++++++++++++++++++++---- src/linked_list.cxx | 19 ++-- 7 files changed, 411 insertions(+), 35 deletions(-) create mode 100644 include/vecbuf.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b6ab03f..634ee80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,13 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/X11/include") # add the executable #message("CXXLIBS" ${CMAKE_CXX_FLAGS}) +# ----------------------------------------------------------------------------- +# Find the Boost library and configure usage +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +find_package(Boost 1.79.0 REQUIRED COMPONENTS serialization) + IF((NOT ${NGEN}) AND (NOT ${STANDALONE})) message("Pseudo-framework build") @@ -65,11 +72,13 @@ if(STANDALONE) ./src/linked_list.cxx ./src/mem_funcs.cxx ./src/util_funcs.cxx ./src/aet.cxx ./src/Logger.cpp ./include/Logger.hpp ./giuh/giuh.h ./giuh/giuh.c) target_link_libraries(${exe_name} PRIVATE m) + target_link_libraries(${exe_name} PRIVATE Boost::serialization) elseif(UNITTEST) add_executable(${exe_name} ./tests/main_unit_test_bmi.cxx ./src/bmi_lgar.cxx ./src/lgar.cxx ./src/soil_funcs.cxx ./src/linked_list.cxx ./src/mem_funcs.cxx ./src/util_funcs.cxx ./src/aet.cxx ./src/Logger.cpp ./include/Logger.hpp ./giuh/giuh.h ./giuh/giuh.c) target_link_libraries(${exe_name} PRIVATE m) + target_link_libraries(${exe_name} PRIVATE Boost::serialization) endif() @@ -88,12 +97,15 @@ else() ./src/util_funcs.cxx ./src/aet.cxx ./src/Logger.cpp ./include/Logger.hpp ./giuh/giuh.c include/all.hxx ./giuh/giuh.h) endif() + target_include_directories(lasambmi PRIVATE include) set_target_properties(lasambmi PROPERTIES VERSION ${PROJECT_VERSION}) set_target_properties(lasambmi PROPERTIES PUBLIC_HEADER ./include/bmi_lgar.hxx) +target_link_libraries(lasambmi PRIVATE Boost::serialization) + include(GNUInstallDirs) install(TARGETS lasambmi diff --git a/include/all.hxx b/include/all.hxx index a3c3fd2..cc64d3b 100755 --- a/include/all.hxx +++ b/include/all.hxx @@ -24,6 +24,7 @@ #include #include #include +#include using namespace std; @@ -53,6 +54,18 @@ struct wetting_front bool to_bottom; // TRUE iff this wetting front is in contact with the layer bottom double dzdt_cm_per_h; // use to store the calculated wetting front speed struct wetting_front *next; // pointer to the next wetting front. + + template + void serialize(Archive &ar, const unsigned int version) { + ar & this->depth_cm; + ar & this->theta; + ar & this->psi_cm; + ar & this->K_cm_per_h; + ar & this->layer_num; + ar & this->front_num; + ar & this->to_bottom; + ar & this->dzdt_cm_per_h; + } }; /* head is a GLOBALLY defined pointer to the first link in the wetting front list. diff --git a/include/bmi_lgar.hxx b/include/bmi_lgar.hxx index b29339a..7c843c5 100644 --- a/include/bmi_lgar.hxx +++ b/include/bmi_lgar.hxx @@ -14,6 +14,8 @@ using namespace std; #include "../bmi/bmi.hxx" #include "all.hxx" #include +#include "vecbuf.hpp" +#include extern "C" { #include "../giuh/giuh.h" @@ -30,7 +32,11 @@ class NotImplemented : public std::logic_error { class BmiLGAR : public bmi::Bmi { public: ~BmiLGAR(); - BmiLGAR():giuh_ordinates(nullptr), giuh_runoff_queue(nullptr) { + BmiLGAR() : + giuh_ordinates(nullptr), + giuh_runoff_queue(nullptr), + m_serialized{} + { this->input_var_names[0] = "precipitation_rate"; this->input_var_names[1] = "potential_evapotranspiration_rate"; this->input_var_names[2] = "soil_temperature_profile"; @@ -130,8 +136,9 @@ public: void global_mass_balance(); double update_calibratable_parameters(); struct model_state* get_model(); - + private: + friend class boost::serialization::access; void realloc_soil(); struct model_state* state; static const int input_var_name_count = 3; @@ -146,6 +153,9 @@ private: double *giuh_ordinates; double *giuh_runoff_queue; + vecbuf m_serialized; + uint64_t m_serialized_length; // needs a stable anchor for GetValuePtr + // unit conversion //struct unit_conversion units; struct bmi_unit_conversion { @@ -163,6 +173,15 @@ private: }; struct bmi_unit_conversion bmi_unit_conv; + + template + void serialize(Archive& ar, const unsigned int version); + template + void serialize_wetting_front_list(Archive &ar, wetting_front **head, int &count); + + void new_serialized(); + void load_serialized(const char* data); + void free_serialized(); }; diff --git a/include/vecbuf.hpp b/include/vecbuf.hpp new file mode 100644 index 0000000..6db982c --- /dev/null +++ b/include/vecbuf.hpp @@ -0,0 +1,115 @@ +#ifndef HPP_STRING_VECBUF +#define HPP_STRING_VECBUF +// https://gist.github.com/stephanlachnit/4a06f8475afd144e73235e2a2584b000 +// SPDX-FileCopyrightText: 2023 Stephan Lachnit +// SPDX-License-Identifier: MIT + +#include +#include +#include + +template> +class vecbuf : public std::basic_streambuf { +public: + using streambuf = std::basic_streambuf; + using char_type = typename streambuf::char_type; + using int_type = typename streambuf::int_type; + using traits_type = typename streambuf::traits_type; + using vector = std::vector; + using value_type = typename vector::value_type; + using size_type = typename vector::size_type; + + // Constructor for vecbuf with optional initial capacity + vecbuf(size_type capacity = 0) : vector_() { reserve(capacity); } + + // Forwarder for std::vector::shrink_to_fit() + constexpr void shrink_to_fit() { vector_.shrink_to_fit(); } + + // Forwarder for std::vector::clear() + constexpr void clear() { vector_.clear(); } + + // Forwarder for std::vector::reserve + constexpr void reserve(size_type capacity) { vector_.reserve(capacity); setp_from_vector(); } + + // Increase the capacity of the buffer by reserving the current_size + additional_capacity + constexpr void reserve_additional(size_type additional_capacity) { reserve(size() + additional_capacity); } + + // Forwarder for std::vector::data + constexpr const value_type* data() const { return vector_.data(); } + + // Forwarder for std::vector::size + constexpr size_type size() const { return vector_.size(); } + + // Forwarder for std::vector::capacity + constexpr size_type capacity() const { return vector_.capacity(); } + + // Implements std::basic_streambuf::xsputn + std::streamsize xsputn(const char_type* s, std::streamsize count) override { + try { + reserve_additional(count); + } + catch (const std::bad_alloc& error) { + // reserve did not work, use slow algorithm + return xsputn_slow(s, count); + } + // reserve worked, use fast algorithm + return xsputn_fast(s, count); + } + +protected: + // Calculates value to std::basic_streambuf::pbase from vector + constexpr value_type* pbase_from_vector() const { return const_cast(vector_.data()); } + + // Calculates value to std::basic_streambuf::pptr from vector + constexpr value_type* pptr_from_vector() const { return const_cast(vector_.data() + vector_.size()); } + + // Calculates value to std::basic_streambuf::epptr from vector + constexpr value_type* epptr_from_vector() const { return const_cast(vector_.data()) + vector_.capacity(); } + + // Sets the values for std::basic_streambuf::pbase, std::basic_streambuf::pptr and std::basic_streambuf::epptr from vector + constexpr void setp_from_vector() { streambuf::setp(pbase_from_vector(), epptr_from_vector()); streambuf::pbump(size()); } + +private: + // std::vector containing the data + vector vector_; + + // Fast implementation of std::basic_streambuf::xsputn if reserve_additional(count) succeeded + std::streamsize xsputn_fast(const char_type* s, std::streamsize count) { + // store current pptr (end of vector location) + auto* old_pptr = pptr_from_vector(); + // resize the vector, does not move since space already reserved + vector_.resize(vector_.size() + count); + // directly memcpy new content to old pptr (end of vector before it was resized) + traits_type::copy(old_pptr, s, count); + // reserve() already calls setp_from_vector(), only adjust pptr to new epptr + streambuf::pbump(count); + + return count; + } + + // Slow implementation of std::basic_streambuf::xsputn if reserve_additional(count) did not succeed, might calls std::basic_streambuf::overflow() + std::streamsize xsputn_slow(const char_type* s, std::streamsize count) { + // reserving entire vector failed, emplace char for char + std::streamsize written = 0; + while (written < count) { + try { + // copy one char, should throw eventually std::bad_alloc + vector_.emplace_back(s[written]); + } + catch (const std::bad_alloc& error) { + // try overflow(), if eof return, else continue + int_type c = this->overflow(traits_type::to_int_type(s[written])); + if (traits_type::eq_int_type(c, traits_type::eof())) { + return written; + } + } + // update pbase, pptr and epptr + setp_from_vector(); + written++; + } + return written; + } + +}; + +#endif diff --git a/src/Logger.cpp b/src/Logger.cpp index f296e42..edb7cc3 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -1,4 +1,4 @@ -#include "Logger.hpp" +#include "../include/Logger.hpp" #include #include #include diff --git a/src/bmi_lgar.cxx b/src/bmi_lgar.cxx index 0241bcd..8f1d1b2 100644 --- a/src/bmi_lgar.cxx +++ b/src/bmi_lgar.cxx @@ -14,6 +14,9 @@ #include "../include/all.hxx" #include "../include/Logger.hpp" +#include +#include + std::stringstream bmilgar_ss(""); // default verbosity is set to 'none' other option 'high' or 'low' needs to be specified in the config file @@ -703,36 +706,64 @@ Finalize() delete [] state->lgar_bmi_params.frozen_factor; delete state->lgar_bmi_input_params; delete state; + this->state = NULL; } int BmiLGAR:: GetVarGrid(std::string name) { - if (name.compare("soil_storage_model") == 0 || name.compare("soil_num_wetting_fronts") == 0) // int + if ( + name.compare("soil_storage_model") == 0 + || name.compare("soil_num_wetting_fronts") == 0 + || name.compare("serialization_free") == 0 + ) // int return 0; - else if (name.compare("precipitation_rate") == 0 || name.compare("precipitation") == 0) - return 1; - else if (name.compare("potential_evapotranspiration_rate") == 0 - || name.compare("potential_evapotranspiration") == 0 - || name.compare("actual_evapotranspiration") == 0) // double + else if ( + name.compare("precipitation_rate") == 0 + || name.compare("precipitation") == 0 + || name.compare("potential_evapotranspiration_rate") == 0 + || name.compare("potential_evapotranspiration") == 0 + || name.compare("actual_evapotranspiration") == 0 + || name.compare("surface_runoff") == 0 + || name.compare("giuh_runoff") == 0 + || name.compare("soil_storage") == 0 + || name.compare("field_capacity") == 0 + || name.compare("ponded_depth_max") == 0 + || name.compare("total_discharge") == 0 + || name.compare("infiltration") == 0 + || name.compare("percolation") == 0 + || name.compare("groundwater_to_stream_recharge") == 0 + || name.compare("mass_balance") == 0 + ) // double return 1; - else if (name.compare("surface_runoff") == 0 || name.compare("giuh_runoff") == 0 - || name.compare("soil_storage") == 0 || name.compare("field_capacity") == 0 || name.compare("ponded_depth_max") == 0)// double - return 1; - else if (name.compare("total_discharge") == 0 || name.compare("infiltration") == 0 - || name.compare("percolation") == 0 || name.compare("groundwater_to_stream_recharge") == 0) // double - return 1; - else if (name.compare("mass_balance") == 0) - return 1; - else if (name.compare("soil_depth_layers") == 0 || name.compare("smcmax") == 0 || name.compare("smcmin") == 0 - || name.compare("van_genuchten_m") == 0 || name.compare("van_genuchten_alpha") == 0 || name.compare("van_genuchten_n") == 0 - || name.compare("hydraulic_conductivity") == 0) // array of doubles (fixed length) + else if ( + name.compare("soil_depth_layers") == 0 + || name.compare("smcmax") == 0 + || name.compare("smcmin") == 0 + || name.compare("van_genuchten_m") == 0 + || name.compare("van_genuchten_alpha") == 0 + || name.compare("van_genuchten_n") == 0 + || name.compare("hydraulic_conductivity") == 0 + ) // array of doubles (fixed length) return 2; - else if (name.compare("soil_moisture_wetting_fronts") == 0 || name.compare("soil_depth_wetting_fronts") == 0) // array of doubles (dynamic length) + else if ( + name.compare("soil_moisture_wetting_fronts") == 0 + || name.compare("soil_depth_wetting_fronts") == 0 + ) // array of doubles (dynamic length) return 3; - else if (name.compare("soil_temperature_profile") == 0) // array of doubles (fixed and of the size of soil temperature profile) + else if ( + name.compare("soil_temperature_profile") == 0 + ) // array of doubles (fixed and of the size of soil temperature profile) return 4; + else if ( + name.compare("serialization_state") == 0 + ) // char + return 5; + else if ( + name.compare("serialization_create") == 0 + ) // uint64_t + return 6; else return -1; } @@ -747,6 +778,10 @@ GetVarType(std::string name) return "int"; else if (var_grid == 1 || var_grid == 2 || var_grid == 3 || var_grid == 4) return "double"; + else if (var_grid == 5) + return "char"; + else if (var_grid == 6) + return "uint64_t"; else return "none"; } @@ -761,6 +796,10 @@ GetVarItemsize(std::string name) return sizeof(int); else if (var_grid == 1 || var_grid == 2 || var_grid == 3 || var_grid == 4) return sizeof(double); + else if (var_grid == 5) + return sizeof(char); + else if (var_grid == 6) + return sizeof(uint64_t); else return 0; } @@ -882,6 +921,8 @@ GetGridSize(const int grid) return this->state->lgar_bmi_params.num_wetting_fronts; else if (grid == 4) // number of cells (discretized temperature profile, input from SFT) return this->state->lgar_bmi_params.num_cells_temp; + else if (grid == 5) + return this->m_serialized_length; // serialized state length else return -1; } @@ -895,8 +936,13 @@ GetValue (std::string name, void *dest) int nbytes = 0; src = this->GetValuePtr(name); - nbytes = this->GetVarNbytes(name); - memcpy (dest, src, nbytes); + + if (name.compare("serialization_state") == 0) { + memcpy(dest, src, this->m_serialized_length); + } else { + nbytes = this->GetVarNbytes(name); + memcpy (dest, src, nbytes); + } } @@ -953,7 +999,12 @@ GetValuePtr (std::string name) return (void*)&this->state->lgar_calib_params.ponded_depth_max; else if (name.compare("field_capacity") == 0) return (void*)&this->state->lgar_calib_params.field_capacity_psi; - else { + else if (name.compare("serialization_state") == 0) + return (void*)(this->m_serialized.data()); + else if (name.compare("serialization_create") == 0) { + this->new_serialized(); + return (void*)(&this->m_serialized_length); + } else { std::stringstream errMsg; errMsg << "variable "<< name << " does not exist"; throw std::runtime_error(errMsg.str()); @@ -990,6 +1041,18 @@ GetValueAtIndices (std::string name, void *dest, int *inds, int len) void BmiLGAR:: SetValue (std::string name, void *src) { + // state serialization exceptions + if (name == "serialization_state") { + this->load_serialized((char*)src); + return; + } else if (name == "serialization_free") { + this->free_serialized(); + return; + } else if (name.compare("serialization_create") == 0) { + auto msg = "Cannot set values with \"serialization_create\"."; + Logger::Log(LogLevel::WARNING, msg); + throw std::runtime_error(msg); + } void * dest = NULL; dest = this->GetValuePtr(name); @@ -1208,4 +1271,159 @@ GetGridNodesPerFace(const int grid, int *nodes_per_face) throw bmi_lgar::NotImplemented(); } + +// low-impoact serialization. This will only archive mutable state that is not recalculated during Update or is accessable through BMI getters/setters +template +void BmiLGAR:: +serialize(Archive& ar, const unsigned int version) { + model_state* state = this->state; + + // GetValuePtr properties + ar & state->lgar_bmi_input_params->precipitation_mm_per_h; + ar & this->bmi_unit_conv.volprecip_timestep_m; + ar & state->lgar_bmi_input_params->PET_mm_per_h; + ar & this->bmi_unit_conv.volPET_timestep_m; + ar & this->bmi_unit_conv.volAET_timestep_m; + ar & this->bmi_unit_conv.volrunoff_giuh_timestep_m; + ar & this->bmi_unit_conv.volend_timestep_m; + ar & this->bmi_unit_conv.volQ_timestep_m; + ar & this->bmi_unit_conv.volin_timestep_m; + ar & this->bmi_unit_conv.volrech_timestep_m; + ar & this->bmi_unit_conv.volQ_gw_timestep_m; + ar & this->bmi_unit_conv.mass_balance_m; + ar & this->bmi_unit_conv.volrunoff_timestep_m; + ar & state->lgar_bmi_params.num_wetting_fronts; + ar & state->lgar_calib_params.ponded_depth_max; + ar & state->lgar_calib_params.field_capacity_psi; + + // end of update + ar & state->lgar_mass_balance.volprecip_timestep_cm; + ar & state->lgar_mass_balance.volin_timestep_cm; + ar & state->lgar_mass_balance.volon_timestep_cm; + ar & state->lgar_mass_balance.volend_timestep_cm; + ar & state->lgar_mass_balance.volAET_timestep_cm; + ar & state->lgar_mass_balance.volrech_timestep_cm; + ar & state->lgar_mass_balance.volrunoff_timestep_cm; + ar & state->lgar_mass_balance.volQ_timestep_cm; + ar & state->lgar_mass_balance.volQ_gw_timestep_cm; + ar & state->lgar_mass_balance.volPET_timestep_cm; + ar & state->lgar_mass_balance.volrunoff_giuh_timestep_cm; + + ar & state->lgar_mass_balance.volprecip_cm; + ar & state->lgar_mass_balance.volin_cm; + ar & state->lgar_mass_balance.volon_cm; + ar & state->lgar_mass_balance.volend_cm; + ar & state->lgar_mass_balance.volAET_cm; + ar & state->lgar_mass_balance.volrech_cm; + ar & state->lgar_mass_balance.volrunoff_cm; + ar & state->lgar_mass_balance.volQ_cm; + ar & state->lgar_mass_balance.volQ_gw_cm; + ar & state->lgar_mass_balance.volPET_cm; + ar & state->lgar_mass_balance.volrunoff_giuh_cm; + ar & state->lgar_mass_balance.volchange_calib_cm; + + ar & boost::serialization::make_array(state->lgar_bmi_params.cum_layer_thickness_cm, state->lgar_bmi_params.num_layers); + + // giuh state + ar & boost::serialization::make_array(this->giuh_runoff_queue, state->lgar_bmi_params.num_giuh_ordinates); + + // in frozen_factor_hydraulic_conductivity + if (state->lgar_bmi_params.sft_coupled) { + ar & boost::serialization::make_array(state->lgar_bmi_params.frozen_factor, state->lgar_bmi_params.num_layers); + } + + // update_calibratable_parameters + ar & state->lgar_bmi_params.field_capacity_psi_cm; + ar & state->lgar_bmi_params.calib_params_flag; + + // may be set in adapative timesteps + if (state->lgar_bmi_params.adaptive_timestep){ + ar & state->lgar_bmi_params.timestep_h; + } + + // how much time has passed since instantiation + ar & state->lgar_bmi_params.time_s; + ar & state->lgar_bmi_params.timesteps; + + // serialization of arbitrarily-lengthed linked-lists + int num_fronts = state->lgar_bmi_params.num_wetting_fronts; + this->serialize_wetting_front_list(ar, &state->head, state->lgar_bmi_params.num_wetting_fronts); + int num_previous = 0; + this->serialize_wetting_front_list(ar, &state->state_previous, num_previous); + + if (Archive::is_loading::value && num_fronts != state->lgar_bmi_params.num_wetting_fronts) { + // reallocate arrays based on new num_wetting_fronts + this->realloc_soil(); + } + ar & boost::serialization::make_array( + state->lgar_bmi_params.soil_moisture_wetting_fronts, state->lgar_bmi_params.num_wetting_fronts + ); + ar & boost::serialization::make_array( + state->lgar_bmi_params.soil_depth_wetting_fronts, state->lgar_bmi_params.num_wetting_fronts + ); + +} + +template +void BmiLGAR::serialize_wetting_front_list(Archive &ar, wetting_front **head, int &count) { + wetting_front *current; + if (Archive::is_saving::value) { + if (count == 0) // assume recalculation needed if 0 + count = listLength(*head); + ar & count; + current = *head; + while (current != NULL) { + ar & (*current); + current = current->next; + } + } else { // loading + ar & count; + listDelete(*head); + *head = NULL; + wetting_front *prior; + for (int i = 0; i < count; ++i) { + current = new wetting_front(); + ar & (*current); + if (i == 0) { + *head = current; + } else { + prior->next = current; + } + prior = current; + } + } +} + +void BmiLGAR::new_serialized() { + this->m_serialized.clear(); + boost::archive::binary_oarchive archive(this->m_serialized); + try { + archive << (*this); + this->m_serialized_length = this->m_serialized.size(); + } catch (const std::exception &e) { + Logger::Log(LogLevel::SEVERE, "Serializing LASAM encountered an error: %s", e.what()); + this->free_serialized(); + throw; + } +} + +void BmiLGAR::load_serialized(const char* data) { + std::stringstream stream(data); + boost::archive::binary_iarchive archive(stream); + try { + archive >> (*this); + } catch (const std::exception &e) { + Logger::Log(LogLevel::SEVERE, "Deserializing LASAM encountered an error: %s", e.what()); + throw; + } + this->free_serialized(); +} + +void BmiLGAR::free_serialized() { + this->m_serialized.clear(); + this->m_serialized.shrink_to_fit(); + this->m_serialized_length = 0; +} + + #endif diff --git a/src/linked_list.cxx b/src/linked_list.cxx index 1390ef9..90483c6 100755 --- a/src/linked_list.cxx +++ b/src/linked_list.cxx @@ -37,7 +37,7 @@ extern void listDelete(struct wetting_front* head) { while (head != NULL) { struct wetting_front *next = head->next; - free( head ); + delete head; head = next; } } @@ -73,7 +73,7 @@ extern struct wetting_front* listCopy(struct wetting_front* current, struct wett } else { - struct wetting_front* wf = (struct wetting_front*)malloc(sizeof(struct wetting_front)); + struct wetting_front* wf = new wetting_front(); wf->depth_cm = current->depth_cm; wf->theta = current->theta; @@ -99,7 +99,7 @@ extern void listInsertFirst(double depth, double theta, int front_num, int layer { //create a link - struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + struct wetting_front *link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -255,7 +255,7 @@ extern struct wetting_front* listDeleteFront(int front_num, struct wetting_front previous = current->next; } - if( current != NULL ) free( current ); + delete current; current = previous; while(previous != NULL) { // decrement all front numbers @@ -283,8 +283,7 @@ extern struct wetting_front* listInsertFront(double depth, double theta, int new if(new_front_num==1) { // create it //create a link - struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); - + struct wetting_front *link = new wetting_front(); link->depth_cm = depth; link->theta = theta; link->front_num = new_front_num; @@ -305,7 +304,7 @@ extern struct wetting_front* listInsertFront(double depth, double theta, int new do { if (previous->front_num == new_front_num-1) { // this is where we want to insert it //create a new link - struct wetting_front *link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + struct wetting_front *link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -353,7 +352,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ if(head == NULL) { // list is empty // Kinda weird. Shouldn't call this function to start a list. Create link in the first position - link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -378,7 +377,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ // yep. It's first //create a link and put it at the beginning of the list - link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link = new wetting_front(); link->depth_cm = depth; link->theta = theta; @@ -407,7 +406,7 @@ extern struct wetting_front* listInsertFrontAtDepth(int num_layers, double *cum_ // it is in this interval // create a link - link = (struct wetting_front*) malloc(sizeof(struct wetting_front)); + link = new wetting_front(); link->depth_cm = depth; link->theta = theta; From 6bb88ced23028e26abe29dcd6e14ac67c0d0cc4d Mon Sep 17 00:00:00 2001 From: Ian Todd Date: Mon, 25 Aug 2025 15:08:07 -0400 Subject: [PATCH 7/8] Convert Update-related asserts to logging the error and throwing --- src/bmi_lgar.cxx | 42 ++++++++++++++++++++++++++++++++++++------ src/lgar.cxx | 34 +++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/bmi_lgar.cxx b/src/bmi_lgar.cxx index 8f1d1b2..82e67ad 100644 --- a/src/bmi_lgar.cxx +++ b/src/bmi_lgar.cxx @@ -192,8 +192,18 @@ Update() LOG(bmilgar_ss.str(), LogLevel::INFO); bmilgar_ss.str(""); } - assert (state->lgar_bmi_input_params->precipitation_mm_per_h >= 0.0); - assert(state->lgar_bmi_input_params->PET_mm_per_h >=0.0); + if (state->lgar_bmi_input_params->precipitation_mm_per_h < 0.0) { + std::stringstream error_message; + error_message << "Pr [mm/h] is less than 0: " << state->lgar_bmi_input_params->precipitation_mm_per_h; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } + if (state->lgar_bmi_input_params->PET_mm_per_h < 0.0) { + std::stringstream error_message; + error_message << "PET [mm/h] is less than 0: " << state->lgar_bmi_input_params->PET_mm_per_h; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } // adaptive time step is set if (adaptive_timestep) { @@ -488,7 +498,13 @@ Update() // store local mass balance error to the struct state->lgar_mass_balance.local_mass_balance = local_mb; - assert (state->head->depth_cm > 0.0); // check on negative layer depth --> move this to somewhere else AJ (later) + // check on negative layer depth --> move this to somewhere else AJ (later) + if (state->head->depth_cm <= 0.0) { + std::stringstream error_message; + error_message << "Cycle " << cycle << " has a depth less than or equal to 0: " << state->head->depth_cm; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } bool lasam_standalone = true; #ifdef NGEN @@ -519,7 +535,12 @@ Update() // update thickness/depth and soil moisture of wetting fronts (used for state coupling) struct wetting_front *current = state->head; for (int i=0; ilgar_bmi_params.num_wetting_fronts; i++) { - assert (current != NULL); + if (current == NULL) { + std::stringstream error_message; + error_message << "Wetting front at index " << i << " is null."; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } state->lgar_bmi_params.soil_moisture_wetting_fronts[i] = current->theta; state->lgar_bmi_params.soil_depth_wetting_fronts[i] = current->depth_cm * state->units.cm_to_m; current = current->next; @@ -577,7 +598,11 @@ Update() void BmiLGAR:: UpdateUntil(double t) { - assert (t > 0.0); + if (t <= 0.0) { + const char *error_message = "Time must be greater than 0."; + LOG(LogLevel::SEVERE, error_message); + throw std::invalid_argument(error_message); + } this->Update(); } @@ -607,7 +632,12 @@ update_calibratable_parameters() layer_num = current->layer_num; soil = state->lgar_bmi_params.layer_soil_type[layer_num]; - assert (current != NULL); + if (current == NULL) { + std::stringstream error_message; + error_message << "Wetting front at index " << i << " is null."; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::invalid_argument(error_message.str()); + } if (verbosity.compare("high") == 0 || verbosity.compare("low") == 0) { bmilgar_ss <<"----------- Calibratable parameters depending on soil layer (initial values) ----------- \n"; diff --git a/src/lgar.cxx b/src/lgar.cxx index a9b9b26..f003693 100755 --- a/src/lgar.cxx +++ b/src/lgar.cxx @@ -856,9 +856,12 @@ extern void frozen_factor_hydraulic_conductivity(struct lgar_bmi_parameters lgar break; } - - // assert (layer_temp > 100.0); // just a check to ensure the while loop executes at least once - assert (count > 0); // just a check to ensure the while loop executes at least once + if (count <= 0) { // just a check to ensure the while loop executes at least once + std::stringstream error_message; + error_message << "The count of layers used for getting layer " << layer << "'s temperature for frozen factor hydraulic conductivity is 0."; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } layer_temp /= count; // layer-averaged temperature @@ -991,6 +994,12 @@ extern void lgar_move_wetting_fronts(double timestep_h, double *volin_cm, int wf int *soil_type, double *frozen_factor, struct wetting_front** head, struct wetting_front* state_previous, struct soil_properties_ *soil_properties) { + if (old_mass <= 0.0) { + std::stringstream error_message; + error_message << "old_mass value must be greater than 0. Current value: " << old_mass; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::invalid_argument(error_message.str()); + } if (verbosity.compare("high") == 0) { LOG(LogLevel::DEBUG,"State before moving wetting fronts...\n"); @@ -1358,8 +1367,6 @@ extern void lgar_move_wetting_fronts(double timestep_h, double *volin_cm, int wf struct wetting_front *wf_free_drainage = listFindFront(wf_free_drainage_demand, *head, NULL); double mass_timestep = (old_mass + precip_mass_to_add) - (actual_ET_demand + free_drainage_demand); - - assert (old_mass > 0.0); if (fabs(wf_free_drainage->theta - theta_e_k1) < 1E-15) { @@ -1616,7 +1623,12 @@ extern void lgar_merge_wetting_fronts(int *soil_type, double *frozen_factor, str double current_mass_this_layer = current->depth_cm * (current->theta - next->theta) + next->depth_cm*(next->theta - next_to_next->theta); current->depth_cm = current_mass_this_layer / (current->theta - next_to_next->theta); - assert (current->depth_cm > 0.0); + if (current->depth_cm <= 0.0) { + std::stringstream error_message; + error_message << "Wetting front at index " << wf << " has a depth less than or equal to 0. Current value: " << current->depth_cm; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } layer_num = current->layer_num; soil_num = soil_type[layer_num]; @@ -2356,7 +2368,15 @@ extern int lgar_read_vG_param_file(char const* vG_param_file_name, int num_soil_ soil_properties[soil].bc_lambda = 2.0 / (p - 3.0); soil_properties[soil].bc_psib_cm = (p + 3.0) * (147.8 + 8.1 * p + 0.092 * p * p) / (2.0 * vg_alpha_per_cm * p * (p - 1.0) * (55.6 + 7.4 * p + p * p)); - assert(0.0 < soil_properties[soil].bc_psib_cm); + if (soil_properties[soil].bc_psib_cm <= 0.0) { + std::stringstream error_message; + error_message << "Brooks & Corey bubbling pressure for soil index " + << soil << " (" << soil_properties[soil].soil_name + << ") must be greater than 0. Current value: " + << soil_properties[soil].bc_psib_cm; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } } else { fprintf(stderr, "ERROR: van Genuchten parameter n must be greater than 1\n"); From fc9c369654202983cdb5be7ddca191e9a063e964 Mon Sep 17 00:00:00 2001 From: Ian Todd Date: Mon, 25 Aug 2025 15:28:36 -0400 Subject: [PATCH 8/8] Convert initialization asserts to EWTS messages --- src/lgar.cxx | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/lgar.cxx b/src/lgar.cxx index f003693..bb681b6 100755 --- a/src/lgar.cxx +++ b/src/lgar.cxx @@ -100,7 +100,12 @@ extern void lgar_initialize(string config_file, struct model_state *state) struct wetting_front *current = state->head; for (int i=0; ilgar_bmi_params.num_wetting_fronts; i++) { - assert (current != NULL); + if (current == NULL) { + std::stringstream error_message; + error_message << "Wetting front at index " << i << " is null."; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } soil = state->lgar_bmi_params.layer_soil_type[i+1]; @@ -433,7 +438,12 @@ extern void InitFromConfigFile(string config_file, struct model_state *state) else if (param_unit == "[h]" || param_unit == "[hr]") state->lgar_bmi_params.timestep_h /= 1.0; // convert to hours - assert (state->lgar_bmi_params.timestep_h > 0); + if (state->lgar_bmi_params.timestep_h <= 0) { + std::stringstream error_message; + error_message << "The timestep must be greater than 0. Current value in hours: " << state->lgar_bmi_params.timestep_h; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } is_timestep_set = true; state->lgar_bmi_params.minimum_timestep_h = state->lgar_bmi_params.timestep_h; @@ -457,7 +467,12 @@ extern void InitFromConfigFile(string config_file, struct model_state *state) else if (param_unit == "[d]" || param_unit == "[day]") state->lgar_bmi_params.endtime_s = stod(param_value) * 86400.0; - assert (state->lgar_bmi_params.endtime_s > 0); + if (state->lgar_bmi_params.endtime_s <= 0) { + std::stringstream error_message; + error_message << "The end time must be greater than 0. Current value in seconds: " << state->lgar_bmi_params.endtime_s; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } is_endtime_set = true; if (verbosity.compare("high") == 0) { @@ -478,7 +493,12 @@ extern void InitFromConfigFile(string config_file, struct model_state *state) else if (param_unit == "[h]" || param_unit == "[hr]") state->lgar_bmi_params.forcing_resolution_h /= 1.0; // convert to hours - assert (state->lgar_bmi_params.forcing_resolution_h > 0); + if (state->lgar_bmi_params.forcing_resolution_h <= 0) { + std::stringstream error_message; + error_message << "The forcing resolution must be greater than 0. Current value in hours: " << state->lgar_bmi_params.forcing_resolution_h; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } is_forcing_resolution_set = true; if (verbosity.compare("high") == 0) { @@ -727,7 +747,13 @@ extern void InitFromConfigFile(string config_file, struct model_state *state) state->lgar_bmi_params.nint = 120; // hacked, not needed to be an input option state->lgar_bmi_params.num_wetting_fronts = state->lgar_bmi_params.num_layers; - assert (state->lgar_bmi_params.num_layers == listLength(state->head)); + if (state->lgar_bmi_params.num_layers != listLength(state->head)) { + std::stringstream error_message; + error_message << "The number of wetting fronts in the configuration (" << state->lgar_bmi_params.num_layers + << ") is not the same as the number of wetting fronts created in memory (" << listLength(state->head) << ")."; + LOG(LogLevel::SEVERE, error_message.str()); + throw std::runtime_error(error_message.str()); + } if (verbosity.compare("high") == 0) { lgar_ss <<"Initial ponded depth is set to zero. \n";