Skip to content

Remove R/rpy2 dependency; replace with cffdrs_py#5176

Open
conbrad wants to merge 21 commits intomainfrom
task/cffdrs-py-no-r
Open

Remove R/rpy2 dependency; replace with cffdrs_py#5176
conbrad wants to merge 21 commits intomainfrom
task/cffdrs-py-no-r

Conversation

@conbrad
Copy link
Collaborator

@conbrad conbrad commented Mar 2, 2026

Replaces all R/rpy2 calls in app/fire_behaviour/cffdrs.py with the pure-Python cffdrs_py package, eliminating the R runtime as a dependency.

Changes:

  • Rewrote all FWI and FBP wrappers in cffdrs.py to use cffdrs_py submodule imports
  • Removed CFFDRS singleton, r_importer.py, and CFFDRS pre-warming from startup
  • Removed rpy2 from pyproject.toml; added cffdrs git dependency (pinned to commit)
  • Removed R installation from base image Dockerfile, OpenShift build.yaml, and local dev Dockerfile
  • Improved None guards on bui_calc, initial_spread_index, fire_weather_index, and crown_fraction_burned; fixed drought_code silently substituting rh=0 when RH is None; fixed
    length_to_breadth_ratio returning rather than raising CFFDRSException; added per-row error handling to hourly_fine_fuel_moisture_code
  • Updated 32 fba_calc tests (removed CFFDRS.instance() pre-warming); deleted test_r.py

Behavioural notes:

  • duff_moisture_code returns 0.0 (not ~0.256) when temperature < 1.1°C — cffdrs_py clamps temp to -1.1 before computing the drying rate (Eq. 16), zeroing it out. The R implementation did not apply this clamp.
  • cbh uses a fuel type default if it is None, this was handled internally before in the R library.

Closes #1926

Test Links:
Landing Page
MoreCast
Percentile Calculator
C-Haines
FireCalc
FireCalc bookmark
Auto Spatial Advisory (ASA)
HFI Calculator
SFMS Insights
Fire Watch

@codecov
Copy link

codecov bot commented Mar 2, 2026

Codecov Report

❌ Patch coverage is 72.65625% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.78%. Comparing base (462ad8c) to head (04d5b76).

Files with missing lines Patch % Lines
.../packages/wps-api/src/app/fire_behaviour/cffdrs.py 72.22% 23 Missing and 12 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5176      +/-   ##
==========================================
- Coverage   68.80%   68.78%   -0.02%     
==========================================
  Files         392      391       -1     
  Lines       16373    16296      -77     
  Branches     1846     1815      -31     
==========================================
- Hits        11265    11210      -55     
+ Misses       4527     4515      -12     
+ Partials      581      571      -10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@conbrad conbrad requested review from brettedw and dgboss March 2, 2026 22:45
FMC, SFC, PC, PDF, CC, CBH, ISI, output = "WSV")
WSV <- ifelse(GS > 0 & FFMC > 0, WSV0, WS)
Calculate the net effective windspeed (WSV).
WSV = Slopecalc(..., output="WSV") when GS > 0 and FFMC > 0, else WS.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:nit - Maybe remove this line of the comment, I don't think it's very helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in: 74fbd6f

fuel_type=fuel_type.value,
ffmc=ffmc,
bui=bui,
wsv=wsv if wsv is not None else 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it setting wsv = 0 here is equivalent to setting wsv = NULL in the R implementation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, good catch, it should be required, changed here: 4d37183

return buildup_index(dmc, dc)


def rate_of_spread_t(fuel_type: FuelTypeEnum,
Copy link
Collaborator

@dgboss dgboss Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the R implementation we used to check the value being returned from the calculation and would raise a CFFDRSException if necessary. We're now returning the value without a check. Is this safe and intentional? We're doing this in a number of places now.

Copy link
Collaborator Author

@conbrad conbrad Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old pattern was defensive for a specific reason: the R functions could return non-numeric types (NA, NULL, wrong-length vectors) and rpy2 would just hand you that back as a Python object. So every call had:

  if isinstance(result[0], float):
      return result[0]
  raise CFFDRSException("Failed to calculate X")

The new Python cffdrs library returns Python values directly, so that specific check is no longer needed. But I could add further input guards.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more consistent input guards here: ee36112

I've left the behaviour for: fine_fuel_moisture_code, duff_moisture_code, drought_code, isi, fwi, bui_calc because:

morecast_v2/forecasts.py — fully defensive, guards every call with if yesterday.X is not None, stores results, checks them before downstream use. None propagation
is intentional and relied upon.

critical_hours.py and fwi_adjust.py — call without None checks, pass results directly into the next function. They depend on the return None contract silently
propagating through the chain: ffmc → isi → fwi, dmc/dc → bui → fwi. If any of these raised instead, those callers would crash.

@brettedw
Copy link
Collaborator

brettedw commented Mar 5, 2026

cbh uses a fuel type default if it is None, this was handled internally before in the R library.

Where does this happen? I think cffdrs_py still handles it internally

Comment on lines +484 to +486
calc_step: bool = False,
batch: bool = True,
hourly_fwi: bool = False,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 args aren't used. I actually don't think hourly_fine_fuel_moisture_code is used in our code base. Do we need it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep unused, removed in: f6b0de4

raise CFFDRSException("Failed to calculate CFB")
csi = critical_surface_intensity(fmc, cbh)
rso = surface_fire_rate_of_spread(csi, sfc)
return _crown_fraction_burned(ros, rso)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crown fraction burned is calculated differently for the C6 fuel type. cffdrs_py has a different function for that calculation, wondering if we should take care of that in this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in: 95847c0

return ws


def flank_rate_of_spread(ros: float, bros: float, lb: float):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think functions like this might be unnecessary

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value in these functions is that we encapsulate the underlying library, so it can change and we only need to make changes to this module.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you mean, they are unused, removed here: cf41aa3

@conbrad
Copy link
Collaborator Author

conbrad commented Mar 5, 2026

cbh uses a fuel type default if it is None, this was handled internally before in the R library.

Where does this happen? I think cffdrs_py still handles it internally

backend/packages/wps-api/src/app/routers/fba_calc.py

@brettedw
Copy link
Collaborator

brettedw commented Mar 6, 2026

cbh uses a fuel type default if it is None, this was handled internally before in the R library.

Where does this happen? I think cffdrs_py still handles it internally

backend/packages/wps-api/src/app/routers/fba_calc.py

Took me awhile to figure out what was happening here, I was confused because R doesn't have fuel type default handling in those functions either, but it comes down to Python vs R math operations on NULL/None. Python will error, R won't.

Non-C6 fuel: ROS still comes from RSS (rate_of_spread.r (line 173)), so you often still get a numeric ROS.
C6 fuel: ROS depends on CFB/RSO, so it typically becomes NA (rate_of_spread.r (line 174)).

This is why C6 fuel type has built in 2 and 7m CBH on the front end

Regardless, all the calculations in FireCalc look the same to me

@conbrad conbrad requested review from brettedw and dgboss March 10, 2026 00:35
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CFFDRS API : Get R the F out of our S (Reference ticket)

3 participants