Deprecate estimate method in favor of class constructor#7771
Deprecate estimate method in favor of class constructor#7771lagru merged 89 commits intoscikit-image:mainfrom
Conversation
7ba0259 to
c163539
Compare
|
This one is ready for a first review - but we need to make some decisions, listed above, so I would be very glad of some initial feedback. |
|
I'll proceed with these (provisional) answers to the points for discussion above:
When I've done the last step, I'll unmark this PR as draft. If anyone disagrees with the above - let me know soon? |
|
That all sounds good to me. I'm fine with |
Could you say more about what you were getting at here? |
|
Nevermind, I had misunderstood what you wrote. I guess the question is: do we return None on failure, or error out? It may be unexpected to get a non-Transform-class-compliant result without obvious failure. |
|
From reading through this, I concur with the general direction. Re failed estimations, if feel like returning Exceptions might work too, but a try except block forces users to check what kind of exception is raised. We could provide that optionally but I wouldn't make it the default. |
|
I like that I have a vague positivity about the current API - in that there is no way that someone could accidentally use a tf = AffineTransform.from_estimate(src, dst, raise_on_failure=True)to: tf = AffineTransform(src, dst):
if tf is None: raise ValueError('Estimation failed') |
|
Matthew, re: the None return: it's not that it's hard to handle, more that it hides failures in a way that may not be obvious to developers. E.g., if you run a program and it works fine the first ten times, but then the 11th time breaks because None doesn't have some attribute, it's going to be confusing. If the error is "we couldn't fit the model", you immediately know what went wrong. Re: naming, I prefer |
|
Re #7771 (comment), I can live happily with Regarding raising on failure, this would be a way to tell users why an estimation failed. Just returning |
I'm happy with raising an error in the class method instead - that was my original implementation. Would you prefer that? |
|
As always with API design, I just have a vague feeling, so I'll have to rely on y'all to help me figure out if the concern is legit or not. |
|
Could we perhaps use a nice sentinel value other than class FailedEstimation:
def __bool__(self):
return Falsethat would still allow a simple |
What would be the advantage over raising an error? |
As stated above, it's easier to handle. |
Ah yes - sorry - I see the point. I guess at the moment, my order of preference is:
but I don't feel very strongly. What do y'all think? |
matthew-brett
left a comment
There was a problem hiding this comment.
Some comments on comments. Nitpickey is fine ...
d474042 to
f038659
Compare
matthew-brett
left a comment
There was a problem hiding this comment.
A couple of comments - thanks.
|
Returning to the API question ... it seems to me that one will often want to do this, as @lagru points out: tf = Transform.from_estimate(src, dst)
if not tf:
"Respond in some way"So far, the None and Otherwise (the try:
tf = Transform.from_estimate(src, dst)
except TransformEstimationError:
"Respond in some way"Of course, we could give the user a choice with: tf = Transform.from_estimate(src, dst, raise=True)where the user then passes on the problem to their user. But this seems like an example where "There should be one-- and preferably only one --obvious way to do it." And we aren't saving the user much compared to: tf = Transform.from_estimate(src, dst)
if not tf:
"Respond in some way"The advantage of Lars' FailedEstimation is that the user can even do: tf = Transform.from_estimate(src, dst)
if not tf:
raise ErrorOfYourChoice(str(tf))So I'm still vaguely in favor of Lars' |
|
As a side-point, and returning to this item:
Lar's |
14f3eab to
5dc6b03
Compare
|
@lagru - you mentioned resolving the stacklevel for the warnings. I wonder whether you could help me with some confusing stacklevel test errors? Example at https://github.com/matthew-brett/scikit-image/actions/runs/14408547193/job/40410880036#step:9:445: This appears to be because I'm using the now-deprecated @classmethod
def from_estimate(cls, data):
instance = cls()
with catch_warnings():
filterwarnings("ignore", message="`estimate` is deprecated")
success = instance.estimate(data)
return instance if success else Nonebut I must admit I didn't understand the stacklevel code in |
|
OK - work now basically done, waiting for a decision on the return value from the |
| def from_estimate(cls, *data): ... | ||
|
|
||
| def residuals(self, *data): ... |
There was a problem hiding this comment.
Does this protocol actually need to support multiple arguments via *data? The test suite only seems to check for the more restrictive case from_estimate(cls, data)?
There was a problem hiding this comment.
I believe that is to match the signature of the ransac function - that allows you to pass multiple arrays as input data. These data then reach the model_class via model = model_class.from_estimate(*samples) (currently L 1153 in fit.py) - where *samples are the data arrays suitably indexed (samples = [d[spl_idxs] for d in data]). Then the data reaches residuals via residuals = np.abs(model.residuals(*data)). In both cases the model has to handle multiple input arrays.
As per @lagru's suggestion.
@lagru's suggestions. Co-authored-by: Lars Grüter <[email protected]>
|
Failures appear unrelated. OK to pass this ball back to you @lagru? |
I hope this makes it a bit more concise and faster to grok. I think especially the `note` directive should help with that.
If we recommend the raise RuntimeError pattern in the geometric example we can also directly recommend it in the documentation. It's not particularly complex. In all cases, it is preceded by "e.g." which makes it clear that this is one way to handle failure.
for readability.
lagru
left a comment
There was a problem hiding this comment.
Finally done. Assuming the CI failures are unrelated, I think we can merge this.
Thanks @matthew-brett for the patience!
|
I added the missing TODO.txt notes and updated the release notes in the PR summary, adding a bit more details and justification. This should be good to go. |
|
🎉 |
* origin/main: Deprecate estimate method in favor of class constructor (scikit-image#7771) Temporary fix for Visual Studio & Clang incompatibility in Windows image (scikit-image#7835) Address deprecations in Pillow 11.3 (scikit-image#7828) Remove unused & obsolete `legacy_datasets`, `legacy_registry` vars (scikit-image#7677) Draft migration guide for skimage2 (scikit-image#7785) Do not report failure in wheels sub-recipe (scikit-image#7806) Use consistent wording in property description. (scikit-image#7804) Add intensity_median to regionprops (scikit-image#7745) CI: Update pypa/gh-action-pypi-publish to v1.12.4 for attestations on PyPI (scikit-image#7793) Document output dtype for transform.resize. (scikit-image#7792) Use `cibuildwheel` to build WASM/Pyodide wheels for `scikit-image`, push nightlies to Anaconda.org (scikit-image#7440) DOC: Include missing gain parameter in adjust_gamma equation (scikit-image#7763) Temporarily pin to `pyodide-build==0.30.0`, and ensure that the correct xbuildenvs are used (scikit-image#7788) Deprecate old names & attributes in RegionProperties (scikit-image#7778) Pin JasonEtco/create-an-issue action to SHA for v2.9.2 (scikit-image#7787) Make doctest-plus work with spin (scikit-image#7786) Report failures on main via issue (scikit-image#7752) Use `workers` instead of alternate parameter names (scikit-image#7302) Fix f-string in otsu plot (scikit-image#7780) Further document use of regionprops function and fix terms. (scikit-image#7518)
* origin/main: (31 commits) Update import convention in certain gallery examples (scikit-image#7764) Refactor fundamental matrix scaling (scikit-image#7767) Add unit test for cval unequal to zero Forward in _generic_edge_filter Remove superfluous mask argument from _generic_edge_filter Only report failure on main branch once Deprecate estimate method in favor of class constructor (scikit-image#7771) Temporary fix for Visual Studio & Clang incompatibility in Windows image (scikit-image#7835) Address deprecations in Pillow 11.3 (scikit-image#7828) Remove unused & obsolete `legacy_datasets`, `legacy_registry` vars (scikit-image#7677) Draft migration guide for skimage2 (scikit-image#7785) Do not report failure in wheels sub-recipe (scikit-image#7806) Use consistent wording in property description. (scikit-image#7804) Add intensity_median to regionprops (scikit-image#7745) CI: Update pypa/gh-action-pypi-publish to v1.12.4 for attestations on PyPI (scikit-image#7793) Document output dtype for transform.resize. (scikit-image#7792) Use `cibuildwheel` to build WASM/Pyodide wheels for `scikit-image`, push nightlies to Anaconda.org (scikit-image#7440) DOC: Include missing gain parameter in adjust_gamma equation (scikit-image#7763) Temporarily pin to `pyodide-build==0.30.0`, and ensure that the correct xbuildenvs are used (scikit-image#7788) Deprecate old names & attributes in RegionProperties (scikit-image#7778) ...
This avoids random failures when the number of max_trials is not enough to successfully estimate a EuclideanTransform. This was also the case previously, but recent updates to our Transform API in scikit-image#7771 made these failures visible in the CI.
This PR follows up on [PR 7771](#7771 (comment)) in continuing the logic of avoiding the creation of uninitialized Model objects, for later completion with `.estimate`. Therefore a) deprecate creation of uninitialized Model objects and b) deprecate use of `params` arguments available in various `predict_*` methods that serve the purpose of initializing uninitialized objects. * Deprecates no-argument constructor calls to the model classes such as `LineModelND`; * Deprecate `.params` attributes of models. Instead set model-specific attributes: `origin, direction` for `LineModelND`; `center, radius` for `CircleModel`, `center, axis_lengths, theta` for `EllipseModel`. * Deprecates use of model-initiating extra parameters (`params`) in `predict*` calls. * Deprecates use of `BaseModel` (it serves no great purpose as a public class, and we will not need it after expiring the other deprecations). We now raise a `ValueError` instead of a `TypeError` when failing to pass a value for `params` (or passing `params=None`) to `predict` methods of an uninitialized transform. ## Release note For maintainers and optionally contributors, please refer to the [instructions](https://scikit-image.org/docs/stable/development/contribute.html#documenting-changes) on how to document this PR for the release notes. ```release-note Deprecate use of model constructor calls without arguments (e.g. `CircleModel()`), leaving an uninitialized instance. Instead prefer input arguments to define instance (e.g. `CircleModel(center, radius)`). This follows on from prior deprecation of the `estimate` method, which had implied the need for the no-argument constructor, of form `cm = CircleMoldel(); cm.estimate(data)`. Deprecate `.params` attributes of models. Instead set model-specific attributes: `origin, direction` for `LineModelND`; `center, radius` for `CircleModel`, `center, ax_lens, theta` for `EllipseModel`. Deprecate use of `params` arguments to `predict*` calls of model objects, because we now ask instead that the user provide initialization equivalent to the `params` content in the class construction (i.e. prefer `cm = CircleModel((2, 3), 4); x = cm.predict_x(t)` to `cm = CircleMoldel(); x = cm.predict_x(t, params=(2, 3, 4))`). Deprecate `skimage.measure.fit.BaseModel`; after we expire the other `*Model*` deprecations, there is no work for an ancestor class to do. In a small cleanup, we now raise a `ValueError` instead of a `TypeError` when failing to pass a value for `params` (or passing `params=None`) to `predict` methods of an uninitialized transform. ```
Description
This is the promised follow-on PR from the discussion more or less starting at #7754 (comment).
To recap briefly:
There is a wart in the current API, in that the
estimatemethod of Transforms ignores any stored state of the initialized Transform, and re-estimates. So, for example:wil discard the initial state with the specified rotations. Another problem is that one generally has to first initialize the transform, most sensibly as an identity transform, and then estimate, thus:
However, the argument to the identity transform is redundant, because we can infer the dimensionality of the points (3) from
srcanddstpoint arrays. So, this PR deprecates theestimatemethod, in favor of a class constructor, like this:For discussion
from_estimategained favor from me (MB) (I proposed it) and @stefanv. @jni leaned towards some fancy footwork to allowestimateto be both the original estimation method, and the class constructor, using the trick here. I thought that was going to be confusing. We can rehearse that argument more.from_estimateAPI returnsNonefor failed estimation. This is generally just as informative as theestimatemethod, except in the case ofPiecewiseAffineTransform, where the estimation involves multiple steps, and resulting affines. Theestimatemethod collects an affine for each step, recording an all-NaN affine for steps that fail estimation, so you can track, from the resultingtform.affinesattribute, where the estimation failed. The returnedNonedoes not give you that granular information. Is there a use for that information? How should it reach the user?LineModelND,CircleModel,EllipseModelat https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/fit.py#L27 to have afrom_estimateclass method (instead of anesimatemethod)?ThinPlateSplineclass (https://github.com/scikit-image/scikit-image/blob/main/skimage/transform/_thin_plate_splines.py#L7)? (I think so).Release note
For maintainers and optionally contributors, please refer to the instructions on how to document this PR for the release notes.