Skip to content

Commit 61f315c

Browse files
authored
Documentation for validation. (#206)
* WIP - Writing up documentation for validation. Primarily focused on dev guide portion for this commit. Will fill out the user guide next. * Filled out the user guide section for automatic validation.
1 parent 55b8b20 commit 61f315c

File tree

3 files changed

+335
-0
lines changed

3 files changed

+335
-0
lines changed

docs/2-validation.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# Validation
2+
3+
## User guide - Automatic validation
4+
Working with ``scarlet2`` provides users significant freedom when working with
5+
``Observation``, ``Source`` and ``Scene`` objects.
6+
In an attempt to provide guidance without restriction, we've implemented
7+
a suite of validation checks that will run automatically at various points in a
8+
``scarlet2`` workflow.
9+
10+
> Note: These validation checks are designed to be efficient.
11+
> If a validation check is performing poorly, please let us know by creating a
12+
> [GitHub issue](https://github.com/pmelchior/scarlet2/issues) explaining what you're
13+
> experiencing.
14+
15+
Currently automatic validation occurs when:
16+
* An instance of the ``Observation`` class is created
17+
* An instance of the ``Source`` class is created
18+
* A user calls ``Scene.fit()``
19+
20+
21+
### Validation results
22+
Every individual validation check will return at least one ``ValidationResult``
23+
object which will one of the following types: Info, Warning, or Error.
24+
All of the results will be printed along with information about the results of
25+
the check.
26+
27+
Below is an example screen shot from a jupyter notebook showing the creation of
28+
a ``Observation`` object. All the checks returned ``ValidationInfo`` results with
29+
the exception of the last, which returned a ``ValidationError``.
30+
31+
![Automatic validation after creating an Observation object.](_static/example_obs_validation.png)
32+
33+
> Note: Validation checks will NEVER cause the program to halt.
34+
> i.e. Even a ``ValidationError`` will not stop the execution of ``scarlet2``.
35+
36+
### Toggling automatic validation checks
37+
Automatic validation checks are enabled by default, and because the results of
38+
validation checks will not halt the program, it is generally fine to leave them
39+
enabled.
40+
41+
However, if you're running ``scarlet2`` in a way that the logged output of
42+
validation checks won't be seen, or you feel that the checks are bothersome, they
43+
can be togged as shown here:
44+
45+
```
46+
from scarlet2.validation_utils import set_validation
47+
48+
# turn off automatic validation checks
49+
set_validation(False)
50+
51+
# turn on automatic validation checks
52+
set_validation(True)
53+
```
54+
55+
> Note: Turning off validation is not persistent. i.e. restarting ``scarlet2``
56+
> will re-enable automatic validation.
57+
58+
### Running validation checks manually
59+
If automatic checks are disabled, or you would like to run validation checks
60+
manually, the following functions are available:
61+
62+
* ``check_fit(scene, observation)``
63+
* ``check_observation(observation)``
64+
* ``check_scene(scene, observation, parameters)``
65+
* ``check_source(source)``
66+
67+
To nicely print the validation results, you can use the `print_validation_results`
68+
function. An example of using ``check_observation`` to run Observation validation
69+
checks together with `print_validation_results` to view the results would look
70+
like this:
71+
72+
```
73+
from scarlet2.validation import check_observation
74+
from scarlet2.validation_utils import print_validation_results
75+
76+
# Assuming the automatic validation is toggled off, we'll create an Observation
77+
observation = Observation(...)
78+
79+
# Run the Observation validation checks
80+
validation_results = check_observation(observation)
81+
82+
# Pretty-print the results
83+
print_validation_results(validation_results)
84+
```
85+
86+
87+
## Dev guide - Implementing validation checks
88+
The goal of the validation checks is to provide guidance for users as they work with ``scarlet2``.
89+
Since guidance changes and the code base evolves, we want to encourage developing
90+
and maintaining validation checks.
91+
92+
Here we include two examples to walk a developer through implementing new validation
93+
checks within the framework that has been established.
94+
95+
96+
### Example - Adding a new validation check
97+
Ideally when modifying existing code or extending API functionality, the developer
98+
will add corresponding validation checks.
99+
100+
Here we'll imagine that a hypothetical method has been added to the ``Observation``
101+
class that will take the square root of a new input parameter ``obs_sqrt``.
102+
The user is free to provide any value for the new input parameter.
103+
We'll write a validation check to provide some guardrails for the user.
104+
105+
#### Write the basic validation check
106+
Validation checks are implemented as methods in a ``*Validation`` class that
107+
is co-located with the class it is validating.
108+
For this example all the validation checks for the ``Observation`` class are
109+
contained in the ``observation.py`` module in a class named
110+
``ObservationValidator``.
111+
112+
Well add the following method to the ``ObservationValidator`` class:
113+
```
114+
import numbers
115+
116+
def check_sqrt_parameter(self) -> ValidationResult:
117+
"""Check that the parameter to be passed to the ``obs_sqrt`` function is
118+
reasonable.
119+
120+
Returns
121+
-------
122+
ValidationResult
123+
An appropriate ValidationResult subclass based on the value of ``obs_sqrt``.
124+
125+
obs_sqrt = self.observation.obs_sqrt
126+
127+
if not isinstance(obs_sqrt, numbers.Number):
128+
return ValidationError(
129+
message="The value of `obs_sqrt` is not numeric",
130+
check=self.__class__.__name__,
131+
context={"obs_sqrt": obs_sqrt}
132+
)
133+
134+
elif(obs_sqrt < 0.):
135+
return ValidationWarning(
136+
message="The value of `obs_sqrt` is < 0. This might be a mistake.",
137+
check=self.__class__.__name__,
138+
context={"obs_sqrt": obs_sqrt}
139+
)
140+
else:
141+
return ValidationInfo(
142+
message="The value of `obs_sqrt` looks good.",
143+
check=self.__class__.__name__,
144+
)
145+
```
146+
147+
When adding a new validation check, it is required to use the following naming scheme:
148+
``check_<something>``.
149+
Failure to do so will mean the check won't be run automatically with the rest of the checks.
150+
151+
A validation check should always return at least one instance of a subclass of ``ValidationResult``.
152+
Returning a list of ``ValidationResults`` subclasses is also ok.
153+
You should not return an instance of the ``ValidationResults`` directly.
154+
The available subclasses are:
155+
* ValidationInfo - for checks passed without any concerns.
156+
* ValidationWarning - for something concerning, but probably won't prevent downstream tasks from completing.
157+
* ValidationError - for what appears to be a problem, and will likely cause failures in later steps.
158+
159+
160+
#### Write unit tests
161+
In our example, new unit tests should be included in ``.../tests/test_observation_checks.py``.
162+
Remember, it's up to the developer to determine when code has sufficient test coverage.
163+
164+
```
165+
def test_obs_sqrt_returns_error(bad_obs):
166+
"""Test that a non-numeric obs_sqrt returns an error."""
167+
obs = Observation(
168+
data=...,
169+
weights=...,
170+
channels=...,
171+
obs_sqrt="scarlet2",
172+
)
173+
174+
checker = ObservationValidator(obs)
175+
176+
results = checker.heck_sqrt_parameter()
177+
178+
assert isinstance(results, ValidationError)
179+
```
180+
181+
Of course, it would make sense to add a few more unit tests to cover the other
182+
possible return and input types, but this should suffice for this example.
183+
184+
At this point the work of adding a new validation check is complete. Nice job!
185+
186+
187+
### Example - Adding a new ``Validator`` class
188+
There may be times when a completely new validator is required.
189+
Here we work through an example of writing a suite of validation checks for a
190+
hypothetical ``scarlet2`` class called ``Thing``.
191+
Fortunately the details of the ``Thing`` class aren't important beyond the
192+
assumption that it is part of ``scarlet2``.
193+
194+
#### Write the new validation class
195+
When writing the new ``Validator`` be sure to:
196+
197+
* [Required] Include the ``ValidationMethodCollector`` metaclass in the class definition.
198+
* [Required] Use the naming scheme for validation checks: ``check_<something>``.
199+
* [Encouraged] Use the validator naming scheme ``*Validator``, in this case ``ThingValidator``.
200+
* [Encouraged] Add the new ``Validator`` in the same module (.py file) as the class it is testing. In this case, in ``thing.py`` after the ``Thing`` class.
201+
202+
A minimal implementation of our new validator would look like this:
203+
```
204+
from .validation_utils import (
205+
ValidationInfo,
206+
ValidationMethodCollector,
207+
ValidationWarning
208+
)
209+
210+
class ThingValidator(metaclass=ValidationMethodCollector):
211+
"""Doc string describing the purpose of `ThingValidator`."""
212+
213+
def __init__(self, thing: Thing):
214+
self.thing = thing
215+
216+
# An example check of self.thing's `parameter`.
217+
def check_thing(self) -> ValidationResult:
218+
"""Doc string explaining the check.
219+
220+
Returns
221+
-------
222+
ValidationResult
223+
ValidationResult object with info about the check.
224+
"""
225+
all_good = self.thing.parameter == True
226+
if all_good:
227+
return ValidationInfo(
228+
message="All is good",
229+
check=self.__class__.__name__,
230+
)
231+
else:
232+
return ValidationWarning(
233+
message="Things might not be good",
234+
check=self.__class__.__name__.
235+
context={"all_good": all_good}
236+
)
237+
```
238+
239+
#### Write the function to run the tests
240+
To allow users to run the validation checks you'll implement a ``check_thing``
241+
function in ``.../src/scarlet2/validation.py``.
242+
For real examples see the ``check_*`` functions here: [GitHub link](https://github.com/pmelchior/scarlet2/blob/main/src/scarlet2/validation.py).
243+
244+
For our example the function would look like this:
245+
```
246+
def check_thing(thing) -> list[ValidationResults]:
247+
"""Check the ``thing`` with the various validation checks.
248+
249+
Parameters
250+
----------
251+
thing : Thing
252+
The ``Thing`` instance to check.
253+
254+
Returns
255+
-------
256+
list[ValidationResults]
257+
A list of ``ValidationResults`` from the execution of the checks in
258+
``ThingValidator``.
259+
"""
260+
261+
return _check(validation_class=ThingValidator, **{"thing_to_check": thing })
262+
```
263+
264+
> Note: If your validator requires more than one input, you'll need to pass those
265+
> in here. Follow the GitHub link above and look at the ``check_fit`` function as
266+
> an example.
267+
268+
#### Run the validation checks automatically
269+
The following code should be included where appropriate, depending on when the
270+
``Validator`` should run. For example, if the the checks should run after
271+
initializing an object, include the snippet at the end of the ``__init__`` method.
272+
273+
Given that the user has not turned off automatic validation checks, the following
274+
code would execute all the ``Thing`` validation checks and print out the results..
275+
276+
```
277+
# (re)-import `VALIDATION_SWITCH` at runtime to avoid using a static/old value
278+
from .validation_utils import VALIDATION_SWITCH
279+
280+
if VALIDATION_SWITCH:
281+
# This import happens here to avoid circular dependencies
282+
from .validation import check_observation
283+
284+
validation_results = check_thing(self)
285+
print_validation_results("Observation validation results", validation_results)
286+
```
287+
288+
#### Create a new test suite
289+
Finally be sure to add a test suite for the new ``ThingValidator``.
290+
It's best to add the new file in ``.../tests/scarlet2/test_thing_checks.py`` so
291+
that it will be automatically detected as part of the continuous integration pipelines.
292+
293+
Our example test suite might look something like the following:
294+
295+
```
296+
import pytest
297+
from scarlet2.thing import Thing, ThingValidator
298+
from scarlet2.validation_utils import (
299+
ValidationInfo,
300+
ValidationWarning,
301+
set_validation
302+
)
303+
304+
@pytest.fixture(autouse=True)
305+
def setup_validation()
306+
# Turn off auto-validation
307+
set_validation(False)
308+
309+
def test_check_thing():
310+
# Create an instance of a Thing
311+
thing = Thing(parameter=True)
312+
313+
checker = ThingValidator(thing)
314+
315+
results = checker.check_thing()
316+
317+
assert isinstance(results, ValidationInfo)
318+
319+
def test_check_thing_warning();
320+
# Create an instance of a Thing
321+
thing = Thing(parameter=False)
322+
323+
checker = ThingValidator(thing)
324+
325+
results = checker.check_thing()
326+
327+
assert isinstance(results, ValidationWarning)
328+
```
329+
330+
With the test suite in place, we now have confidence that the logic in our checks
331+
is behaving as expected. Given that we've followed the typical naming scheme for
332+
tests and put this test suite in the correct directory, it should be discovered
333+
automatically and included as part of the continuous integration tests with every
334+
future commit.
220 KB
Loading

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ optimizer/sampler. If you want a fully fledged library out of the box, you need
3333
3434
0-quickstart
3535
1-howto
36+
2-validation
3637
api
3738
```
3839

0 commit comments

Comments
 (0)