diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ded820a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(git:*)" + ] + } +} diff --git a/.ruby-version b/.ruby-version index 7921bd0..7bcbb38 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.8 +3.4.9 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb9456e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Wisconsin Irrigation Scheduling Program (WISP)** — a Rails 8.0 web app for irrigation water management, developed for UW-Madison. It tracks daily root-zone water balance (rainfall, irrigation, ET, deep drainage) using the checkbook method and integrates with an external ag-weather microservice. + +## Commands + +```bash +# Development +bin/rails s # Start server (localhost:3000) +bundle exec rspec # Run all tests +bundle exec rspec spec/path/to_spec.rb # Run a single test file +bundle exec standard # Lint (Ruby Standard) + +# Database +bundle exec rake db:setup db:seed # Create DB, load schema, seed plants/soil types +bundle exec rake yearly:reset # Reset all field data (runs automatically Feb 15) + +# Deployment +cap staging deploy # Deploy to dev.wisp.cals.wisc.edu +cap production deploy # Deploy to wisp.cals.wisc.edu +``` + +Guard can auto-run tests on file changes: `guard -i rspec` + +## Architecture + +### Domain Model Hierarchy + +``` +User (Devise auth) + └── Group (farm operation group, via memberships) + └── Farm (collection of pivots, one per year) + └── Pivot (irrigation equipment / field location) + └── Field (soil/crop area with water balance params) + ├── Crop (plant type + emergence date + MAD + root zone) + └── FieldDailyWeather (one row per day: rain, irrigation, ET, AD, soil moisture) +``` + +Fields also link to **WeatherStations** (called "Field Groups" in the UI) via `multi_edit_links`, allowing shared weather/irrigation data entry across grouped fields. + +### Key Calculations (vendor gem: `asigbiophys`) + +Located in `vendor/asigbiophys/lib/`: +- **`ADCalculator`** — Available water Depletion: tracks daily soil water balance +- **`ETCalculator`** — Evapotranspiration: uses reference ET from ag-weather + crop-specific coefficients, supporting two methods: Percent Cover or LAI (Leaf Area Index) + +### External Dependency: ag-weather + +`app/clients/ag_weather.rb` wraps REST calls to the ag-weather service running on port `8080`. This service provides reference ET, precipitation, and degree-day data. The app requires ag-weather to be running locally for full functionality. + +### Plants (STI) + +Plant types live in `app/models/plants/` as STI subclasses (Corn, Potato, Soybean, etc.) with definitions seeded from `db/plants.yml`. Soil water holding capacity defaults come from `db/soil_types.yml`. + +### Routes & Controllers + +All main WISP UI views route through `/wisp/*` (collection actions only — no standard CRUD). Resource controllers (`farms`, `pivots`, `fields`, `crops`, `field_daily_weather`, `weather_stations`) use jqGrid for server-side data tables and respond primarily to `index` + `post_data` actions. + +### Security + +- **Rack::Attack** (`config/initializers/rack_attack.rb`): rate-limits to 25 req/sec globally, 5 write req/sec per IP +- **Devise** handles authentication; group memberships (`memberships` table) control access with an admin flag + +## Ruby & Rails Upgrades + +When upgrading Ruby: update `.ruby-version`, `config/deploy.rb`, and the README version reference. + +When upgrading Rails: `THOR_MERGE="code -d $1 $2" rails app:update` (uses VSCode as merge tool). + +## Testing + +RSpec with FactoryBot. Tests live in `spec/`. Migrations are excluded from Standard linting (`.standard.yml`). diff --git a/Capfile b/Capfile index 508de25..f59d354 100644 --- a/Capfile +++ b/Capfile @@ -6,6 +6,14 @@ require "capistrano/deploy" require "capistrano/scm/git" install_plugin Capistrano::SCM::Git +# Include tasks from other gems included in your Gemfile +# +# For documentation on these, see for example: +# +# https://github.com/capistrano/rbenv +# https://github.com/capistrano/bundler +# https://github.com/capistrano/rails +# require "capistrano/rbenv" require "capistrano/bundler" require "capistrano/rails/assets" diff --git a/Gemfile.lock b/Gemfile.lock index c6c6471..02e4b00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,7 +13,7 @@ PATH GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.16) + action_text-trix (2.1.17) railties actioncable (8.1.2) actionpack (= 8.1.2) @@ -87,13 +87,13 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.8) + addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) - airbrussh (1.6.0) + airbrussh (1.6.1) sshkit (>= 1.6.1, != 1.7.0) ast (2.4.3) base64 (0.3.0) - bcrypt (3.1.21) + bcrypt (3.1.22) bcrypt_pbkdf (1.1.2) bigdecimal (4.0.1) bindex (0.8.1) @@ -134,7 +134,7 @@ GEM date (3.5.1) decent_exposure (3.0.4) activesupport (>= 4.0) - devise (5.0.2) + devise (5.0.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 7.0) @@ -148,7 +148,7 @@ GEM railties (>= 6.1) drb (2.2.3) ed25519 (1.4.0) - erb (6.0.1) + erb (6.0.2) erubi (1.13.1) execjs (2.10.0) factory_bot (6.5.6) @@ -205,7 +205,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.18.1) + json (2.19.2) language_server-protocol (3.17.0.5) launchy (3.1.1) addressable (~> 2.8) @@ -224,7 +224,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.25.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) lumberjack (1.4.2) @@ -237,7 +237,8 @@ GEM marcel (1.1.0) method_source (1.1.0) mini_mime (1.1.5) - minitest (6.0.1) + minitest (6.0.2) + drb (~> 2.0) prism (~> 1.5) multi_xml (0.8.1) bigdecimal (>= 3.1, < 5) @@ -255,19 +256,19 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.1) net-protocol - net-ssh (7.3.0) + net-ssh (7.3.2) nio4r (2.7.5) - nokogiri (1.19.1-aarch64-linux-gnu) + nokogiri (1.19.2-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-aarch64-linux-musl) + nokogiri (1.19.2-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.1-arm-linux-gnu) + nokogiri (1.19.2-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-arm-linux-musl) + nokogiri (1.19.2-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-musl) + nokogiri (1.19.2-x86_64-linux-musl) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) @@ -294,7 +295,7 @@ GEM psych (5.3.1) date stringio - public_suffix (7.0.2) + public_suffix (7.0.5) puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) @@ -326,8 +327,8 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-ujs (0.1.0) railties (>= 3.1) @@ -365,17 +366,17 @@ GEM rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.3) + rspec-rails (8.0.4) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) rubocop (1.84.2) json (~> 2.3) @@ -388,7 +389,7 @@ GEM rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-performance (1.26.1) @@ -444,11 +445,11 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.26.0) stringio (3.2.0) - terser (1.2.6) + terser (1.2.7) execjs (>= 0.3.0, < 3) thor (1.5.0) tilt (2.7.0) - timeout (0.6.0) + timeout (0.6.1) tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -460,12 +461,11 @@ GEM valid_attribute (2.0.0) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 6.0.0) - webmock (3.26.1) + railties (>= 8.0.0) + webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -534,7 +534,7 @@ DEPENDENCIES will_paginate CHECKSUMS - action_text-trix (2.1.16) sha256=f645a2c21821b8449fd1d6770708f4031c91a2eedf9ef476e9be93c64e703a8a + action_text-trix (2.1.17) sha256=b44691639d77e67169dc054ceacd1edc04d44dc3e4c6a427aa155a2beb4cc951 actioncable (8.1.2) sha256=dc31efc34cca9cdefc5c691ddb8b4b214c0ea5cd1372108cbc1377767fb91969 actionmailbox (8.1.2) sha256=058b2fb1980e5d5a894f675475fcfa45c62631103d5a2596d9610ec81581889b actionmailer (8.1.2) sha256=f4c1d2060f653bfe908aa7fdc5a61c0e5279670de992146582f2e36f8b9175e9 @@ -546,12 +546,12 @@ CHECKSUMS activerecord (8.1.2) sha256=acfbe0cadfcc50fa208011fe6f4eb01cae682ebae0ef57145ba45380c74bcc44 activestorage (8.1.2) sha256=8a63a48c3999caeee26a59441f813f94681fc35cc41aba7ce1f836add04fba76 activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae - addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057 - airbrussh (1.6.0) sha256=7e2cf581f2319d2c2b2b672c9fc486efb4dfcfed4bd2dadbef5f10b8b2a000d0 + addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 + airbrussh (1.6.1) sha256=9a5fc95583cefe722054a016d26ff0338cf00072b031b829086dde2039d5836a asigbiophys (0.1.4) ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b - bcrypt (3.1.21) sha256=5964613d750a42c7ee5dc61f7b9336fb6caca429ba4ac9f2011609946e4a2dcf + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e @@ -574,14 +574,14 @@ CHECKSUMS csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 decent_exposure (3.0.4) sha256=5ce9e3df24f8d77f094b2e612d1c43a78a2de879847ccbb57c0e596e6c303cf1 - devise (5.0.2) sha256=254330c9290e612ad9681e8a18c96aa21fb95bc391af936b84480965444bb0b6 + devise (5.0.3) sha256=c4c065051cdc4ace11547b2b7f5c3c4c97d0f1269250f5fe90f614ff78f29546 diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d dotenv-rails (3.2.0) sha256=657e25554ba622ffc95d8c4f1670286510f47f2edda9f68293c3f661b303beab drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 - erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 + erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 execjs (2.10.0) sha256=6bcb8be8f0052ff9d370b65d1c080f2406656e150452a0abdb185a133048450d factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077 @@ -606,7 +606,7 @@ CHECKSUMS jqgrid-jquery-rails (4.6.1) sha256=78eb642f6e481dfaf9bd678670abf5415bbe8566ced102dfaa4ba38e4f8de47c jquery-rails (4.6.1) sha256=619f3496cdcdeaae1fd6dafa52dbac3fc45b745d4e09712da4184a16b3a8d9c0 jquery-ui-rails (8.0.0) - json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986 + json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e letter_opener (1.10.0) sha256=2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2 @@ -614,13 +614,13 @@ CHECKSUMS lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6 + loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 lumberjack (1.4.2) sha256=40de5ae46321380c835031bcc1370f13bba304d29f2b5f5bb152061a5a191b95 mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef - minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb + minitest (6.0.2) sha256=db6e57956f6ecc6134683b4c87467d6dd792323c7f0eea7b93f66bd284adbc3d multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a nenv (0.3.0) sha256=d9de6d8fb7072228463bf61843159419c969edb34b3cef51832b516ae7972765 net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad @@ -629,14 +629,14 @@ CHECKSUMS net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 - net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0 + net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 - nokogiri (1.19.1-aarch64-linux-gnu) sha256=cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32 - nokogiri (1.19.1-aarch64-linux-musl) sha256=1e2150ab43c3b373aba76cd1190af7b9e92103564063e48c474f7600923620b5 - nokogiri (1.19.1-arm-linux-gnu) sha256=0a39ed59abe3bf279fab9dd4c6db6fe8af01af0608f6e1f08b8ffa4e5d407fa3 - nokogiri (1.19.1-arm-linux-musl) sha256=3a18e559ee499b064aac6562d98daab3d39ba6cbb4074a1542781b2f556db47d - nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a - nokogiri (1.19.1-x86_64-linux-musl) sha256=4267f38ad4fc7e52a2e7ee28ed494e8f9d8eb4f4b3320901d55981c7b995fc23 + nokogiri (1.19.2-aarch64-linux-gnu) sha256=c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19 + nokogiri (1.19.2-aarch64-linux-musl) sha256=7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515 + nokogiri (1.19.2-arm-linux-gnu) sha256=b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081 + nokogiri (1.19.2-arm-linux-musl) sha256=61114d44f6742ff72194a1b3020967201e2eb982814778d130f6471c11f9828c + nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f + nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8 notiffany (0.1.3) sha256=d37669605b7f8dcb04e004e6373e2a780b98c776f8eb503ac9578557d7808738 orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 @@ -652,7 +652,7 @@ CHECKSUMS prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 - public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3 @@ -662,7 +662,7 @@ CHECKSUMS rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 rails (8.1.2) sha256=5069061b23dfa8706b9f0159ae8b9d35727359103178a26962b868a680ba7d95 rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d - rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560 + rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 rails-ujs (0.1.0) sha256=e03af9a79af5370d94f31be04e920a167bc4497cb991b5dcad90b6bd9fb142e0 railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a @@ -677,11 +677,11 @@ CHECKSUMS rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 - rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c - rspec-rails (8.0.3) sha256=b0a440e7a10700317d898a014852e26660867298c4076dbc3baa99c768b79dc1 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f - rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 sassc (2.4.0) sha256=4c60a2b0a3b36685c83b80d5789401c2f678c1652e3288315a1551d811d9f83e @@ -701,10 +701,10 @@ CHECKSUMS standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 - terser (1.2.6) sha256=6ddf00b93df7015b07e2b9b149e74cd70fa7aa4f0f89a15d9922a6ebd13f37ab + terser (1.2.7) sha256=1b12eb49769dadac44caac3485b38928ff4ab435f1bbbacfe8048cff84c6aa1b thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3 - timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 @@ -713,8 +713,8 @@ CHECKSUMS useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 valid_attribute (2.0.0) sha256=963a580e1724c8a53ed5f0cc7a361eb51cc705e40f218d60d3b27bc8d9c8d6a9 warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 - web-console (4.2.1) sha256=e7bcf37a10ea2b4ec4281649d1cee461b32232d0a447e82c786e6841fd22fe20 - webmock (3.26.1) sha256=4f696fb57c90a827c20aadb2d4f9058bbff10f7f043bd0d4c3f58791143b1cd7 + web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 + webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90 websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 whenever (1.1.2) sha256=af139a25cf6b5c7d2122686724c0b9a4a7495135a1f79a404e29f4cce48abd83 @@ -722,4 +722,4 @@ CHECKSUMS zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH - 4.0.4 + 4.0.8 diff --git a/WISP_ARCHITECTURE_AND_MESONET_PLAN.md b/WISP_ARCHITECTURE_AND_MESONET_PLAN.md new file mode 100644 index 0000000..8bcb303 --- /dev/null +++ b/WISP_ARCHITECTURE_AND_MESONET_PLAN.md @@ -0,0 +1,484 @@ +# WISP Architecture & Mesonet Implementation Plan + +**Wisconsin Irrigation Scheduling Program (WISP)** +*Architecture reference and implementation guide for external teams* + +--- + +## Abbreviations & Definitions + +| Abbreviation | Full Term | Definition | +|-------------|-----------|------------| +| AD | Allowable Depletion | Running daily measure of how much water has been depleted from the root zone relative to field capacity, in inches. Positive = water available; zero or negative = irrigation needed. Also called RAW (Readily Available Water). | +| AD_max | Maximum Allowable Depletion | The depletion threshold at which irrigation should be triggered. Equals MAD × TAW. | +| DD | Deep Drainage | Water that has percolated below the root zone because inputs (rain + irrigation) exceeded field capacity. Lost from the plant-available water budget. | +| ET | Evapotranspiration | Combined water loss from soil evaporation and plant transpiration. Measured in inches/day. | +| ETo / ref_ET | Reference Evapotranspiration | Standardized ET for a hypothetical short grass reference surface under well-watered conditions. Provided by ag-weather stations. Actual crop ET is derived by applying a crop coefficient. | +| adj_ET | Adjusted ET | The actual estimated crop water use for a given day, calculated by scaling ref_ET by a crop coefficient based on canopy cover or LAI. | +| FC | Field Capacity | The fraction of soil volume occupied by water after excess has drained (approximately 24–48 hrs after saturation). Upper bound of plant-available water. | +| Kc | Crop Coefficient | Dimensionless multiplier applied to ref_ET to estimate crop-specific water use. Derived from canopy cover (percent cover method) or leaf area index (LAI method). | +| LAI | Leaf Area Index | Ratio of total one-sided leaf area to ground surface area. Dimensionless. Used to compute Kc via Beer's law: Kc = 1.1 × (1 − e^(−1.5 × LAI)). | +| MAD | Maximum Allowable Depletion | The fraction of TAW that can be depleted before irrigation stress occurs. Default is 0.50 (50%). Crop- and grower-specific. | +| MRZD | Managed Root Zone Depth | The effective depth of the active crop root zone, in inches. Determines how large the water reservoir is. | +| pct_cover | Percent Canopy Cover | The fraction of the ground shaded by the crop canopy, expressed as 0–100%. Used as the primary input to the percent cover ET method. | +| PWP | Permanent Wilting Point | The soil moisture fraction below which plants can no longer extract water. Sets the lower bound of plant-available water. | +| RAW | Readily Available Water | Synonym for AD_max (Allowable Depletion). The portion of TAW that can be used before irrigation stress. | +| TAW | Total Available Water | Total inches of plant-available water in the root zone between field capacity and permanent wilting point. TAW = (FC − PWP) × MRZD. | +| WISP | Wisconsin Irrigation Scheduling Program | The UW-Madison web application this document describes. | + +--- + +## Part 1: WISP Architecture + +### Overview + +WISP implements the **checkbook method** of irrigation scheduling — a daily root-zone water balance that tracks inputs (rain, irrigation) against outputs (evapotranspiration) to estimate how much plant-available water remains in the soil. The core output is **AD (Allowable Depletion)**, a running daily tally in inches of water. When AD approaches zero the soil is at the irrigation trigger point. + +The application is a Rails 8 web app backed by PostgreSQL. It pulls reference ET and precipitation from an external ag-weather microservice (REST API). All irrigation and soil moisture inputs come from growers entering data. + +--- + +### Domain Model Hierarchy + +``` +User (Devise auth) + └── Group (farm operation) + └── Farm (one per year) + └── Pivot (irrigation equipment / GPS location) + └── Field (soil + crop configuration) + ├── Crop (plant type, emergence date, MAD, root zone) + └── FieldDailyWeather (one row per calendar day) +``` + +Fields can also share weather/irrigation data across multiple fields via **WeatherStation** (called "Field Groups" in the UI), linked through `multi_edit_links`. + +--- + +### Static Configuration (set once per field/season) + +| Parameter | Source | Description | +|-----------|--------|-------------| +| Soil type | User selects | Determines field capacity and wilting point | +| Field capacity (FC) | Soil type lookup or manual | Fraction of soil volume that holds water (e.g., 0.24 for loam) | +| Permanent wilting point (PWP) | Soil type lookup or manual | Fraction below which plants cannot extract water | +| Managed root zone depth (MRZD) | Crop default or manual | Depth in inches of active root zone | +| Max allowable depletion (MAD) | Crop default (50%) | Fraction of TAW that can deplete before irrigation is needed | +| Emergence date | User enters | Date crop emerged; governs canopy/LAI growth start | +| ET method | User selects | Percent Cover or LAI | + +#### Soil Type Reference Table + +| Soil | Field Capacity | Perm. Wilting Pt | +|------|---------------|-----------------| +| Sand | 0.10 | 0.04 | +| Sandy Loam | 0.15 | 0.05 | +| Loam | 0.24 | 0.08 | +| Silt Loam | 0.30 | 0.16 | +| Silt | 0.31 | 0.10 | +| Clay Loam | 0.34 | 0.15 | +| Clay | 0.37 | 0.20 | + +#### Crop Default Root Zone Depths + +| Crop | MRZD (inches) | Crop | MRZD (inches) | +|------|--------------|------|--------------| +| Onion | 12 | Sweet Corn | 27 | +| Carrot | 12 | Asparagus | 27 | +| Leafy Greens | 12 | Soybean | 33 | +| Cabbage | 15 | Field Corn | 33 | +| Broccoli | 15 | Wheat | 33 | +| Celery | 15 | Barley | 33 | +| Mint | 15 | Shell Peas | 36 | +| Tomato | 15 | Other | 36 | +| Potato | 16 | Alfalfa | 42 | +| Sweet Potato | 16 | Snap Bean | 21 | +| Beets | 18 | Pepper | 18 | +| Melon | 18 | Pumpkin | 18 | +| Cucumber | 18 | Winter Squash | 18 | +| Summer Squash | 18 | | | + +--- + +### Derived Setup Values (calculated once, recalculated if soil/crop changes) + +``` +TAW = (FC - PWP) × MRZD [inches; Total Available Water] +AD_max = MAD_frac × TAW [inches; max AD before irrigation needed] +pct_moisture_at_AD_min = (FC − (AD_max / MRZD)) × 100 [%; soil moisture at irrigation trigger] +``` + +These values define the "water budget" for the season. AD ranges from 0 (full, at field capacity) down to AD_max (at irrigation trigger) and further down to the permanent wilting point. + +--- + +### Daily Calculation Loop + +WISP runs April 1 – November 30. For each day the following inputs are needed: + +| Input | Source in WISP | Notes | +|-------|---------------|-------| +| `ref_ET` | ag-weather microservice (inches/day) | ETo, reference evapotranspiration | +| `rain` | ag-weather microservice (inches) | Overrideable by grower | +| `irrigation` | Grower-entered (inches) | | +| `pct_cover` or `leaf_area_index` | User-entered / interpolated / growth curve | Depends on ET method | + +#### Step 1 — Compute Adjusted ET + +The reference ET (ETo) is a standardized value for a reference grass surface. A crop coefficient scales it for actual crop water use. + +**Percent Cover Method** (default, most common): + +Uses regression coefficients from UW Extension pub A3600 (Table C), derived by J. Panuska, that relate percent canopy cover to a crop coefficient. The lookup covers 0–80%+ cover in 10% bands. Below 80% cover, two adjacent band values are interpolated linearly. At or above 80% cover, `adj_ET = ref_ET` (full crop coefficient ≈ 1.0). + +``` +# Regression coefficients [intercept, slope] per 10% cover band: +coeff = [ + [0,0], # 0% — special case (see below) + [-0.002263, 0.2377], # 10% + [-0.002789, 0.3956], # 20% + [-0.002368, 0.5395], # 30% + [-0.000316, 0.6684], # 40% + [-0.000053, 0.7781], # 50% + [ 0.001053, 0.8772], # 60% + [ 0.001947, 0.9395], # 70% + [ 0.000000, 1.0000], # 80%+ +] + +# For a given pct_cover and ref_ET: +band = floor(pct_cover / 10) +adj_ET_low = coeff[band][0] + ref_ET * coeff[band][1] +adj_ET_high = coeff[band+1][0] + ref_ET * coeff[band+1][1] +adj_ET = adj_ET_low + ((pct_cover - band*10) / 10) * (adj_ET_high - adj_ET_low) +``` + +*Special case at 0% cover:* a stepped lookup by ref_ET magnitude handles the bare-soil ET (0.0 / 0.010 / 0.020 inches/day for low/medium/high ref_ET), then interpolates up to the 10% band value. + +**LAI Method** (alternate, currently corn-specific in WISP): + +``` +# Corn LAI growth curve (days since emergence): +LAI = 9e-12 × days_since_emergence^7.95 × exp(-0.1 × days_since_emergence) + +# Crop coefficient and adjusted ET: +Kc = 1.1 × (1 − exp(−1.5 × LAI)) +adj_ET = Kc × ref_ET +``` + +For non-corn crops in the LAI path, WISP currently uses a degree-day based placeholder quadratic: `LAI ≈ −0.000003 × DD² + 0.0073 × DD − 0.6728`. + +#### Step 2 — Compute Change in Daily Storage + +``` +delta_storage = rain + irrigation − adj_ET +``` + +#### Step 3 — Update AD + +``` +water_inches = prev_AD + delta_storage + +if water_inches > AD_max: + AD = AD_max + deep_drainage = water_inches − AD_max # excess drains below root zone +else: + AD = water_inches + deep_drainage = 0.0 + +# Hard floor at permanent wilting point: +AD = max(AD, −(1 − MAD_frac) × TAW) # AD_at_PWP +``` + +Deep drainage is logged but otherwise discarded — it is water that has percolated below the root zone. + +#### Step 4 — Compute Soil Moisture Percent + +``` +pct_moisture = pct_moisture_at_AD_min + (AD / MRZD) × 100 +``` + +This back-converts AD (inches of available water) to a volumetric soil moisture percentage for display and for comparison against sensor readings. + +#### Missing ET Handling + +When `ref_ET` is zero or missing for a day, WISP substitutes the **mean of the top 3 adjusted ET values from the prior 7 days** (via a rolling ring buffer). This prevents the water balance from freezing on data-gap days. + +#### Soil Moisture Override (Observation Reset) + +If the user enters an **observed soil moisture %** for a day, the normal rain/ET/irrigation delta is bypassed. Instead, AD is re-derived directly from the observation: + +``` +AD = MRZD × (obs_pct_moisture − pct_moisture_at_AD_min) / 100 +AD = min(AD, TAW) # cannot exceed field capacity +``` + +This is a critical feature for resetting the modeled balance after a sensor reading or soil probe measurement. + +--- + +### Percent Cover Interpolation + +Between user-entered canopy cover dates, WISP linearly interpolates the `calculated_pct_cover` column. The user enters a few anchor points (e.g., emergence = 0%, 4 weeks later = 40%, at peak = 80%) and the system fills in daily values. This is the primary way canopy data is managed in practice. + +--- + +### ET Method Summary Comparison + +| | Percent Cover | LAI | +|-|--------------|-----| +| **User input** | % canopy cover on key dates | — (automatic growth curve) | +| **Growth curve** | None (user provides) | Polynomial/exponential function of days since emergence | +| **Crop coeff** | Regression table (A3600) | Beer's law: 1.1×(1−e^(−1.5×LAI)) | +| **Best for** | Any crop, simple, widely used | Corn (calibrated), requires degree-day data | +| **Mesonet fit** | Excellent | Possible if degree-days available | + +--- + +### External Weather Dependency + +WISP calls an ag-weather REST microservice (running on port 8080) to fetch: + +- `/evapotranspirations` — reference ET by lat/lon and date range +- `/precips` — precipitation by lat/lon and date range +- `/degree_days` — accumulated degree days (for LAI crops) + +The mesonet replaces this dependency entirely for the simplified implementation. + +--- + +### Irrigation Trigger Logic + +The daily status column color-codes fields: +- **Blue** — AD > 0 (water in the bank, no irrigation needed) +- **Yellow** — AD is within 2 days of 0 at projected ET rates +- **Red** — AD ≤ 0 (irrigation overdue) + +Projection uses the rolling mean of recent adj_ET values (same ring-buffer calculation as the missing ET fill). + +--- + +## Part 2: Mesonet Implementation Plan + +### Objective + +Add an irrigation scheduling widget to the state mesonet website that computes the daily water balance for a user-selected weather station, without a persistent database. The user provides a small number of inputs; the mesonet provides weather data. The calculation runs entirely in the browser (or on a stateless API call) for the current season to date. + +--- + +### Key Simplifications vs. WISP + +| WISP feature | Mesonet approach | +|-------------|-----------------| +| Multi-user accounts, farms, pivots, fields | No accounts; single-session, stateless | +| Persistent daily weather records | Fetch live from mesonet on demand | +| Full irrigation event tracking | User can optionally enter irrigation amounts | +| Observed soil moisture reset per day | User enters current soil moisture once to reset | +| Linear interpolation of pct_cover over season | User enters a single current pct_cover value | +| Multiple crops per field across years | One crop type per session | + +--- + +### User Inputs + +#### Required (always shown) + +| Input | Options | Maps to | +|-------|---------|---------| +| Weather station | Dropdown of mesonet stations | Data source for ref_ET, precip | +| Crop type | Dropdown (crop list from plants.yml) | Default MRZD, MAD | +| Soil type | Dropdown (7 soil types) | FC, PWP | +| Current % canopy cover | 0–100% slider or text field | adj_ET calculation | +| Season start / emergence date | Date picker (defaults to May 1) | Start of water balance | + +#### Optional / Adjustable (collapsible "Advanced" section) + +| Input | Default | Notes | +|-------|---------|-------| +| Current soil moisture (%) | Computed from balance | Allows observation-based reset | +| Root zone depth (inches) | Crop default | Override if known | +| MAD fraction | 0.50 | Override for sensitive crops | +| Irrigation applied (inches, per date) | 0 | Key override for accuracy | +| Precipitation adjustments | Mesonet value | User can correct for local gauge | + +--- + +### Mesonet Data Used + +The mesonet already provides these per-station, per-day values: + +| Data | WISP equivalent | Notes | +|------|----------------|-------| +| Reference ET (ETo, inches/day) | `ref_et` | Must be in inches; convert from mm if needed | +| Precipitation (inches) | `rain` | User-adjustable | +| Soil moisture (%) | `entered_pct_moisture` | Optional reset/anchor | +| Temperature (°F) | Degree days (optional) | Only needed for LAI method | + +--- + +### Recommended ET Method for Mesonet + +**Use the Percent Cover method.** It requires only a single canopy cover value entered by the user, uses no degree-day data, and is the most widely applicable across crop types. The LAI method could be offered as an advanced option for corn growers if degree-day data is available. + +--- + +### Calculation Flow (stateless, single session) + +``` +1. User selects station, crop, soil type, pct_cover, emergence date +2. Fetch daily weather (ref_ET, precip) from mesonet API for + emergence_date → today +3. Apply static setup: + FC, PWP ← soil type table + MRZD ← crop table (or user override) + MAD_frac ← 0.5 (or user override) + TAW = (FC − PWP) × MRZD + AD_max = MAD_frac × TAW + pct_at_min = (FC − AD_max/MRZD) × 100 +4. Set initial AD: + If user entered current soil moisture → convert to AD + Else → AD = 0.0 (assume field at capacity at season start) +5. For each day from emergence_date to today: + adj_ET = adj_et_pct_cover(ref_ET, pct_cover) + delta = rain + irrigation − adj_ET + AD, DD = daily_ad_and_dd(prev_AD, delta, MAD_frac, TAW) + AD = max(AD, AD_at_PWP) + pct_moisture = pct_at_min + (AD / MRZD × 100) + If user has entered obs. moisture for this date: + Override AD and pct_moisture from observation +6. Display daily table + current status +7. Optionally project 3–5 days forward using recent avg adj_ET +``` + +**Canopy cover handling:** For simplicity, use the single user-entered % cover as a constant for the entire season to date. Alternatively, offer a simple two-point input: emergence date (0%) and "today" (user's value), then linearly interpolate. + +--- + +### Output Display + +| Column | Description | +|--------|-------------| +| Date | Calendar date | +| Ref. ET (in) | From mesonet | +| Adj. ET (in) | After crop coefficient | +| Rain (in) | From mesonet (E = user-edited) | +| Irrigation (in) | User-entered | +| AD (in) | Running water balance | +| Soil Moisture (%) | Computed (E = entered by user) | +| Status | Full / OK / Caution / Irrigate | + +**Status thresholds (relative to AD_max):** + +| Status | Condition | +|--------|-----------| +| Full | AD > AD_max × 0.9 | +| OK | AD > AD_max × 0.5 | +| Caution | AD > 0 | +| Irrigate | AD ≤ 0 | + +--- + +### Implementation Technology Options + +| Approach | Pros | Cons | +|----------|------|------| +| Pure JavaScript / HTML in browser | Zero backend, instant, embeds in existing site | Weather API must allow CORS | +| Thin server-side script (Python/PHP) | Handles CORS, easy to add state if needed later | Requires server endpoint | +| Progressive web app / React component | Best for interactive editing, responsive | More build infrastructure | + +The calculation logic is simple enough to implement cleanly in JavaScript (no framework required) or as a single Python/Ruby script. The math is ~50 lines of pure functions with no dependencies. + +--- + +### Pseudocode Reference (language-agnostic) + +```python +# SETUP +FC, PWP = soil_type_lookup[soil_type] +MRZD = crop_lookup[crop_type]["mrzd"] +MAD = 0.50 +TAW = (FC - PWP) * MRZD +AD_max = MAD * TAW +pct_at_min = (FC - AD_max / MRZD) * 100 + +# DAILY LOOP +def adj_et_pct_cover(ref_et, pct_cover): + coeff = [(0,0),(-0.002263,0.2377),(-0.002789,0.3956), + (-0.002368,0.5395),(-0.000316,0.6684), + (-0.000053,0.7781),(0.001053,0.8772), + (0.001947,0.9395),(0.0,1.0)] + if ref_et < 1e-6: return 0.0 + band = min(int(pct_cover / 10), 7) + if band >= 8: return ref_et + lo = coeff[band][0] + ref_et * coeff[band][1] + hi = coeff[band+1][0] + ref_et * coeff[band+1][1] + frac = (pct_cover - band * 10) / 10 + return lo + frac * (hi - lo) + +def run_balance(days, initial_ad=0.0): + ad = initial_ad + results = [] + for day in days: + if day.obs_moisture is not None: + # Reset from observation + ad = MRZD * (day.obs_moisture - pct_at_min) / 100 + ad = min(ad, TAW) + else: + adj_et = adj_et_pct_cover(day.ref_et, pct_cover) + delta = day.rain + day.irrigation - adj_et + ad = min(ad + delta, AD_max) + ad = max(ad, -(1 - MAD) * TAW) # PWP floor + pct_moist = pct_at_min + (ad / MRZD * 100) + results.append({"date": day.date, "ad": ad, "pct_moisture": pct_moist}) + return results +``` + +--- + +### Phased Implementation Roadmap + +#### Phase 1 — Core Calculator (MVP) +- Single-page tool: station dropdown, crop, soil, pct_cover, emergence date +- Fetch mesonet ref_ET and precip for current season +- Display daily water balance table with status indicator +- Highlight today's row; show "Irrigate now" / "OK" at top + +#### Phase 2 — User Adjustments +- Editable precipitation column (flag adjusted values with "E") +- Irrigation input field per day (or a bulk "applied X inches on date Y" form) +- Soil moisture observation entry to reset the balance + +#### Phase 3 — Enhanced Canopy Model +- Two-point canopy entry (emergence % and current %) +- Linear interpolation between points +- Optional: 3-day forward projection using recent avg ET + +#### Phase 4 — Optional Persistence (if desired later) +- LocalStorage to remember station/crop/soil preferences +- URL-shareable state (encode inputs in query string) +- If server-side: lightweight session or signed token — no database required + +--- + +### Key Equations Summary + +| Calculation | Formula | Units | +|-------------|---------|-------| +| TAW | (FC − PWP) × MRZD | inches | +| AD_max | MAD × TAW | inches | +| pct_at_AD_min | (FC − AD_max/MRZD) × 100 | % | +| Adj. ET (pct cover) | Regression interpolation (A3600 table) | inches/day | +| Adj. ET (LAI corn) | 1.1 × (1 − e^(−1.5 × LAI)) × ref_ET | inches/day | +| Delta storage | rain + irrigation − adj_ET | inches/day | +| New AD | min(prev_AD + delta, AD_max) | inches | +| Deep drainage | max(0, prev_AD + delta − AD_max) | inches | +| Soil moisture | pct_at_AD_min + (AD/MRZD × 100) | % | + +--- + +### Source References + +- Core calculation gem: `vendor/asigbiophys/lib/` (ADCalculator, ETCalculator modules) +- Crop coefficients: UW Extension pub A3600, Table C (percent cover regression by J. Panuska) +- LAI corn curve: WI_Irrigation_Scheduler_(WIS)_VV6.3.11.xls +- Root zone depths: USDA NRCS Table 3.4 (nrcs141p2_017640.pdf) +- Soil water holding capacity: WISP `db/soil_types.yml` +- Plant defaults: WISP `db/plants.yml` diff --git a/config/deploy.rb b/config/deploy.rb index 8d82f2d..ffae31b 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -27,19 +27,29 @@ # Default value for linked_dirs is [] # set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system') -set :linked_dirs, %w[log tmp/pids tmp/cache tmp/sockets] +set :linked_dirs, %w[log tmp/pids tmp/sockets public/assets] # Default value for default_env is {} # set :default_env, { path: '/opt/ruby/bin:$PATH' } # Default value for keep_releases is 5 -# set :keep_releases, 5 +set :keep_releases, 5 # rbenv set :deploy_user, "deploy" set :rbenv_type, :user +set :rbenv_ruby, File.read(".ruby-version").strip # set the rbenv_ruby to the same version as specified in .ruby-version namespace :deploy do + desc "Precompile assets" + after :updated, :precompile_assets do + on roles(:app) do + within release_path do + execute :rake, "assets:precompile" + end + end + end + desc "Restart application" after :publishing, :restart do on roles(:app), in: :sequence, wait: 5 do