diff --git a/.github/workflows/run_ci.yml b/.github/workflows/run_ci.yml new file mode 100644 index 0000000..599f051 --- /dev/null +++ b/.github/workflows/run_ci.yml @@ -0,0 +1,63 @@ +name: PolyODENet_CI + +on: + pull_request: + branches: + - master + push: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + - name: get-python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: upgrade-pip + run: | + python -m pip install --upgrade pip + python -m pip install wheel + - name: get-numpy + run: | + pip install numpy + - name: get-pytorch + run: | + pip install torch==1.11.0+cpu -f https://download.pytorch.org/whl/torch_stable.html + - name: get-torchdiffeq + run: | + git clone https://github.com/rtqichen/torchdiffeq + cd torchdiffeq + python setup.py install + - name: get-pytest + run: | + pip install -U pytest + pip install -U pytest-cov + pytest --version + - name: install-PolyODENet + run: | + python setup.py install + - name: run-tests + run: | + cd Tests + pytest --cov=. --cov-report=xml + pwd + ls -l + cd .. + pwd + ls -l + # - name: publish-coverage + # uses: codecov/codecov-action@v2 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # directory: ./Tests + # env_vars: OS,PYTHON + # fail_ci_if_error: true + # files: ./Tests/coverage.xml + # flags: unittests + # name: PolyODENet + # verbose: true diff --git a/Source/__init__.py b/Source/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Source/main.py b/Source/main.py index fb2a796..23a0938 100644 --- a/Source/main.py +++ b/Source/main.py @@ -3,12 +3,27 @@ from Source.visualize import plot_write_ode import numpy as np import os +import sys import torch import datetime import torch.multiprocessing as mp def main(): + ''' + Main and Do_it construct. + + Introducing this two level approach allows the code to be run in two + different ways: + - From the command line when the main function will pass the system + command line arguments. + - From inside another Python script when the do_it function can have an + artificial set of command line arguments passed to it. + The latter approach is a way to run test cases with, e.g. pytest. + ''' + do_it(sys.argv[1:]) + +def do_it(arguments): parser = argparse.ArgumentParser() parser.add_argument('-c', "--conserve", type=int, default=0, help="Number of conservation constraints") @@ -48,7 +63,7 @@ def main(): help="Working directory") parser.add_argument("--zeroth", action='store_true', help="Add 0th order terms") - args = parser.parse_args() + args = parser.parse_args(arguments) world_size = args.jobs os.chdir(os.path.expandvars(os.path.expanduser(args.work_dir))) @@ -116,6 +131,7 @@ def main(): print(trainer.ode.basis_weights.data.numpy()) torch.save(trainer.ode, args.name+'.pt') plot_write_ode(trainer.ode, concentrations, timestamps, args.name, trainer.device) + return trainer.ode.basis_weights.data.numpy() if __name__ == '__main__': diff --git a/Source/visualize.py b/Source/visualize.py index f3e6467..f39f5f8 100644 --- a/Source/visualize.py +++ b/Source/visualize.py @@ -13,7 +13,7 @@ def plot_write_ode(ode, real_concentrations, real_timestamps, name, device): tensor_concentrations.to(device) tensor_timestamps.to(device) - with open(f"Rate_equations_of_{name}.txt", "w") as f: + with open(f"Rate_equations_of_{name.replace('/','_')}.txt", "w") as f: f.write(str(ode)) for i in range(len(real_timestamps)): @@ -36,10 +36,10 @@ def plot_write_ode(ode, real_concentrations, real_timestamps, name, device): plt.ylabel("Concentration") plt.ylim(bottom=-0.1) plt.legend() - title = f'Plot_of_{name}_set_{i}' + title = f"Plot_of_{name.replace('/','_')}_set_{i}" plt.title(title) plt.savefig(f"{title}.jpg", dpi=300) pred_df = pd.DataFrame(data=np.hstack([timestamps[:, np.newaxis], pred_conc]), columns=["## Time"] + [f'Set {j + 1} ' for j in range(pred_conc.shape[1])]) - pred_df.to_csv(f"Predicted_Concentrations_of_{name}_set_{i}.csv", sep='\t', index=False, float_format='%8.6f') + pred_df.to_csv(f"Predicted_Concentrations_of_{name.replace('/','_')}_set_{i}.csv", sep='\t', index=False, float_format='%8.6f') diff --git a/Tests/test_basic/README.md b/Tests/test_basic/README.md new file mode 100644 index 0000000..5666aeb --- /dev/null +++ b/Tests/test_basic/README.md @@ -0,0 +1,49 @@ +## Test constructed from Example 1 +### Consecutive reactions with 1 unknown intermediate + +This example has consecutive reactions with species C being the intermediate. + +A <--> C --> B + +The dimensionless reaction constants are: +* k_1+ = 2 (first reaction forward) +* k_1- = 1 (first reaction reverse) +* k_2 = 3 (second reaction) + +The corresponding ODEs are: + +d[A]/dt = -2[A] + 1[C] + +d[B]/dt = 3[C] + +d[C]/dt = 2[A] - 4[C] + +The original data are generated using scipy.integrate.odeint. +The initial condition is A0=2, B0=0, C0=0. + +In the 'ex1.txt' file, concentration data for C are replaced with -1, +meaning missing profile. Only the initial condition C0=0 is kept. + +The 'ex1.guess' file gives almost-optimized ODE coefficients so the +test can be fast. + +The 'ex1.ind' and 'ex1.scale' files control the polynomial model. + +Use the following command for a test run. + +```train_poly -f ex1.txt -m 1000 -N ex1 -igs``` + +Your 'Rate_equations_of_ex1.txt' should look like + +``` +PolynomialODE(): + +d[A]/dt = -1.84[A] + 0.682[C] + +d[B]/dt = 2.73[C] + +d[C]/dt = 1.84[A] - 3.32[C] +``` + +The test case is executed by the ```test_basic.py``` script. This script has +to be invoked by `pytest` from one directory up. diff --git a/Tests/test_basic/ex1.guess b/Tests/test_basic/ex1.guess new file mode 100644 index 0000000..93524b9 --- /dev/null +++ b/Tests/test_basic/ex1.guess @@ -0,0 +1,3 @@ +-1.0 0.0 1.0 + 0.0 0.0 0.0 + 2.0 2.0 -4.0 diff --git a/Tests/test_basic/ex1.ind b/Tests/test_basic/ex1.ind new file mode 100644 index 0000000..c1a16dd --- /dev/null +++ b/Tests/test_basic/ex1.ind @@ -0,0 +1,3 @@ +0 0 0 1 0 0 +1 0 1 1 1 1 +2 0 2 1 2 2 diff --git a/Tests/test_basic/ex1.scale b/Tests/test_basic/ex1.scale new file mode 100644 index 0000000..c2b8964 --- /dev/null +++ b/Tests/test_basic/ex1.scale @@ -0,0 +1,3 @@ +1. 0. -1. +0. 0. 0. +1. 1. 1. diff --git a/Tests/test_basic/ex1.txt b/Tests/test_basic/ex1.txt new file mode 100644 index 0000000..259e4fd --- /dev/null +++ b/Tests/test_basic/ex1.txt @@ -0,0 +1,40 @@ +0.00000000e+00 2.00000000e+00 0.00000000e+00 0.00000000e+00 +5.00000000e-02 1.81405372e+00 1.35894200e-02 -1 +1.00000000e-01 1.65282218e+00 4.93666785e-02 -1 +1.50000000e-01 1.51198657e+00 1.01118013e-01 -1 +2.00000000e-01 1.38808346e+00 1.64033431e-01 -1 +2.50000000e-01 1.27832791e+00 2.34405081e-01 -1 +3.00000000e-01 1.18047370e+00 3.09389644e-01 -1 +3.50000000e-01 1.09270275e+00 3.86821013e-01 -1 +4.00000000e-01 1.01353765e+00 4.65062829e-01 -1 +4.50000000e-01 9.41772570e-01 5.42892366e-01 -1 +5.00000000e-01 8.76418400e-01 6.19409136e-01 -1 +5.50000000e-01 8.16659425e-01 6.93963034e-01 -1 +6.00000000e-01 7.61818928e-01 7.66097856e-01 -1 +6.50000000e-01 7.11331926e-01 8.35506946e-01 -1 +7.00000000e-01 6.64723524e-01 9.01998415e-01 -1 +7.50000000e-01 6.21591704e-01 9.65467851e-01 -1 +8.00000000e-01 5.81593652e-01 1.02587704e+00 -1 +8.50000000e-01 5.44434844e-01 1.08323726e+00 -1 +9.00000000e-01 5.09860345e-01 1.13759634e+00 -1 +9.50000000e-01 4.77647843e-01 1.18902857e+00 -1 +1.00000000e+00 4.47602076e-01 1.23762684e+00 -1 +1.05000000e+00 4.19550345e-01 1.28349666e+00 -1 +1.10000000e+00 3.93338904e-01 1.32675151e+00 -1 +1.15000000e+00 3.68830036e-01 1.36750929e+00 -1 +1.20000000e+00 3.45899681e-01 1.40588967e+00 -1 +1.25000000e+00 3.24435504e-01 1.44201209e+00 -1 +1.30000000e+00 3.04335307e-01 1.47599422e+00 -1 +1.35000000e+00 2.85505726e-01 1.50795097e+00 -1 +1.40000000e+00 2.67861151e-01 1.53799367e+00 -1 +1.45000000e+00 2.51322822e-01 1.56622956e+00 -1 +1.50000000e+00 2.35818075e-01 1.59276147e+00 -1 +1.55000000e+00 2.21279701e-01 1.61768766e+00 -1 +1.60000000e+00 2.07645403e-01 1.64110167e+00 -1 +1.65000000e+00 1.94857328e-01 1.66309239e+00 -1 +1.70000000e+00 1.82861665e-01 1.68374408e+00 -1 +1.75000000e+00 1.71608296e-01 1.70313651e+00 -1 +1.80000000e+00 1.61050482e-01 1.72134506e+00 -1 +1.85000000e+00 1.51144597e-01 1.73844089e+00 -1 +1.90000000e+00 1.41849884e-01 1.75449113e+00 -1 +1.95000000e+00 1.33128239e-01 1.76955904e+00 -1 diff --git a/Tests/test_basic/test_basic.py b/Tests/test_basic/test_basic.py new file mode 100644 index 0000000..4cb4720 --- /dev/null +++ b/Tests/test_basic/test_basic.py @@ -0,0 +1,13 @@ +from Source.main import do_it +import numpy + +def train_basic(): + args = ['-f', 'test_basic/ex1.txt', '-m', '1000', '-N', 'test_basic/ex1', '-igs'] + return do_it(args) + +def test_basic(): + answer = numpy.array([[-1.8376, 0., 1.8376], [ 0., 0., 0. ], [ 0.6819, 2.7304, -3.3204]]) + result = train_basic() + diff = result - answer + vector = diff.reshape((9)) + assert numpy.linalg.norm(vector,numpy.inf) < 1.0e-3