From 7b73d9e8e7fed26ba3d917581b53bb6c90e1f087 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 24 Feb 2025 11:47:47 +0000 Subject: [PATCH 1/5] Add test workflow for github --- .github/workflows/test.yml | 44 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 ++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..11a9792 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: tests + +on: + push: + branches: [ main ] + paths: + - '**.py' + - '.github/workflows/tests.yml' + pull_request: + branches: [ main ] + paths: + - '**.py' + - '.github/workflows/tests.yml' + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: python3 -m pip install '.[test]' + - name: Lint with flake8 + run: | + python3 -m pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + python3 -m pytest diff --git a/pyproject.toml b/pyproject.toml index 8a572a1..b5efe8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" license = {file = "LICENSE"} classifiers = ["License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Programming Language :: Python :: 3"] -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = ["numpy>=1.21.0", "scipy>=1.7.0", @@ -25,6 +25,7 @@ dependencies = ["numpy>=1.21.0", [project.optional-dependencies] dev = ["ruff", "pre-commit"] +test = ["pytest"] [project.urls] Home = "https://github.com/pipliggins/EVo" From ef57c751cea3133f6e00a7c00b6713085592b36c Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 24 Feb 2025 11:52:35 +0000 Subject: [PATCH 2/5] fix file name --- .github/workflows/{test.yml => tests.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{test.yml => tests.yml} (98%) diff --git a/.github/workflows/test.yml b/.github/workflows/tests.yml similarity index 98% rename from .github/workflows/test.yml rename to .github/workflows/tests.yml index 11a9792..4dd23b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ on: paths: - '**.py' - '.github/workflows/tests.yml' - workflow_dispatch: + jobs: build: From 42a5c1527cfacdbaa45f86a61e94258f9faa6ee5 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 24 Feb 2025 12:28:46 +0000 Subject: [PATCH 3/5] Allow the user to set the output location --- .gitignore | 3 +-- README.md | 12 ++++++------ evo/dgs.py | 20 ++++++++++++++------ evo/dgs_classes.py | 2 ++ evo/writeout.py | 6 ++---- tests/integration/test_coh.py | 7 +++++-- tests/integration/test_cohs.py | 7 +++++-- tests/integration/test_cohsn.py | 7 +++++-- tests/integration/test_oh.py | 7 +++++-- tests/integration/test_soh.py | 7 +++++-- 10 files changed, 50 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 7ddf7da..882d4d6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,7 @@ __pycache__/ .venv -Output/* -evo/Output/* +outputs/* # sphinx docs docs/_build/ diff --git a/README.md b/README.md index 52f505d..6f4dd2f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ EVo can be set up using either melt volatile contents, or for a set amount of at ### Prerequisites -This programme requires Python 3 to run. +This programme requires Python 3 to run. If you use python virtual environments, or Anaconda, requirements files (requirements.txt and environment.yml for virtualenv and conda, respectively) can be found in the Data file. @@ -29,19 +29,19 @@ To install locally, EVo must be downloaded from GitHub using into the project directory where you wish to use EVo. EVo must then be locally pip-installed: ``` cd EVO -python -m pip install -e evo/ +python -m pip install -e . ``` From this point, EVo can either be imported into your python scripts as a regular module using -`install evo` +`import evo` and run using -`evo.main('chem_file', 'env_file', 'output_options_file')` +`evo.main(, , , folder=)` Or EVo can be run directly from the terminal from inside the `evo` directory: ``` cd EVO/evo -python dgs.py input/chem.yaml input/env.yaml --output input/output.yaml +python dgs.py input/chem.yaml input/env.yaml --output-options input/output.yaml ``` The model should run and produce an output file Outputs/dgs_output_*.csv, and a set of graphs in the Output folder, if a decompression run has been selected. @@ -53,7 +53,7 @@ The different run types, model parameters and input values are all set in the en There are multiple run types which can be selected within EVo. At the highest level, a run can either be (1). single pressure, where equilibration between the gas and the melt only occurs at 1 pressure step set using P_START, or (2) a decompression run, where calculations start at P_START and run through pressure steps (the max and min size of which can be set using DP_MAX and DP_MIN respectively) until P_STOP is reached. These two high-level un options can be toggled between using SINGLE_STEP, where True sets up EVo to do a single pressure run, and False asks for decompression. Within these two high-level run types, 3 options for selecting starting conditions are available: -* Standard: pick a pressure (P_START), a starting gas weight fraction (WgT) and some combination of either current melt volatile contents, or gas fugacities. EVo will calculate the missing variables and either stop at that point for a single pressure run, or continue on in decompression mode. This is the default option, and will be used if both FIND_SATURATION and ATOMIC_MASS_SET are False. +* Standard: pick a pressure (P_START), a starting gas weight fraction (WgT) and some combination of either current melt volatile contents, or gas fugacities. EVo will calculate the missing variables and either stop at that point for a single pressure run, or continue on in decompression mode. This is the default option, and will be used if both FIND_SATURATION and ATOMIC_MASS_SET are False. * Volatile saturation: Chosen by switching FIND_SATURATION to True, given only the melt volatile contents and magma fO2, EVo will calculate the volatile saturation pressure and start a run from there. Any values given in P_START and WgT will be ignored; WgT will be set to 1e-8 at the volatile saturation point. * Atomic set: Chosen by switching ATOMIC_MASS_SET to True (and FIND_SATURATION to False). Given the melt fO2 and the atomic weight fractions of each of the other volatile species (H, +/- C, S & N, set using ATOMIC_H etc.), Evo will calculate both the appropriate distribution of each element across the different species considered, and the volatile saturation point of that composition. Particularly useful for studies where fO2 is varied but the amount of other elements should be held constant. Again, P_START and WgT will be ignored, WgT will be set to 1e-8 at the volatile saturation point. diff --git a/evo/dgs.py b/evo/dgs.py index 40ef809..2aa6c81 100644 --- a/evo/dgs.py +++ b/evo/dgs.py @@ -59,6 +59,7 @@ # python main [ ensure these are on your system ] import argparse import time +from pathlib import Path import numpy as np @@ -74,7 +75,7 @@ # ------------------------------------------------------------------------ -def main(f_chem, f_env, f_out): +def main(f_chem, f_env, f_out, folder="outputs"): """Main function for EVo. Call to run the model. @@ -87,6 +88,8 @@ def main(f_chem, f_env, f_out): Path to the environment input file f_out : str or NoneType Path to the file describing the required outputs, None if not used + folder : str + Path to the folder to write the results to """ start = time.time() @@ -97,6 +100,7 @@ def main(f_chem, f_env, f_out): # Instantiate the run, thermosystem, melt and output objects run, sys, melt, out = readin(f_chem, f_env, f_out) + run.results_folder = Path(folder) print("Set parameters:") run.print_conditions() @@ -228,19 +232,23 @@ def main(f_chem, f_env, f_out): ) my_parser.add_argument( - "--output", + "--output-options", help="use selected output options from output.yaml file", ) + my_parser.add_argument( + "-o", "--output", help="the folder location to write the results to" + ) + # Parse in files args = my_parser.parse_args() f_chem = args.chem # set chemical compositions file f_env = args.env # set environment file - if args.output: - f_out = args.output # set output file as an optional input - main(f_chem, f_env, f_out) + if args.output_options: + f_out = args.output_options # set output file as an optional input + main(f_chem, f_env, f_out, folder=args.output) else: f_out = None - main(f_chem, f_env, f_out) + main(f_chem, f_env, f_out, folder=args.output) diff --git a/evo/dgs_classes.py b/evo/dgs_classes.py index 041fca5..5af0f10 100644 --- a/evo/dgs_classes.py +++ b/evo/dgs_classes.py @@ -189,6 +189,8 @@ def __init__(self): self.GRAPHITE_SATURATED = False self.GRAPHITE_START = 0.0 # initial melt graphite content + self.results_folder = None + def param_set(self, params): """ Sets class up with parameters from the environment file. diff --git a/evo/writeout.py b/evo/writeout.py index 0746cd0..0fcd791 100644 --- a/evo/writeout.py +++ b/evo/writeout.py @@ -5,7 +5,6 @@ import glob import os -from pathlib import Path import matplotlib.pyplot as plt import numpy as np @@ -168,7 +167,7 @@ def writeout_file(sys, gas, melt, P, crashed=False): df = pd.DataFrame(data) - filepath = Path(__file__).parent / "Output" + filepath = sys.run.results_folder if not os.path.exists(filepath): os.makedirs(filepath) @@ -227,8 +226,7 @@ def writeout_figs(sys, melt, gas, out, P): List of the pressures each step was calculated at (bar) """ - filepath = Path(__file__).parent / "Output" - + filepath = sys.run.results_folder filelist = glob.glob(str(filepath / "*.png")) # Removes previous files so if output specification is changed # there is no confusion as to up to date files. diff --git a/tests/integration/test_coh.py b/tests/integration/test_coh.py index 31ac5c3..fed2c35 100644 --- a/tests/integration/test_coh.py +++ b/tests/integration/test_coh.py @@ -2,9 +2,12 @@ import pandas as pd -def test_coh_default(): +def test_coh_default(tmp_path): df = evo.main( - "evo/input/chem.yaml", "tests/integration/input_files/env_coh.yaml", None + "evo/input/chem.yaml", + "tests/integration/input_files/env_coh.yaml", + None, + folder=tmp_path, ) assert isinstance(df, pd.DataFrame) diff --git a/tests/integration/test_cohs.py b/tests/integration/test_cohs.py index c2b8773..2813129 100644 --- a/tests/integration/test_cohs.py +++ b/tests/integration/test_cohs.py @@ -2,9 +2,12 @@ import pandas as pd -def test_cohs_default(): +def test_cohs_default(tmp_path): df = evo.main( - "evo/input/chem.yaml", "tests/integration/input_files/env_cohs.yaml", None + "evo/input/chem.yaml", + "tests/integration/input_files/env_cohs.yaml", + None, + folder=tmp_path, ) assert isinstance(df, pd.DataFrame) diff --git a/tests/integration/test_cohsn.py b/tests/integration/test_cohsn.py index cf3e1b2..add8d36 100644 --- a/tests/integration/test_cohsn.py +++ b/tests/integration/test_cohsn.py @@ -2,9 +2,12 @@ import pandas as pd -def test_cohsn_default(): +def test_cohsn_default(tmp_path): df = evo.main( - "evo/input/chem.yaml", "tests/integration/input_files/env_cohsn.yaml", None + "evo/input/chem.yaml", + "tests/integration/input_files/env_cohsn.yaml", + None, + folder=tmp_path, ) assert isinstance(df, pd.DataFrame) diff --git a/tests/integration/test_oh.py b/tests/integration/test_oh.py index 8d565d8..df2d78b 100644 --- a/tests/integration/test_oh.py +++ b/tests/integration/test_oh.py @@ -2,9 +2,12 @@ import pandas as pd -def test_oh_default(): +def test_oh_default(tmp_path): df = evo.main( - "evo/input/chem.yaml", "tests/integration/input_files/env_oh.yaml", None + "evo/input/chem.yaml", + "tests/integration/input_files/env_oh.yaml", + None, + folder=tmp_path, ) assert isinstance(df, pd.DataFrame) diff --git a/tests/integration/test_soh.py b/tests/integration/test_soh.py index c640cf2..e68038b 100644 --- a/tests/integration/test_soh.py +++ b/tests/integration/test_soh.py @@ -2,9 +2,12 @@ import pandas as pd -def test_soh_default(): +def test_soh_default(tmp_path): df = evo.main( - "evo/input/chem.yaml", "tests/integration/input_files/env_soh.yaml", None + "evo/input/chem.yaml", + "tests/integration/input_files/env_soh.yaml", + None, + folder=tmp_path, ) assert isinstance(df, pd.DataFrame) From e6e76f634bb75f379f632d974bd1cb71a3b01d3d Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 24 Feb 2025 12:34:37 +0000 Subject: [PATCH 4/5] Fix fmq folder path --- evo/writeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evo/writeout.py b/evo/writeout.py index 0fcd791..79d4be6 100644 --- a/evo/writeout.py +++ b/evo/writeout.py @@ -270,7 +270,7 @@ def plot_fo2FMQ(melt, gas, P, path): plt.xscale("log") plt.xlabel("Pressure (bars)") plt.ylabel(r"$\Delta$ FMQ") - plt.savefig("Output/FMQ.png") + plt.savefig(os.path.join(path, "FMQ.png")) plt.close() fo2 = [] From 8819266533569f16e8dd339cbf8cc7864cf2e8a5 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Mon, 24 Feb 2025 13:03:32 +0000 Subject: [PATCH 5/5] use pathlib --- evo/writeout.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/evo/writeout.py b/evo/writeout.py index 79d4be6..37e8596 100644 --- a/evo/writeout.py +++ b/evo/writeout.py @@ -3,9 +3,6 @@ contains options to produce a graph of the results. """ -import glob -import os - import matplotlib.pyplot as plt import numpy as np import pandas as pd @@ -168,8 +165,8 @@ def writeout_file(sys, gas, melt, P, crashed=False): df = pd.DataFrame(data) filepath = sys.run.results_folder - if not os.path.exists(filepath): - os.makedirs(filepath) + if not filepath.exists(): + filepath.mkdir() if not crashed: file_name = ( @@ -182,7 +179,7 @@ def writeout_file(sys, gas, melt, P, crashed=False): f"_{sys.run.RUN_TYPE}_{sys.T:.0f}K.csv" ) - output_path = os.path.join(filepath, file_name) + output_path = filepath / file_name if sys.run.FIND_SATURATION is True or sys.run.ATOMIC_MASS_SET is True: df_sat = pd.DataFrame( @@ -227,12 +224,13 @@ def writeout_figs(sys, melt, gas, out, P): """ filepath = sys.run.results_folder - filelist = glob.glob(str(filepath / "*.png")) + # filelist = glob.glob(str(filepath / "*.png")) + filelist = list(filepath.glob("*.png")) # Removes previous files so if output specification is changed # there is no confusion as to up to date files. for file in filelist: - os.remove(file) + file.unlink() if ( out is not None ): # If an output file listing requested figures has been included: @@ -270,7 +268,7 @@ def plot_fo2FMQ(melt, gas, P, path): plt.xscale("log") plt.xlabel("Pressure (bars)") plt.ylabel(r"$\Delta$ FMQ") - plt.savefig(os.path.join(path, "FMQ.png")) + plt.savefig(path / "FMQ.png") plt.close() fo2 = [] @@ -281,7 +279,7 @@ def plot_fo2FMQ(melt, gas, P, path): # plt.xscale('log') plt.xlabel("Pressure (bars)") plt.ylabel("log(10) fO2") - plt.savefig(os.path.join(path, "fO2.png")) + plt.savefig(path / "fO2.png") plt.close() @@ -324,7 +322,7 @@ def plot_gasspecies_mol(gas, P, path): plt.gca().invert_yaxis() plt.xlabel(f"Speciation in a {gas.sys.run.GAS_SYS} gas (mol frac)") plt.ylabel("Pressure (bar)") - plt.savefig(os.path.join(path, "speciation(mol).png")) + plt.savefig(path / "speciation(mol).png") plt.close() @@ -369,7 +367,7 @@ def plot_gasspecies_wt(gas, P, path): plt.gca().invert_yaxis() plt.xlabel(f"Gas phase speciation of a {gas.sys.run.GAS_SYS} system (wt %)") plt.ylabel("Pressure (bar)") - plt.savefig(os.path.join(path, "speciation(wt).png")) + plt.savefig(path / "speciation(wt).png") plt.close() @@ -404,7 +402,7 @@ def plot_meltspecies(melt, P, path): plt.gca().invert_yaxis() plt.xlabel("Melt volatile content (wt%)") plt.ylabel("Pressure (bar)") - plt.savefig(os.path.join(path, "meltspecies.png")) + plt.savefig(path / "meltspecies.png") plt.close() @@ -421,4 +419,4 @@ def plot_gasfraction(sys, P, path): ax1.set_ylabel("Pressure (bar)") ax2.plot(cnvt.frac2perc(sys.GvF), P) ax2.set_xlabel("Exsolved gas volume %") - plt.savefig(os.path.join(path, "exsolved_gas.png")) + plt.savefig(path / "exsolved_gas.png")