Skip to content

Commit

Permalink
Merge pull request #37 from sparks-baird/cli
Browse files Browse the repository at this point in the history
working cli via `xtal2png --encode ...` and `xtal2png --decode ...` syntax
  • Loading branch information
sgbaird committed May 28, 2022
2 parents 9c335a8 + 09c267c commit 75c1e14
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 60 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ MANIFEST
.python-version
src/xtal2png/meta.yaml
xtal2png/meta.yaml
tmp/**
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,76 @@ Then take a look into the `scripts` and `notebooks` folders.
```bash
conda env update -f environment.lock.yml --prune
``` -->


## Command Line Interface (CLI)

Make sure to install the package first per the installation instructions above. Here is
how to access the help for the CLI and a few examples to get you started.

### Help

You can see the usage information of the `xtal2png` CLI script via:

```bash
(xtal2png) PS C:\Users\sterg\Documents\GitHub\sparks-baird\xtal2png> xtal2png --help
```

> ```bash
> usage: xtal2png [-h] [--version] [-p STRING] [-s STRING] [--encode] [--decode] [-v] [-vv]
>
> Crystal to PNG encoder/decoder.
>
> optional arguments:
> -h, --help show this help message and exit
> --version show program's version number and exit
> -p STRING, --path STRING
> Crystallographic information file (CIF) filepath
> (extension must be .cif or .CIF) or path to directory
> containing .cif files or processed PNG filepath or path
> to directory containing processed .png files (extension
> must be .png or .PNG). Assumes CIFs if --encode flag is
> used. Assumes PNGs if --decode flag is used.
> -s STRING, --save-dir STRING
> Directory to save processed PNG files or decoded CIFs to.
> --encode Encode CIF files as PNG images.
> --decode Decode PNG images as CIF files.
> -v, --verbose set loglevel to INFO
> -vv, --very-verbose set loglevel to DEBUG
> ```
### Examples
To encode a single CIF file located at `src/xtal2png/utils/Zn2B2PbO6.cif` as a PNG and save the PNG to the `tmp` directory:
```bash
xtal2png --encode --path src/xtal2png/utils/Zn2B2PbO6.cif --save-dir tmp
```
To encode all CIF files contained in the `src/xtal2png/utils` directory as a PNG and
save corresponding PNGs to the `tmp` directory:
```bash
xtal2png --encode --path src/xtal2png/utils --save-dir tmp
```
To decode a single structure-encoded PNG file located at
`data/preprocessed/Zn8B8Pb4O24,volume=623,uid=b62a.png` as a CIF file and save the CIF
file to the `tmp` directory:
```bash
xtal2png --decode --path data/preprocessed/Zn8B8Pb4O24,volume=623,uid=b62a.png --save-dir tmp
```
To decode all structure-encoded PNG file contained in the `data/preprocessed` directory as CIFs and save the CIFs to the `tmp` directory:
```bash
xtal2png --decode --path data/preprocessed --save-dir tmp
```
Note that the save directory (e.g. `tmp`) including any parents (e.g. `ab/cd/tmp`) will
be created automatically if the directory does not already exist.
## Project Organization
```
Expand Down
130 changes: 70 additions & 60 deletions src/xtal2png/core.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,4 @@
"""
This is a skeleton file that can serve as a starting point for a Python
console script. To run this script uncomment the following lines in the
``[options.entry_points]`` section in ``setup.cfg``::
console_scripts =
fibonacci = xtal2png.skeleton:run
Then run ``pip install .`` (or ``pip install -e .`` for editable mode)
which will install the command ``fibonacci`` inside your current environment.
Besides console scripts, the header (i.e. until ``_logger``...) of this file can
also be used as template for Python modules.
Note:
This skeleton file can be safely removed if not needed!
References:
- https://setuptools.pypa.io/en/latest/userguide/entry_point.html
- https://pip.pypa.io/en/stable/reference/pip_install
"""
"""Crystal to PNG conversion core functions and scripts."""

import argparse
import logging
Expand All @@ -37,6 +17,7 @@
from PIL import Image
from pymatgen.core.lattice import Lattice
from pymatgen.core.structure import Structure
from pymatgen.io.cif import CifWriter

from xtal2png import __version__
from xtal2png.utils.data import dummy_structures, rgb_scaler, rgb_unscaler
Expand All @@ -53,25 +34,8 @@

# ---- Python API ----
# The functions defined in this section can be imported by users in their
# Python scripts/interactive interpreter, e.g. via
# `from xtal2png.skeleton import fib`,
# when using this Python module as a library.


# def fib(n):
# """Fibonacci example function

# Args:
# n (int): integer

# Returns:
# int: n-th Fibonacci number
# """
# assert n > 0
# a, b = 1, 1
# for _i in range(n - 1):
# a, b = b, a + b
# return a
# Python scripts/interactive interpreter, e.g. via `from xtal2png.core import
# XtalConverter`, when using this Python module as a library.


ATOM_ID = 1
Expand All @@ -91,6 +55,11 @@
DISTANCE_KEY = "distance"


def construct_save_name(s: Structure):
save_name = f"{s.formula.replace(' ', '')},volume={int(np.round(s.volume))},uid={str(uuid4())[0:4]}" # noqa: E501
return save_name


class XtalConverter:
"""Convert between pymatgen Structure object and PNG-encoded representation."""

Expand Down Expand Up @@ -144,7 +113,9 @@ def __init__(

def xtal2png(
self,
structures: List[Union[Structure, str, "PathLike[str]"]],
structures: Union[
List[Union[Structure, str, "PathLike[str]"]], str, "PathLike[str]"
],
show: bool = False,
save: bool = True,
):
Expand All @@ -153,7 +124,8 @@ def xtal2png(
Parameters
----------
structures : List[Union[Structure, str, PathLike[str]]]
pymatgen Structure objects or path to CIF files.
pymatgen Structure objects or path to CIF files or path to directory
containing CIF files.
show : bool, optional
Whether to display the PNG-encoded file, by default False
save : bool, optional
Expand Down Expand Up @@ -230,7 +202,7 @@ def process_filepaths_or_structures(self, structures):

# load the CIF and convert to a pymatgen Structure
S.append(Structure.from_file(s))
save_names.append(str(s))
save_names.append(Path(str(s)).stem)

elif isinstance(s, Structure):
if not first_is_structure:
Expand All @@ -239,9 +211,7 @@ def process_filepaths_or_structures(self, structures):
)

S.append(s)
save_names.append(
f"{s.formula.replace(' ', '')},volume={int(np.round(s.volume))},uid={str(uuid4())[0:4]}" # noqa
)
save_names.append(construct_save_name(s))
else:
raise ValueError(
f"structures should be of type `str`, `os.PathLike` or `pymatgen.core.structure.Structure`, not {type(S)} (entry {i})" # noqa
Expand Down Expand Up @@ -278,8 +248,9 @@ def png2xtal(
S = self.arrays_to_structures(data)

if save:
# save new CIF files
1 + 1
for s in S:
fpath = path.join(self.save_dir, construct_save_name(s) + ".cif")
CifWriter(s).write_file(fpath)

return S

Expand Down Expand Up @@ -599,18 +570,39 @@ def parse_args(args):
Returns:
:obj:`argparse.Namespace`: command line parameters namespace
"""
parser = argparse.ArgumentParser(description="Crystal to PNG converter.")
parser = argparse.ArgumentParser(description="Crystal to PNG encoder/decoder.")
parser.add_argument(
"--version",
action="version",
version="xtal2png {ver}".format(ver=__version__),
)
parser.add_argument(
"-p",
"--path",
dest="fpath",
help="Crystallographic information file (CIF) filepath (extension must be .cif or .CIF) or path to directory containing .cif files.", # noqa: E501
help="Crystallographic information file (CIF) filepath (extension must be .cif or .CIF) or path to directory containing .cif files or processed PNG filepath or path to directory containing processed .png files (extension must be .png or .PNG). Assumes CIFs if --encode flag is used. Assumes PNGs if --decode flag is used.", # noqa: E501
type=str,
metavar="STRING",
)
parser.add_argument(
"-s",
"--save-dir",
dest="save_dir",
default=".",
help="Directory to save processed PNG files or decoded CIFs to.",
type=str,
metavar="STRING",
)
parser.add_argument(
"--encode",
action="store_true",
help="Encode CIF files as PNG images.",
)
parser.add_argument(
"--decode",
action="store_true",
help="Decode PNG images as CIF files.",
)
parser.add_argument(
"-v",
"--verbose",
Expand Down Expand Up @@ -643,28 +635,46 @@ def setup_logging(loglevel):


def main(args):
"""Wrapper allowing :func:`fib` to be called with string arguments in a CLI fashion
Instead of returning the value from :func:`fib`, it prints the result to the
``stdout`` in a nicely formatted message.
"""Wrapper allowing :func:`XtalConverter()` :func:`xtal2png()` and
:func:`png2xtal()` methods to be called with string arguments in a CLI fashion.
Args:
args (List[str]): command line parameters as list of strings
(for example ``["--verbose", "42"]``).
(for example ``["--verbose", "example.cif"]``).
"""
args = parse_args(args)
setup_logging(args.loglevel)
_logger.debug("Beginning conversion to PNG format")
if Path(args.fpath).suffix in [".cif", ".CIF"]:

if args.encode and args.decode:
raise ValueError("Specify --encode or --decode, not both.")

if args.encode:
ext = ".cif"
elif args.decode:
ext = ".png"
else:
raise ValueError("Specify at least one of --encode or --decode")

if Path(args.fpath).suffix in [ext, ext.upper()]:
fpaths = [args.fpath]
elif path.isdir(args.fpath):
fpaths = glob(path.join(args.fpath, "*.cif"))
fpaths = glob(path.join(args.fpath, f"*{ext}"))
if fpaths == []:
raise ValueError(
f"Assuming --path input is directory to files. No files of type {ext} present in {args.fpath}" # noqa: E501
)
else:
raise ValueError(
f"Input should be a path to a single .cif file or a path to a directory containing cif file(s). Received: {args.fpath}" # noqa: E501
f"Input should be a path to a single {ext} file or a path to a directory containing {ext} file(s). Received: {args.fpath}" # noqa: E501
)

XtalConverter().xtal2png(fpaths)
xc = XtalConverter(save_dir=args.save_dir)
if args.encode:
xc.xtal2png(fpaths, save=True)
elif args.decode:
xc.png2xtal(fpaths, save=True)

_logger.info("Script ends here")


Expand All @@ -685,7 +695,7 @@ def run():
# After installing your project with pip, users can also run your Python
# modules as scripts via the ``-m`` flag, as defined in PEP 338::
#
# python -m xtal2png.skeleton 42
# python -m xtal2png.core example.cif
#
run()

Expand Down
50 changes: 50 additions & 0 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from os import path

from xtal2png.core import main, parse_args


def test_parse_encode_args():
args = parse_args(["--encode", "--path", "src/xtal2png/utils/Zn2B2PbO6.cif"])
return args


def test_parse_decode_args():
args = parse_args(
["--decode", "--path", "data/external/preprocessed/Zn2B2PbO6.png"]
)
return args


def test_encode_single():
fpath = path.join("src", "xtal2png", "utils", "Zn2B2PbO6.cif")
args = ["--encode", "--path", fpath, "--save-dir", "tmp"]
main(args)


def test_encode_dir():
fpath = path.join("src", "xtal2png", "utils")
args = ["--encode", "--path", fpath, "--save-dir", path.join("tmp", "tmp")]
main(args)


def test_decode_single():
fpath = path.join("data", "preprocessed", "Zn8B8Pb4O24,volume=623,uid=b62a.png")
args = ["--decode", "--path", fpath, "--save-dir", "tmp"]
main(args)


def test_decode_dir():
fpath = path.join("data", "preprocessed")
args = ["--decode", "--path", fpath, "--save-dir", "tmp"]
main(args)


if __name__ == "__main__":
args = test_parse_encode_args()
args = test_parse_decode_args()
test_encode_single()
test_encode_dir()
test_decode_single()
test_decode_dir()

1 + 1

0 comments on commit 75c1e14

Please sign in to comment.