|
| 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 | + |
| 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. |
0 commit comments