Skip to content

Add target_projection and target_pixelsize to ModelSpec and integrate into AWY, UMH - #2545

Open
claire-simpson wants to merge 64 commits into
natcap:feature/projection-pixel-size-updatesfrom
claire-simpson:task/2267-update-projection-handling-for-dh
Open

Add target_projection and target_pixelsize to ModelSpec and integrate into AWY, UMH#2545
claire-simpson wants to merge 64 commits into
natcap:feature/projection-pixel-size-updatesfrom
claire-simpson:task/2267-update-projection-handling-for-dh

Conversation

@claire-simpson

@claire-simpson claire-simpson commented May 8, 2026

Copy link
Copy Markdown
Contributor

Description

Initial PR to add target_projection and target_pixelsize dropdown inputs. First step is testing adding these to AWY and UMH. AWY will also have an optional new AOI input.

There are still some outstanding decisions, namely:

  1. What the default behavior should be for target_projection if the default input is not projected (as we're now moving away from requiring most inputs to be projected)
  2. Whether the new target spatial dropdowns should live behind an Advanced Options toggle, or something similar, to reduce their visibilty
  3. Whether custom pixel sizes (not derived from an input dataset) should be permitted

This PR also addresses some issues in the workbench where we are directly modifying state, and now that the dropdown menus need to be responsive to more changes (e.g., via responsive_to and having target_pixelsize dropdown change in response to target_projection changes), there were more opportunities for things to get out of sync and show stale states or run extra unneeded validation.

First step to address #2267

Checklist

  • Updated HISTORY.rst and link to any relevant issue (if these changes are user-facing)
  • Updated the user's guide (if needed) --> TODO: update UMH and AWY chapters
  • Tested the Workbench UI (if relevant)

…ulate it and validation function in spatial inputs natcap#2267
…s of target projection and get projection of spatial dataset natcap#2267
…t projection string or a filepath with that target projection natcap#2267
…emove validation that default projection must be projected; add functionality to select default pixel size based on dropdown_function; check units when checking projection natcap#2267
…updates' into task/2267-update-projection-handling-for-dh
…; refresh pixelsize options (but not selection) when target projection refreshes; existing selections remain selected as long as still valid but invalid selections fallback to first opt in dropdown; return empty result to get tests passing natcap#2267
@davemfish davemfish added this to the Workbench DH Integration milestone Jul 28, 2026
@claire-simpson
claire-simpson marked this pull request as ready for review July 29, 2026 18:11

@davemfish davemfish left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@claire-simpson I reviewed most of the Python side of this PR and thought that was a good stopping point for now. I didn't look at the python tests yet, or the Workbench side too closely.

I think the design here is really good overall. I had lots of comments about trying to simplify things here and there, and make things a little easier to read and reason about. But this stuff is necessarily somewhat complicated. Thanks for all your hard work on this!


# reproject watersheds_path to target_projection
target_spatial_prj = utils.get_raster_or_vector_projection(
args[args['target_projection']])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is there this nested args[args[...]]? The docstring from execute suggests this should be args['target_projection'].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right and the docstring is incorrect. args['target_projection'] would just be the input id (e.g., lulc_path) instead of the actual filepath we want, which is retrieved via args['lulc_path']. In addition to correcting the docstring, would a comment help make it clear this nesting is intentional?

'fraction', 'cost', 'height', 'kw_price'

args['target_projection'] (string): (optional) if a non-empty string,
path to input that defines the target projection and alignment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be more accurate to say "path to a GDAL raster/vector" rather than "path to input"? That makes it clear that the value is a filepath rather than a projection string of some kind. Maybe naming the key "target_projection_source" would also help make that clear.

I don't remember we ever discuss the possibility of having a wkt string as the value for this arg either. I know we want Workbench users to be able to select a dataset, but python users might like the flexibility of passing a WKT string or similar. That's a larger decision we can discuss though, if we haven't already.

These same suggestions/questions apply to target_pixelsize

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, this docstring is inaccurate. Would it be helpful to rename this arg target_projection_id?

f'"{", ".join(str(x) for x in sorted(missing_ws_ids))}"')

# reproject watersheds_path to target_projection
target_spatial_prj = utils.get_raster_or_vector_projection(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this is a WKT string, it could be nice to name the variable target_projection_wkt or something like that, just because there are so many different objects that could represent a projection. It's nice to quickly see what this one represents.

if pygeoprocessing.get_gis_type(
args[args['target_projection']]) == pygeoprocessing.RASTER_TYPE:
raster_align_index = base_raster_path_list.index(
args[args['target_projection']])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Okay, related to my earlier question, this looks like a clear reason why we want target_projection to be a data source rather than just a projection string 👍 I guess the alternative would require this "align_index_source" to be yet another UI input.

# fallback to aligning everything to the arg with default pixel size
raster_align_index = base_raster_path_list.index(
args[args['target_pixelsize']])
target_pixel_size = utils.get_raster_pixel_size_in_tgt_projection_units(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would slightly prefer get_raster_pixel_size_in_target_proj_units or get_pixel_size_in_target_proj_units. Don't mean to be too nitpicky, but I figure now is the time to do so. I appreciate the name telling me what units will apply to these values though!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would definitely rather nitpick now rather than later when the function potentially more widely used!

Comment thread src/natcap/invest/spec.py
return values

def preprocess_spatial_reference_args(self, args):
"""Set target_projection and target_pixelsize if they aren't in args

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We want people to be able to run the model without these new args, right? The Workbench will always make sure they are included, but python users may not include them, and that's okay. This preprocessor will set the default values in that case. This is called from within MODEL_SPEC.setup which is called in the body of every model's execute function. (I'm just reminding myself how this all works).

Comment thread src/natcap/invest/spec.py Outdated
)
]
if default_projection_inputs:
args['target_projection'] = default_projection_inputs[0].id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doesn't the value of args['target_projection'] need to be the filepath to a spatial dataset? This looks like it would be the id of the Input spec instance instead.

Comment thread src/natcap/invest/spec.py Outdated
all_options = self.get_input(
'target_pixelsize').dropdown_function(args, self)
if all_options:
args['target_pixelsize'] = all_options[0].key

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this "target_pixelsize" input has no value, do we always default to the same one as args['target_projection']? Here it looks like we're just choosing whichever is first in the list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If this "target_pixelsize" input has no value, do we always default to the same one as args['target_projection']?

Yes, the default behavior is to fallback to the same default target projection value (which as you pointed out above, should actually only occur if the target projection value is a raster). In UMH however, there are different default target_pixelsize values specified per model (and defined in a custom dropdown_function: _get_pixelsize_umh).

Here it looks like we're just choosing whichever is first in the list.

Yes, we are choosing whichever the first value is. In _get_pixel_size_options we are ordering the options such that if a default value exists, that will be the first option in the list of options. If no default value exists (which would occur if the default projection is a vector and no custom dropdown_function is specified), this will just be whatever value is first. The other option that I considered was, in absence of a default value, setting a placeholder "select an option", but this seems contradictory to what we've discussed, i.e., avoiding forcing a user to engage with these target projection and pixelsize dropdowns.

What do you think? Should I add a is_default_pixelsize attribute similar to is_default_projection, or keep the current design where is_default_projection also typically defines the default pixel size, and if a different default pixel size is required for a model, that value gets set in a customized version of _get_pixel_size_option? For the majority of models, the same input can serve as the default projection and pixel size, however this is certainly not universally true, and I recognize that the current design lacks clarity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After thinking about this more, I am leaning towards adding a is_default_pixelsize attribute and still supporting the conditional defaults à la UMH. I do think that for the 2 spatial dropdowns we should decide on whether to (a) raise an error if the default value does not pass validation (e.g., because its not projected) as demonstrated for target_projection, or (b) grab the first option in the options list regardless of if its the default. I wrote a comment about this too:

#TODO: or do we want to set [target_projection] just like with target pixel size?
# pros: will find a suitable spatial input if one exists
# cons: less transparent to user?

Comment thread src/natcap/invest/spec.py
args (dict): argument dictionary mapping input keys to input values

Returns:
dictionary mapping input keys to preprocessed input values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like this function should probably make a copy of the args dict first rather than modifying the one passed by the caller. In general it could be surprising to the caller if the object they passed in was modified.

try:
warning_msg = parameter_spec.validate(args[key])
if not warning_msg:
warning_msg = parameter_spec.validate_with_context(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Okay, now I understand the need for validate_with_context method on all Inputs!

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.

2 participants