# Sample image width against focus position

The complete source is included below.

```powershell
python -m nextastro_obs_client.examples.focus_curve --ra 12.5 --dec -30 --target "Focus field" --filter V --start 12000 --step 50 --samples 7 --seconds 2 --star 512 512
```

Sweeps explicit device steps, writes focus_curve.csv and requests a return to --start. It does not assume --start was the pre-script position. The star, aperture and sky annulus must be chosen for the real image. The diagnostic width is a positive-flux second-moment Gaussian-equivalent FWHM, not OBS's fitted autofocus result; defocus, background, crowding and a truncated aperture affect it. A failed return does not hide the original exception.

Student extension: Compare with obs.focuser.autofocus(), fit a focus curve and inspect the star thumbnails.


## Python source

```python
"""Sample absolute focus steps and measure an explicitly selected star."""
from nextastro_obs_client.examples._common import parser, target_arguments, point, execute, write_rows, return_after


def run(obs, camera, args):
    point(obs, args)
    rows = []
    # Return to the student's explicit sweep start, not a guessed best focus.
    with return_after(lambda: obs.focuser.move_steps(args.start)):
        for index in range(args.samples):
            position = args.start + index * args.step
            obs.focuser.move_steps(position)
            image = camera.expose(seconds=args.seconds, filter=args.filter, target_name=args.target)[0]
            source = image.measure_source(x=args.star[0], y=args.star[1], radius=args.radius,
                                         sky_inner=args.sky_inner, sky_outer=args.sky_outer)
            rows.append(dict(position_steps=position, moment_fwhm_pixels=source.moment_fwhm_pixels,
                             centroid_x=source.centroid_x, centroid_y=source.centroid_y, path=str(image.path)))
            write_rows(args.output, rows)
    return rows


def main(argv=None):
    p = parser("Focus curve")
    target_arguments(p)
    p.add_argument("--start", type=int, required=True, help="Sweep start and return position in device steps")
    p.add_argument("--step", type=int, required=True)
    p.add_argument("--samples", type=int, required=True)
    p.add_argument("--seconds", type=float, required=True)
    p.add_argument("--star", type=float, nargs=2, required=True)
    p.add_argument("--radius", type=float, default=10)
    p.add_argument("--sky-inner", type=float, default=14)
    p.add_argument("--sky-outer", type=float, default=20)
    p.add_argument("--output", default="focus_curve.csv")
    return execute(p, run, argv)


if __name__ == "__main__":
    main()
```

Shared argument and session helpers: [Example support code](support.md).
