diff --git a/.copier-answers.yml b/.copier-answers.yml
new file mode 100644
index 0000000..ea2046b
--- /dev/null
+++ b/.copier-answers.yml
@@ -0,0 +1,16 @@
+# Changes here will be overwritten by Copier
+_commit: 2.6.0
+_src_path: gh:DiamondLightSource/python-copier-template
+author_email: luke.fiddy@diamond.ac.uk
+author_name: Luke Fiddy
+description: package to calibrate bimorph mirrors to focus a beamline to a target
+ position
+distribution_name: bimorph-mirror-analysis
+docker: false
+docs_type: README
+git_platform: github.com
+github_org: ''
+package_name: bimorph_mirror_analysis
+pypi: false
+repo_name: bimorph-mirror-analysis
+type_checker: pyright
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..979a89c
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,46 @@
+// For format details, see https://containers.dev/implementors/json_reference/
+{
+ "name": "Python 3 Developer Container",
+ "build": {
+ "dockerfile": "../Dockerfile",
+ "target": "developer"
+ },
+ "remoteEnv": {
+ // Allow X11 apps to run inside the container
+ "DISPLAY": "${localEnv:DISPLAY}"
+ },
+ "customizations": {
+ "vscode": {
+ // Set *default* container specific settings.json values on container create.
+ "settings": {
+ "python.defaultInterpreterPath": "/venv/bin/python"
+ },
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": [
+ "ms-python.python",
+ "github.vscode-github-actions",
+ "tamasfe.even-better-toml",
+ "redhat.vscode-yaml",
+ "ryanluker.vscode-coverage-gutters",
+ "charliermarsh.ruff",
+ "ms-azuretools.vscode-docker"
+ ]
+ }
+ },
+ "features": {
+ // add in eternal history and other bash features
+ "ghcr.io/diamondlightsource/devcontainer-features/bash-config:1.0.0": {}
+ },
+ // Create the config folder for the bash-config feature
+ "initializeCommand": "mkdir -p ${localEnv:HOME}/.config/bash-config",
+ "runArgs": [
+ // Allow the container to access the host X11 display and EPICS CA
+ "--net=host",
+ // Make sure SELinux does not disable with access to host filesystems like tmp
+ "--security-opt=label=disable"
+ ],
+ // Mount the parent as /workspaces so we can pip install peers as editable
+ "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",
+ // After the container is created, install the python project in editable form
+ "postCreateCommand": "pip install $([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e '.[dev]' && pre-commit install"
+}
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..f4c6889
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# Contribute to the project
+
+Contributions and issues are most welcome! All issues and pull requests are
+handled through [GitHub](https://github.com//bimorph-mirror-analysis/issues). Also, please check for any existing issues before
+filing a new one. If you have a great idea but it involves big changes, please
+file a ticket before making a pull request! We want to make sure you don't spend
+your time coding something that might not fit the scope of the project.
+
+## Issue or Discussion?
+
+Github also offers [discussions](https://github.com//bimorph-mirror-analysis/discussions) as a place to ask questions and share ideas. If
+your issue is open ended and it is not obvious when it can be "closed", please
+raise it as a discussion instead.
+
+## Code Coverage
+
+While 100% code coverage does not make a library bug-free, it significantly
+reduces the number of easily caught bugs! Please make sure coverage remains the
+same or is improved by a pull request!
+
+## Developer Information
+
+It is recommended that developers use a [vscode devcontainer](https://code.visualstudio.com/docs/devcontainers/containers). This repository contains configuration to set up a containerized development environment that suits its own needs.
+
+This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects.
+
+For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/2.6.0/how-to.html).
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..aa65892
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,21 @@
+---
+name: Bug Report
+about: The template to use for reporting bugs and usability issues
+title: " "
+labels: 'bug'
+assignees: ''
+
+---
+
+Describe the bug, including a clear and concise description of the expected behavior, the actual behavior and the context in which you encountered it (ideally include details of your environment).
+
+## Steps To Reproduce
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+
+## Acceptance Criteria
+- Specific criteria that will be used to judge if the issue is fixed
diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md
new file mode 100644
index 0000000..52c84dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue.md
@@ -0,0 +1,13 @@
+---
+name: Issue
+about: The standard template to use for feature requests, design discussions and tasks
+title: " "
+labels: ''
+assignees: ''
+
+---
+
+A brief description of the issue, including specific stakeholders and the business case where appropriate
+
+## Acceptance Criteria
+- Specific criteria that will be used to judge if the issue is fixed
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 0000000..8200afe
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,8 @@
+Fixes #ISSUE
+
+### Instructions to reviewer on how to test:
+1. Do thing x
+2. Confirm thing y happens
+
+### Checks for reviewer
+- [ ] Would the PR title make sense to a user on a set of release notes
diff --git a/.github/actions/install_requirements/action.yml b/.github/actions/install_requirements/action.yml
new file mode 100644
index 0000000..d33e080
--- /dev/null
+++ b/.github/actions/install_requirements/action.yml
@@ -0,0 +1,34 @@
+name: Install requirements
+description: Install a version of python then call pip install and report what was installed
+inputs:
+ python-version:
+ description: Python version to install, default is from Dockerfile
+ default: "dev"
+ pip-install:
+ description: Parameters to pass to pip install
+ default: "$([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e .[dev]"
+
+runs:
+ using: composite
+ steps:
+ - name: Get version of python
+ run: |
+ PYTHON_VERSION="${{ inputs.python-version }}"
+ if [ $PYTHON_VERSION == "dev" ]; then
+ PYTHON_VERSION=$(sed -n "s/ARG PYTHON_VERSION=//p" Dockerfile)
+ fi
+ echo "PYTHON_VERSION=$PYTHON_VERSION" >> "$GITHUB_ENV"
+ shell: bash
+
+ - name: Setup python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Install packages
+ run: pip install ${{ inputs.pip-install }}
+ shell: bash
+
+ - name: Report what was installed
+ run: pip freeze
+ shell: bash
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..184ba36
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ actions:
+ patterns:
+ - "*"
+
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ dev-dependencies:
+ patterns:
+ - "*"
diff --git a/.github/pages/index.html b/.github/pages/index.html
new file mode 100644
index 0000000..c495f39
--- /dev/null
+++ b/.github/pages/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Redirecting to main branch
+
+
+
+
+
+
diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py
new file mode 100755
index 0000000..c06813a
--- /dev/null
+++ b/.github/pages/make_switcher.py
@@ -0,0 +1,96 @@
+"""Make switcher.json to allow docs to switch between different versions."""
+
+import json
+import logging
+from argparse import ArgumentParser
+from pathlib import Path
+from subprocess import CalledProcessError, check_output
+
+
+def report_output(stdout: bytes, label: str) -> list[str]:
+ """Print and return something received frm stdout."""
+ ret = stdout.decode().strip().split("\n")
+ print(f"{label}: {ret}")
+ return ret
+
+
+def get_branch_contents(ref: str) -> list[str]:
+ """Get the list of directories in a branch."""
+ stdout = check_output(["git", "ls-tree", "-d", "--name-only", ref])
+ return report_output(stdout, "Branch contents")
+
+
+def get_sorted_tags_list() -> list[str]:
+ """Get a list of sorted tags in descending order from the repository."""
+ stdout = check_output(["git", "tag", "-l", "--sort=-v:refname"])
+ return report_output(stdout, "Tags list")
+
+
+def get_versions(ref: str, add: str | None) -> list[str]:
+ """Generate the file containing the list of all GitHub Pages builds."""
+ # Get the directories (i.e. builds) from the GitHub Pages branch
+ try:
+ builds = set(get_branch_contents(ref))
+ except CalledProcessError:
+ builds = set()
+ logging.warning(f"Cannot get {ref} contents")
+
+ # Add and remove from the list of builds
+ if add:
+ builds.add(add)
+
+ # Get a sorted list of tags
+ tags = get_sorted_tags_list()
+
+ # Make the sorted versions list from main branches and tags
+ versions: list[str] = []
+ for version in ["master", "main"] + tags:
+ if version in builds:
+ versions.append(version)
+ builds.remove(version)
+
+ # Add in anything that is left to the bottom
+ versions += sorted(builds)
+ print(f"Sorted versions: {versions}")
+ return versions
+
+
+def write_json(path: Path, repository: str, versions: list[str]):
+ """Write the JSON switcher to path."""
+ org, repo_name = repository.split("/")
+ struct = [
+ {"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"}
+ for version in versions
+ ]
+ text = json.dumps(struct, indent=2)
+ print(f"JSON switcher:\n{text}")
+ path.write_text(text, encoding="utf-8")
+
+
+def main(args=None):
+ """Parse args and write switcher."""
+ parser = ArgumentParser(
+ description="Make a versions.json file from gh-pages directories"
+ )
+ parser.add_argument(
+ "--add",
+ help="Add this directory to the list of existing directories",
+ )
+ parser.add_argument(
+ "repository",
+ help="The GitHub org and repository name: ORG/REPO",
+ )
+ parser.add_argument(
+ "output",
+ type=Path,
+ help="Path of write switcher.json to",
+ )
+ args = parser.parse_args(args)
+
+ # Write the versions file
+ versions = get_versions("origin/gh-pages", args.add)
+ write_json(args.output, args.repository, versions)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/_check.yml b/.github/workflows/_check.yml
new file mode 100644
index 0000000..a6139c1
--- /dev/null
+++ b/.github/workflows/_check.yml
@@ -0,0 +1,27 @@
+on:
+ workflow_call:
+ outputs:
+ branch-pr:
+ description: The PR number if the branch is in one
+ value: ${{ jobs.pr.outputs.branch-pr }}
+
+jobs:
+ pr:
+ runs-on: "ubuntu-latest"
+ outputs:
+ branch-pr: ${{ steps.script.outputs.result }}
+ steps:
+ - uses: actions/github-script@v7
+ id: script
+ if: github.event_name == 'push'
+ with:
+ script: |
+ const prs = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ head: context.repo.owner + ':${{ github.ref_name }}'
+ })
+ if (prs.data.length) {
+ console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`)
+ return prs.data[0]["number"]
+ }
diff --git a/.github/workflows/_dist.yml b/.github/workflows/_dist.yml
new file mode 100644
index 0000000..b1c4c93
--- /dev/null
+++ b/.github/workflows/_dist.yml
@@ -0,0 +1,36 @@
+on:
+ workflow_call:
+
+jobs:
+ build:
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # Need this to get version number from last tag
+ fetch-depth: 0
+
+ - name: Build sdist and wheel
+ run: >
+ export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) &&
+ pipx run build
+
+ - name: Upload sdist and wheel as artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist
+
+ - name: Check for packaging errors
+ run: pipx run twine check --strict dist/*
+
+ - name: Install produced wheel
+ uses: ./.github/actions/install_requirements
+ with:
+ pip-install: dist/*.whl
+
+ - name: Test module --version works using the installed wheel
+ # If more than one module in src/ replace with module name to test
+ run: python -m $(ls --hide='*.egg-info' src | head -1) --version
diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml
new file mode 100644
index 0000000..81b6264
--- /dev/null
+++ b/.github/workflows/_release.yml
@@ -0,0 +1,32 @@
+on:
+ workflow_call:
+
+jobs:
+ artifacts:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ merge-multiple: true
+
+ - name: Zip up docs
+ run: |
+ set -vxeuo pipefail
+ if [ -d html ]; then
+ mv html $GITHUB_REF_NAME
+ zip -r docs.zip $GITHUB_REF_NAME
+ rm -rf $GITHUB_REF_NAME
+ fi
+
+ - name: Create GitHub Release
+ # We pin to the SHA, not the tag, for security reasons.
+ # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
+ uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
+ with:
+ prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
+ files: "*"
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml
new file mode 100644
index 0000000..f652d41
--- /dev/null
+++ b/.github/workflows/_test.yml
@@ -0,0 +1,62 @@
+on:
+ workflow_call:
+ inputs:
+ python-version:
+ type: string
+ description: The version of python to install
+ required: true
+ runs-on:
+ type: string
+ description: The runner to run this job on
+ required: true
+ secrets:
+ CODECOV_TOKEN:
+ required: true
+
+env:
+ # https://github.com/pytest-dev/pytest/issues/2042
+ PY_IGNORE_IMPORTMISMATCH: "1"
+
+jobs:
+ run:
+ runs-on: ${{ inputs.runs-on }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # Need this to get version number from last tag
+ fetch-depth: 0
+
+ - if: inputs.python-version == 'dev'
+ name: Install dev versions of python packages
+ uses: ./.github/actions/install_requirements
+
+ - if: inputs.python-version == 'dev'
+ name: Write the requirements as an artifact
+ run: pip freeze --exclude-editable > /tmp/dev-requirements.txt
+
+ - if: inputs.python-version == 'dev'
+ name: Upload dev-requirements.txt
+ uses: actions/upload-artifact@v4
+ with:
+ name: dev-requirements
+ path: /tmp/dev-requirements.txt
+
+ - if: inputs.python-version != 'dev'
+ name: Install latest versions of python packages
+ uses: ./.github/actions/install_requirements
+ with:
+ python-version: ${{ inputs.python-version }}
+ pip-install: ".[dev]"
+
+ - name: Run tests
+ run: tox -e tests
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ name: ${{ inputs.python-version }}/${{ inputs.runs-on }}
+ files: cov.xml
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/_tox.yml b/.github/workflows/_tox.yml
new file mode 100644
index 0000000..a13536d
--- /dev/null
+++ b/.github/workflows/_tox.yml
@@ -0,0 +1,22 @@
+on:
+ workflow_call:
+ inputs:
+ tox:
+ type: string
+ description: What to run under tox
+ required: true
+
+
+jobs:
+ run:
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install python packages
+ uses: ./.github/actions/install_requirements
+
+ - name: Run tox
+ run: tox -e ${{ inputs.tox }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..24198c7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ check:
+ uses: ./.github/workflows/_check.yml
+
+ lint:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ uses: ./.github/workflows/_tox.yml
+ with:
+ tox: pre-commit,type-checking
+
+ test:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ strategy:
+ matrix:
+ runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest
+ python-version: ["3.10", "3.11", "3.12"]
+ include:
+ # Include one that runs in the dev environment
+ - runs-on: "ubuntu-latest"
+ python-version: "dev"
+ fail-fast: false
+ uses: ./.github/workflows/_test.yml
+ with:
+ runs-on: ${{ matrix.runs-on }}
+ python-version: ${{ matrix.python-version }}
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ dist:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ uses: ./.github/workflows/_dist.yml
+
+ release:
+ if: github.ref_type == 'tag'
+ needs: [dist]
+ uses: ./.github/workflows/_release.yml
+ permissions:
+ contents: write
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0f33bf2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,71 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+**/_version.py
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+cov.xml
+.pytest_cache/
+.mypy_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+docs/_api
+
+# PyBuilder
+target/
+
+# likely venv names
+.venv*
+venv*
+
+# further build artifacts
+lockfiles/
+
+# ruff cache
+.ruff_cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..60fc23f
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,24 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-yaml
+ - id: check-merge-conflict
+ - id: end-of-file-fixer
+
+ - repo: local
+ hooks:
+ - id: ruff
+ name: lint with ruff
+ language: system
+ entry: ruff check --force-exclude
+ types: [python]
+ require_serial: true
+
+ - id: ruff-format
+ name: format with ruff
+ language: system
+ entry: ruff format --force-exclude
+ types: [python]
+ require_serial: true
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..933c580
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-vscode-remote.remote-containers",
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..36d8f50
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,23 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug Unit Test",
+ "type": "debugpy",
+ "request": "launch",
+ "justMyCode": false,
+ "program": "${file}",
+ "purpose": [
+ "debug-test"
+ ],
+ "console": "integratedTerminal",
+ "env": {
+ // Enable break on exception when debugging tests (see: tests/conftest.py)
+ "PYTEST_RAISE": "1",
+ },
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..101c75f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "editor.formatOnSave": true,
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": "explicit"
+ },
+ "files.insertFinalNewline": true,
+ "[python]": {
+ "editor.defaultFormatter": "charliermarsh.ruff",
+ },
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..c999e86
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,16 @@
+// See https://go.microsoft.com/fwlink/?LinkId=733558
+// for the documentation about the tasks.json format
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "label": "Tests, lint and docs",
+ "command": "tox -p",
+ "options": {
+ "cwd": "${workspaceRoot}"
+ },
+ "problemMatcher": [],
+ }
+ ]
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..35d2abf
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+# The devcontainer should use the developer target and run as root with podman
+# or docker with user namespaces.
+ARG PYTHON_VERSION=3.11
+FROM python:${PYTHON_VERSION} AS developer
+
+# Add any system dependencies for the developer/build environment here
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ graphviz \
+ && rm -rf /var/lib/apt/lists/*
+
+# Set up a virtual environment and put it in PATH
+RUN python -m venv /venv
+ENV PATH=/venv/bin:$PATH
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5c5bfe0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+[![CI](https://github.com//bimorph-mirror-analysis/actions/workflows/ci.yml/badge.svg)](https://github.com//bimorph-mirror-analysis/actions/workflows/ci.yml)
+[![Coverage](https://codecov.io/gh//bimorph-mirror-analysis/branch/main/graph/badge.svg)](https://codecov.io/gh//bimorph-mirror-analysis)
+
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
+
+# bimorph_mirror_analysis
+
+package to calibrate bimorph mirrors to focus a beamline to a target position
+
+This is where you should write a short paragraph that describes what your module does,
+how it does it, and why people should use it.
+
+Source |
+:---: | :---:
+Releases |
+
+This is where you should put some images or code snippets that illustrate
+some relevant examples. If it is a library then you might put some
+introductory code here:
+
+```python
+from bimorph_mirror_analysis import __version__
+
+print(f"Hello bimorph_mirror_analysis {__version__}")
+```
+
+Or if it is a commandline tool then you might put some example commands here:
+
+```
+python -m bimorph_mirror_analysis --version
+```
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..10f97b9
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,108 @@
+[build-system]
+requires = ["setuptools>=64", "setuptools_scm[toml]>=8"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "bimorph-mirror-analysis"
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+]
+description = "package to calibrate bimorph mirrors to focus a beamline to a target position"
+dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"]
+dynamic = ["version"]
+license.file = "LICENSE"
+readme = "README.md"
+requires-python = ">=3.10"
+
+[project.optional-dependencies]
+dev = [
+ "copier",
+ "pipdeptree",
+ "pre-commit",
+ "pyright",
+ "pytest",
+ "pytest-cov",
+ "ruff",
+ "tox-direct",
+ "types-mock",
+]
+
+[project.scripts]
+bimorph-mirror-analysis = "bimorph_mirror_analysis.__main__:main"
+
+[project.urls]
+GitHub = "https://github.com//bimorph-mirror-analysis"
+
+[[project.authors]] # Further authors may be added by duplicating this section
+email = "luke.fiddy@diamond.ac.uk"
+name = "Luke Fiddy"
+
+
+[tool.setuptools_scm]
+version_file = "src/bimorph_mirror_analysis/_version.py"
+
+[tool.pyright]
+typeCheckingMode = "standard"
+reportMissingImports = false # Ignore missing stubs in imported modules
+
+[tool.pytest.ini_options]
+# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
+addopts = """
+ --tb=native -vv
+ """
+# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings
+filterwarnings = "error"
+# Doctest python code in docs, python code in src docstrings, test functions in tests
+testpaths = "docs src tests"
+
+[tool.coverage.run]
+data_file = "/tmp/bimorph_mirror_analysis.coverage"
+
+[tool.coverage.paths]
+# Tests are run from installed location, map back to the src directory
+source = ["src", "**/site-packages/"]
+
+# tox must currently be configured via an embedded ini string
+# See: https://github.com/tox-dev/tox/issues/999
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+skipsdist=True
+
+[testenv:{pre-commit,type-checking,tests}]
+# Don't create a virtualenv for the command, requires tox-direct plugin
+direct = True
+passenv = *
+allowlist_externals =
+ pytest
+ pre-commit
+ pyright
+commands =
+ pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs}
+ type-checking: pyright src tests {posargs}
+ tests: pytest --cov=bimorph_mirror_analysis --cov-report term --cov-report xml:cov.xml {posargs}
+"""
+
+[tool.ruff]
+src = ["src", "tests"]
+line-length = 88
+lint.select = [
+ "B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
+ "C4", # flake8-comprehensions - https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
+ "E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e
+ "F", # pyflakes rules - https://docs.astral.sh/ruff/rules/#pyflakes-f
+ "W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w
+ "I", # isort - https://docs.astral.sh/ruff/rules/#isort-i
+ "UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up
+ "SLF", # self - https://docs.astral.sh/ruff/settings/#lintflake8-self
+]
+
+[tool.ruff.lint.per-file-ignores]
+# By default, private member access is allowed in tests
+# See https://github.com/DiamondLightSource/python-copier-template/issues/154
+# Remove this line to forbid private member access in tests
+"tests/**/*" = ["SLF001"]
diff --git a/src/bimorph_mirror_analysis/__init__.py b/src/bimorph_mirror_analysis/__init__.py
new file mode 100644
index 0000000..a2ffbf3
--- /dev/null
+++ b/src/bimorph_mirror_analysis/__init__.py
@@ -0,0 +1,11 @@
+"""Top level API.
+
+.. data:: __version__
+ :type: str
+
+ Version number as calculated by https://github.com/pypa/setuptools_scm
+"""
+
+from ._version import __version__
+
+__all__ = ["__version__"]
diff --git a/src/bimorph_mirror_analysis/__main__.py b/src/bimorph_mirror_analysis/__main__.py
new file mode 100644
index 0000000..55c4d6e
--- /dev/null
+++ b/src/bimorph_mirror_analysis/__main__.py
@@ -0,0 +1,24 @@
+"""Interface for ``python -m bimorph_mirror_analysis``."""
+
+from argparse import ArgumentParser
+from collections.abc import Sequence
+
+from . import __version__
+
+__all__ = ["main"]
+
+
+def main(args: Sequence[str] | None = None) -> None:
+ """Argument parser for the CLI."""
+ parser = ArgumentParser()
+ parser.add_argument(
+ "-v",
+ "--version",
+ action="version",
+ version=__version__,
+ )
+ parser.parse_args(args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..ebe9c10
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,21 @@
+import os
+from typing import Any
+
+import pytest
+
+# Prevent pytest from catching exceptions when debugging in vscode so that break on
+# exception works correctly (see: https://github.com/pytest-dev/pytest/issues/7409)
+if os.getenv("PYTEST_RAISE", "0") == "1":
+
+ @pytest.hookimpl(tryfirst=True)
+ def pytest_exception_interact(call: pytest.CallInfo[Any]):
+ if call.excinfo is not None:
+ raise call.excinfo.value
+ else:
+ raise RuntimeError(
+ f"{call} has no exception data, an unknown error has occurred"
+ )
+
+ @pytest.hookimpl(tryfirst=True)
+ def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]):
+ raise excinfo.value
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..2e8ebc1
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,9 @@
+import subprocess
+import sys
+
+from bimorph_mirror_analysis import __version__
+
+
+def test_cli_version():
+ cmd = [sys.executable, "-m", "bimorph_mirror_analysis", "--version"]
+ assert subprocess.check_output(cmd).decode().strip() == __version__