diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..c41479b --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + target: auto + threshold: 0% + patch: + default: + target: auto + threshold: 0% diff --git a/.copier-answers.yml b/.copier-answers.yml index 800b494..0512741 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,7 +1,8 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: v0.12.1 +_commit: v0.16.0 _src_path: https://github.com/mbercx/python-copier -description: Tools for running and parsing Quantum ESPRESSO calculations +coverage: codecov doc_deploy: rtd docs: mkdocs package_name: qe-tools +type_check: loose diff --git a/.github/workflows/.ci.yml b/.github/workflows/.ci.yml deleted file mode 100644 index afad501..0000000 --- a/.github/workflows/.ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: ci - -on: - push: - branches: - - main - pull_request: - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install Hatch - run: | - pip install hatch - hatch env prune - - - name: Run pre-commit - run: hatch run pre-commit:run - - tests: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.10', '3.14'] - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - run: pip install hatch - - run: hatch test diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..dddcb46 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,48 @@ +name: cd + +on: + push: + tags: + - 'v[0-9]*.[0-9]*.[0-9]*' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install Hatch + run: pip install hatch + + - name: Build sdist and wheel + run: hatch build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..6cfe438 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,89 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install Hatch + run: pip install hatch + + - name: Run pre-commit + run: hatch run pre-commit:run --all-files + + commit-msgs: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Check commit message format + run: | + base="${{ github.event.pull_request.base.sha }}" + head="${{ github.event.pull_request.head.sha }}" + fail=0 + while IFS= read -r subject; do + msg=$(mktemp) + printf '%s\n' "$subject" > "$msg" + if ! python dev/check_commit_msg.py "$msg"; then + fail=1 + fi + rm -f "$msg" + done < <(git log --format=%s "$base..$head") + exit $fail + + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.14'] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install hatch + - run: hatch test --cover + + - name: Install coverage + if: '!cancelled()' + run: pip install coverage[toml] + + - name: Coverage summary + if: '!cancelled()' + run: | + echo '## Test Coverage' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + coverage report >> $GITHUB_STEP_SUMMARY 2>&1 || true + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: Generate coverage XML + if: '!cancelled()' + run: coverage xml || true + + - name: Upload coverage to Codecov + if: '!cancelled()' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + fail_ci_if_error: false diff --git a/.github/workflows/copier-update.yaml b/.github/workflows/copier-update.yaml new file mode 100644 index 0000000..8cf1fcd --- /dev/null +++ b/.github/workflows/copier-update.yaml @@ -0,0 +1,80 @@ +name: Copier Update + +on: + schedule: + - cron: '0 5 * * 1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + copier-update: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install copier + run: pip install copier + + - name: Update from template + # --trust is required for non-interactive mode; it allows execution of + # any _tasks defined by the upstream template. + run: copier update --defaults --force --trust + + - name: Check for changes + id: check + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Check for existing PR + if: ${{ steps.check.outputs.changed == 'true' }} + + id: existing-pr + env: + GH_TOKEN: ${{ github.token }} + run: | + EXISTING=$(gh pr list --state open --json headRefName,number --jq '[.[] | select(.headRefName | startswith("copier-update/"))][0].number') + if [ -n "$EXISTING" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "::notice::Update PR #$EXISTING already exists, skipping." + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Create PR + if: ${{ steps.check.outputs.changed == 'true' && steps.existing-pr.outputs.exists == 'false' }} + + env: + GH_TOKEN: ${{ github.token }} + run: | + BRANCH="copier-update/${{ github.run_id }}" + git config user.name "copier-update[bot]" + git config user.email "copier-update[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add . + git commit -m "Update from copier template" + git push origin "$BRANCH" + gh pr create \ + --title "Update from copier template" \ + --body "Automated update from the upstream copier template. + + This PR was created by the \`copier-update\` workflow. + Please review the changes carefully before merging. + + > If there are conflicts or unexpected changes, you can run + > \`copier update\` locally to resolve them interactively." diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c660600..d42e50d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,6 @@ -repos: - - repo: local - hooks: - - id: mypy - name: Type-check with mypy - entry: mypy - language: python - types: [python] - require_serial: true - files: ^src/qe_tools/outputs/ +default_install_hook_types: [pre-commit, commit-msg] +repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.2 hooks: @@ -19,3 +11,22 @@ repos: - id: ruff-format name: Format with Ruff types_or: [ python, pyi ] + + - repo: local + hooks: + - id: mypy + name: Type-check with mypy + entry: mypy + language: system + types: [ python ] + require_serial: true + files: ^src/ + + - repo: local + hooks: + - id: check-commit-msg + name: Check commit message format + entry: python dev/check_commit_msg.py + language: system + stages: [commit-msg] + diff --git a/dev/check_commit_msg.py b/dev/check_commit_msg.py new file mode 100644 index 0000000..af045eb --- /dev/null +++ b/dev/check_commit_msg.py @@ -0,0 +1,59 @@ +"""Validate that the commit subject starts with a recognized type emoji. + +Convention: https://mbercx.github.io/python-copier/dev-standards/#specifying-the-type-of-change + +Usage (called by pre-commit as a `commit-msg` hook): + + python dev/check_commit_msg.py .git/COMMIT_EDITMSG +""" + +import sys +from pathlib import Path + +# Keep in sync with `dev/update_changelog.py` and `docs/dev-standards.md`. +# Listed in priority order (matches the dev-standards table). +VALID_EMOJIS: tuple[str, ...] = ( + # Changelog sections + "๐Ÿ’ฅ", + "๐Ÿ“ฆ", + "โŒ", + "โœจ", + "๐Ÿ‘Œ", + "๐Ÿ›", + "๐Ÿ“š", + # Developer sections + "๐Ÿ”„", + "๐Ÿงช", + "โช", + "๐Ÿ”ง", + "๐Ÿงน", + # Excluded from changelog, but still valid types + "๐Ÿš€", + "๐Ÿญ", + "โ“", +) + + +def main() -> int: + commit_msg_file = Path(sys.argv[1]) + message = commit_msg_file.read_text(encoding="utf-8") + + first_line = message.split("\n", maxsplit=1)[0] + if not first_line.startswith(VALID_EMOJIS): + print( + f"โŒ Commit subject must start with a type emoji.\n" + f"\n" + f" Got: {first_line!r}\n" + f"\n" + f" Allowed: {' '.join(VALID_EMOJIS)}\n" + f"\n" + f" See: https://mbercx.github.io/python-copier/dev-standards/#specifying-the-type-of-change", + file=sys.stderr, + ) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dev/update_changelog.py b/dev/update_changelog.py new file mode 100644 index 0000000..95afc83 --- /dev/null +++ b/dev/update_changelog.py @@ -0,0 +1,182 @@ +"""Update `CHANGELOG.md` based on commits since the latest release tag. + +Commit type conventions: https://mbercx.github.io/python-copier/dev-standards/#specifying-the-type-of-change +""" + +# ruff: noqa: S603, S607 + +import re +import subprocess +from pathlib import Path + +from qe_tools.__about__ import __version__ + +ROOT = Path(__file__).resolve().parent.parent +GIT_REMOTE = "origin" + +CHANGELOG_SECTIONS: dict[str, str] = { + "๐Ÿ’ฅ": "Breaking changes", + "๐Ÿ“ฆ": "Dependency updates", + "โŒ": "Deprecations", + "โœจ": "New features", + "๐Ÿ‘Œ": "Improvements", + "๐Ÿ›": "Bug fixes", + "๐Ÿ“š": "Documentation", +} + +DEVELOPER_SECTIONS: dict[str, str] = { + "๐Ÿ”„": "Refactor", + "๐Ÿงช": "Tests", + "โช": "Reverts", + "๐Ÿ”ง": "DevOps", + "๐Ÿงน": "Cleanup", +} + +ALL_SECTIONS = CHANGELOG_SECTIONS | DEVELOPER_SECTIONS + +EXCLUDED_EMOJIS: set[str] = {"๐Ÿš€", "๐Ÿญ", "โ“"} + + +def get_github_url() -> str | None: + """Derive `https://github.com/org/repo` from the git remote origin, or `None`.""" + try: + url = subprocess.run( + ["git", "remote", "get-url", GIT_REMOTE], + capture_output=True, + check=True, + encoding="utf-8", + cwd=ROOT, + ).stdout.strip() + except subprocess.CalledProcessError: + return None + + match = re.match(r"(?:https://github\.com/|git@github\.com:)(.+?)(?:\.git)?$", url) + return f"https://github.com/{match.group(1)}" if match else None + + +def get_latest_tag() -> str | None: + """Return the latest `vX.Y.Z` tag, or `None` if no tags exist.""" + result = subprocess.run( + ["git", "tag", "--sort=v:refname"], + capture_output=True, + check=True, + encoding="utf-8", + cwd=ROOT, + ) + tags = [ + t for t in result.stdout.splitlines() if re.fullmatch(r"v\d+\.\d+\.\d+\S*", t) + ] + return tags[-1] if tags else None + + +def get_commits(since_tag: str | None) -> str: + """Return the `git log` output since the given tag, or all commits if `None`.""" + cmd = ["git", "log", "--pretty=format:%h|%H|%s"] + if since_tag: + cmd.append(f"{since_tag}..HEAD") + return subprocess.run( + cmd, + capture_output=True, + check=True, + encoding="utf-8", + cwd=ROOT, + ).stdout + + +def classify_commit(message: str) -> tuple[str | None, str]: + """Return `(emoji, stripped_message)` or `(None, message)` if not a changelog type.""" + for emoji in ALL_SECTIONS: + if message.startswith(emoji): + return emoji, message[len(emoji) :].lstrip() + return None, message + + +def update_changelog() -> None: + """Update `CHANGELOG.md` for a first draft of the release.""" + version = __version__ + + changelog_path = ROOT / "CHANGELOG.md" + current = ( + changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else "" + ) + + if f"## v{version}" in current: + print(f"๐Ÿ”„ Version v{version} already in CHANGELOG.md. Skipping.") + return + + github_url = get_github_url() + if github_url is None: + print( + f"โš ๏ธ Could not derive GitHub URL from remote '{GIT_REMOTE}'. Commit links will use plain hashes." + ) + + latest_tag = get_latest_tag() + commits_raw = get_commits(latest_tag) + + if not commits_raw.strip(): + print("๐Ÿคท No commits found since last tag. Skipping.") + return + + pr_pattern = re.compile(r"\s*\(#\d+\)$") + + sections: dict[str, list[str]] = {emoji: [] for emoji in ALL_SECTIONS} + uncategorized: list[str] = [] + + for line in commits_raw.splitlines(): + if not line: + continue + hash_short, hash_long, message = line.split("|", maxsplit=2) + + # Strip PR number from the message + message = pr_pattern.sub("", message) + + # Classify by leading emoji + emoji, stripped_msg = classify_commit(message) + + if emoji is None and any(message.startswith(e) for e in EXCLUDED_EMOJIS): + continue + + if github_url: + entry = ( + f"* {stripped_msg} [[{hash_short}]({github_url}/commit/{hash_long})]" + ) + else: + entry = f"* {stripped_msg} [{hash_short}]" + + if emoji is None: + uncategorized.append(entry) + print(f"โš ๏ธ Uncategorized commit: {hash_short} {message}") + else: + sections[emoji].append(entry) + + # Build changelog: uncategorized first to improve visibility + section_text = "" + if uncategorized: + section_text += "\n### โ“ Uncategorized\n\n" + section_text += "\n".join(uncategorized) + "\n" + + # Main changelog sections -> User oriented + for emoji, section_name in CHANGELOG_SECTIONS.items(): + if sections[emoji]: + section_text += f"\n### {emoji} {section_name}\n\n" + section_text += "\n".join(sections[emoji]) + "\n" + + # Developer section with plain-text subsections + dev_text = "" + for emoji, section_name in DEVELOPER_SECTIONS.items(): + if sections[emoji]: + dev_text += f"\n{emoji} {section_name}\n\n" + dev_text += "\n".join(sections[emoji]) + "\n" + + if dev_text: + section_text += f"\n#### Developer\n{dev_text}" + + header = "# Changelog\n\n" + body = current.removeprefix("# Changelog").lstrip("\n") + new_entry = f"## v{version}\n{section_text}" + changelog_path.write_text(header + new_entry + "\n" + body, encoding="utf-8") + print(f"โœจ Updated CHANGELOG.md for v{version}.") + + +if __name__ == "__main__": + update_changelog() diff --git a/docs/developer.md b/docs/developer.md index be36c8b..dc4dd5f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -32,7 +32,7 @@ and install the package locally in **editable** mode (`-e`): ๐Ÿ”ง **Pre-commit** - To make sure your changes adhere to our formatting/linting preferences, install the pre-commit hooks: + To make sure your changes adhere to our formatting/linting preferences and commit-message convention, install the pre-commit hooks: pre-commit install @@ -72,7 +72,7 @@ and install the package locally in **editable** mode (`-e`): ๐Ÿ”ง **Pre-commit** - To make sure your changes adhere to our formatting/linting preferences, install the pre-commit hooks: + To make sure your changes adhere to our formatting/linting preferences and commit-message convention, install the pre-commit hooks: uvx pre-commit install @@ -118,7 +118,7 @@ and install the package locally in **editable** mode (`-e`): ๐Ÿ”ง **Pre-commit** - To make sure your changes adhere to our formatting/linting preferences, install the pre-commit hooks: + To make sure your changes adhere to our formatting/linting preferences and commit-message convention, install the pre-commit hooks: hatch run pre-commit:install @@ -179,3 +179,45 @@ And the following rules for the files in the `tests` directory: | --------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `INP001` | [implicit-namespace-package](https://docs.astral.sh/ruff/rules/implicit-namespace-package/) | When tests are not part of the package, there is no need for `__init__.py` files. | | `S101` | [assert](https://docs.astral.sh/ruff/rules/assert/) | Asserts should not be used in production environments, but are fine for tests. | + +And the following rules for the files in the `dev` directory: + +| Code | Rule | Rationale / Note | +| --------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `INP001` | [implicit-namespace-package](https://docs.astral.sh/ruff/rules/implicit-namespace-package/) | Dev scripts are not part of the package, so there is no need for `__init__.py` files. | +| `T201` | [print](https://docs.astral.sh/ruff/rules/print/) | Dev scripts use `print()` for user-facing output, which is fine outside of library code. | + +## Release + + +!!! important + Before the **first** release works, the repository has to be registered as a PyPI [Trusted Publisher](https://docs.pypi.org/trusted-publishers/) and a `pypi` GitHub environment has to exist. + See the [`python-copier` first-publication guide](https://mbercx.github.io/python-copier/publishing/) for that one-time setup โ€” you only do it once per project. + +Releases of `qe-tools` are cut by pushing a `vX.Y.Z` tag to GitHub. +The `cd` workflow under `.github/workflows/cd.yaml` then builds an sdist and wheel with Hatch and publishes them to PyPI. + +1. Bump the version and generate the changelog draft: + + hatch run bump + + This runs `hatch version` to update `src/qe-tools/__about__.py`, then runs `dev/update_changelog.py` to prepend a new section to `CHANGELOG.md` with commits sorted by type. + Review the generated changelog, make any edits, and commit the bump on `main` (typically via a PR). + +2. Tag the bump commit and push the tag: + + git tag -a v -m '๐Ÿš€ Release v' + git push origin v + +3. The `cd.yaml` workflow picks up the tag, builds the distributions, and publishes them to PyPI. + +The git tag and the version in `__about__.py` must agree. +PyPI only sees the version baked into the built distribution, so a mismatch will silently publish under the wrong version (or be rejected as a duplicate of an existing release), and re-tagging after the fact is awkward. + +## Commit messages + +Each commit subject must start with a leading emoji indicating the type of change. +This is enforced locally by a `commit-msg` pre-commit hook (`dev/check_commit_msg.py`) and in CI by a `commit-msgs` job that checks every commit in a pull request. +The changelog script (`dev/update_changelog.py`) uses the same emojis to automatically sort commits into the right sections, so the sorting happens at commit time, when the changes are still fresh in memory. + +For the full specification and emoji table, see the [commit message conventions](https://mbercx.github.io/python-copier/dev-standards/#specifying-the-type-of-change). diff --git a/pyproject.toml b/pyproject.toml index a0da2d0..0f7c09e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,8 @@ tests = [ "pytest", "pytest_cases", "timeout_decorator", - "pytest-regressions" + "pytest-regressions", + "pytest-cov" ] pre-commit = [ "pre-commit", @@ -75,6 +76,10 @@ path = "src/qe_tools/__about__.py" [tool.hatch.envs.default] installer = 'uv' +scripts.bump = [ + "hatch version {args}", + "python dev/update_changelog.py", +] [tool.hatch.envs.docs] features = ["docs"] @@ -91,15 +96,29 @@ scripts.run = "pre-commit run {args}" features = ["tests", "ase", "pymatgen"] randomize = false parallel = false -run = "pytest {args}" [[tool.hatch.envs.hatch-test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] +[tool.mypy] +python_version = "3.10" +files = ["src"] +warn_unused_ignores = true +warn_redundant_casts = true +warn_unreachable = true + [[tool.mypy.overrides]] -module = "glom" +module = ["glom", "ase", "ase.*", "pymatgen", "pymatgen.*", "aiida", "aiida.*"] ignore_missing_imports = true +[tool.coverage.run] +source = ["qe_tools"] +branch = true + +[tool.coverage.report] +show_missing = true +fail_under = 50 + [tool.ruff] lint.ignore = [ "TRY003", # https://docs.astral.sh/ruff/rules/raise-vanilla-args/ @@ -112,3 +131,4 @@ lint.ignore = [ ] [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["INP001", "S101", "PT009", "B026", "PT027", "T201"] +"dev/**/*.py" = ["INP001", "T201"] diff --git a/src/qe_tools/inputs/base.py b/src/qe_tools/inputs/base.py index e5279ad..db55cc4 100644 --- a/src/qe_tools/inputs/base.py +++ b/src/qe_tools/inputs/base.py @@ -762,12 +762,12 @@ def _validate_species_name(atom_name, pseudo_file_name): ) from exc # Make sure the card block lines were extracted. If they were, store the # string of lines as blockstr. - if match.group("block") is None: # type: ignore[union-attr] + if match.group("block") is None: raise ParsingError( "The ATOMIC_POSITIONS card block was parse as empty in\n" + txt ) - blockstr = match.group("block") # type: ignore[union-attr] + blockstr = match.group("block") # Define a small helper function to convert strings of fortran-type floats. def fortfloat(string): @@ -1283,7 +1283,7 @@ def _get_parameters_from_cell_bare(*, ibrav: int, cell: CellT) -> ParametersT: parameters[cos_bc] = np.dot(v2, v3) / (parameters[cell_b] * parameters[cell_c]) else: raise ValueError(f"The given 'ibrav' value '{ibrav}' is not understood.") - return parameters + return {k: float(v) for k, v in parameters.items()} def _check_parameters( @@ -1305,7 +1305,7 @@ def _check_parameters( using_celldm=False, qe_version=qe_version, ) - if not np.allclose(cell_reconstructed, cell, rtol=0, atol=tolerance): + if not np.allclose(cell_reconstructed, np.asarray(cell), rtol=0, atol=tolerance): raise ValueError( f"The cell {cell_reconstructed} constructed with ibrav={ibrav}, parameters={parameters} does not match " f"the input cell{cell}." diff --git a/src/qe_tools/outputs/parsers/pw.py b/src/qe_tools/outputs/parsers/pw.py index 8e13247..0d23278 100644 --- a/src/qe_tools/outputs/parsers/pw.py +++ b/src/qe_tools/outputs/parsers/pw.py @@ -44,7 +44,7 @@ def parse(content): if ( element_root.find("general_info").find("creator").get("VERSION") == "7.0" - ): # type: ignore + ): schema_filename = "qes_211101.xsd" except AttributeError: pass diff --git a/tests/test_cell_conversion.py b/tests/test_cell_conversion.py index 37ca7a8..071b467 100644 --- a/tests/test_cell_conversion.py +++ b/tests/test_cell_conversion.py @@ -31,7 +31,9 @@ def case_structure_generator(path): return ins, None, ValueError outs = {} - for key in ["a", "b", "c", "cosab", "cosac", "cosbc"] + [f"celldm({i})" for i in range(1, 7)]: + for key in ["a", "b", "c", "cosab", "cosac", "cosbc"] + [ + f"celldm({i})" for i in range(1, 7) + ]: if key in system_dict: outs[key] = system_dict[key]