diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..cf2a912 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,19 @@ +Contributions are welcome! + +If you have a new feature/bug report, make sure you create an [issue](https://github.com/htjb/maxsmooth/issues), and consult existing ones first, in case your suggestion is already being addressed. + +If you want to go ahead and create the feature yourself, you should fork the repository to you own github account and create a new branch with an appropriate name. Commit any code modifications to that branch, push to GitHub, and then create a pull request via your forked repository. + +## Contributing - `pre-commit` + +To try and maintain a consistent style the code base is using pre-commit, ruff and isort. If you are ready to contribute to `maxsmooth` please follow the instructions below before making a PR. + +First, ensure that pre-commit is installed: +``` +pip install pre-commit +``` +Then install the pre-commit to the .git folder: +``` +pre-commit install +``` +Before running `git commit` you should run `pre-commit run --files your_file` on any additional or changed files. This will check the code against ruff and isort and make any appropriate changes. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..603d467 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context and describe the tests that have been added (if any). + +Fixes # (issue) + +# Checklist: + +- [ ] I have performed a self-review of my own code +- [ ] New and existing unit tests pass locally with my changes (`python -m pytest`) +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have appropriately incremented the [semantic version number](https://semver.org/) in both README.rst and maxsmooth/_version.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d656046..b7ae4c8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,7 +15,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.12", "3.13"] + steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -25,14 +26,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest coverage - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # 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 + python -m pip install pytest coverage + pip install -e . - name: Test with pytest run: | coverage run --source=maxsmooth -m pytest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..57cc901 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tools + run: | + pip install "ruff==0.12.9" "isort==6.0.1" + + - name: Run Ruff (check mode) + run: ruff check . \ No newline at end of file diff --git a/.github/workflows/version_checks.yaml b/.github/workflows/version_checks.yaml new file mode 100644 index 0000000..f3c2eae --- /dev/null +++ b/.github/workflows/version_checks.yaml @@ -0,0 +1,18 @@ +name: Version_Checks + +on: + pull_request: + branches: [master] + +jobs: + version-checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + - name: Upgrade pip and install linters + run: | + python -m pip install --upgrade pip + python -m pip install packaging + - name: Check version number + run: python ./bin/check_version.py \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5d6484 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Sphinx documentation +docs/_build/ + +# Jupyter Notebook +.ipynb_checkpoints + +# OS +.DS_Store \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ea79323 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.12.9 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + args: [--line-length=79] + + - repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + args: ["--profile", "black"] \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml index 61b55e3..44221d2 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,18 +1,21 @@ # Required version: 2 -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/source/conf.py - fail_on_warning: false +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with MkDocs +mkdocs: + configuration: mkdocs.yaml -# Optionally build your docs in additional formats such as PDF and ePub -formats: - - htmlzip # Optionally set the version of Python and requirements required to build your docs python: - version: 3.8 install: - - requirements: docs/requirements.txt - - requirements: requirements.txt + - method : pip + path : . + extra_requirements : + - docs \ No newline at end of file diff --git a/Basin-hopping_Nelder-Mead/BHNM/msf_fit.py b/Basin-hopping_Nelder-Mead/BHNM/msf_fit.py deleted file mode 100644 index 967239c..0000000 --- a/Basin-hopping_Nelder-Mead/BHNM/msf_fit.py +++ /dev/null @@ -1,255 +0,0 @@ -import numpy as np -from scipy.optimize import minimize, basinhopping -import sys - - -class max_fit_BH(object): - def __init__(self, x, y, N, mid_point, **kwargs): - - self.step_size = kwargs.pop('step_size', 0.5) - self.temp = kwargs.pop('temp', 1) - self.interval = kwargs.pop('interval', 50) - self.normalisation = kwargs.pop('data_norm', True) - - if self.normalisation is True: - self.x = x/x.max() - self.true_x = x - self.y = y/y.std() + y.mean() / y.std() - self.true_y = y - else: - self.x = x - self.y = y - - self.N = N - self.mid_point = mid_point - - self.fit_result = self.fit() - - def fit(self): - - def func1(params): - y_sum = self.y[self.mid_point] * np.sum([ - params[i] * (self.x / self.x[self.mid_point])**i - for i in range(self.N)], axis=0) - message = 'test' - const = constraint(params, message) - if const == 1: - return np.sum((self.y - y_sum)**2) - if const == -1: - return np.sum((self.y - y_sum)**2)+1e80 - - def constraint(params, message): - - m = np.arange(0, self.N, 1) - deriv = [] - for j in range(len(m)): - if m[j] >= 2: - dif = [] - for i in range(self.N - m[j]): - if i <= (self.N - m[j]): - dif_m_bit = ( - self.y[self.mid_point] / - self.x[self.mid_point]) * \ - np.math.factorial(m[j]+i) / \ - np.math.factorial(i) * \ - params[m[j]+i]*(self.x)**i / \ - (self.x[self.mid_point])**(i+1) - dif.append(dif_m_bit) - dif = np.array(dif) - derivative = dif.sum(axis=0) - deriv.append([m[j], derivative]) - deriv = np.array(deriv) - derive = [] - for i in range(deriv.shape[0]): - derive.append(deriv[i, 1]) - derive = np.array(derive) - pass_fail = [] # 1==pass, 0==fail - for i in range(derive.shape[0]): - if np.any(derive[i, :] == 1e-7): - pass_fail.append(0) - elif np.any(derive[i, :] > -1e-7) and \ - np.any(derive[i, :] < 1e-7): - pass_fail.append(0) - else: - pass_fail.append(1) - pass_fail = np.array(pass_fail) - - if np.any(pass_fail == 0): - const = -1 # failed - else: - const = 1 # satisfied - - return const - - params0 = [(self.y[-1]-self.y[0])/2]*(self.N) - res = basinhopping( - func1, params0, niter=10000, niter_success=100*self.N, - stepsize=self.step_size, T=self.temp, interval=self.interval, - seed=1) - print('msf fit params', res) - - parameters = res.x.copy() - if self.normalisation is True: - for i in range(len(parameters)): - if i == 0: - parameters[i] = parameters[i] * \ - (1+self.true_y.mean()/self.true_y[self.mid_point]) \ - - self.true_y.mean()/(self.true_y[self.mid_point]) - else: - parameters[i] = parameters[i] * \ - (1+self.true_y.mean()/self.true_y[self.mid_point]) - - message = 'summary' - const = constraint(parameters, message) - if const != 1: - print('Error: condition violated') - sys.exit(1) - - def fitting(params): - if self.normalisation is True: - y_sum = self.true_y[self.mid_point]*np.sum([ - params[i]*(self.true_x/self.true_x[self.mid_point])**i - for i in range(self.N)], axis=0) - else: - y_sum = self.y[self.mid_point]*np.sum([ - params[i]*(self.x/self.x[self.mid_point])**i - for i in range(self.N)], axis=0) - return y_sum - - fitted_y = fitting(parameters) - if self.normalisation is True: - print('chi BH', np.sum((self.true_y-fitted_y)**2)) - else: - print('chi BH', np.sum((self.y-fitted_y)**2)) - - return res.x - - -class max_fit_NM(object): - def __init__(self, x, y, N, BH_params, mid_point, **kwargs): - - self.normalisation = kwargs.pop('data_norm', True) - - if self.normalisation is True: - self.x = x/x.max() - self.true_x = x - self.y = y/y.std() + y.mean()/y.std() - self.true_y = y - else: - self.x = x - self.y = y - - self.N = N - self.BH_params = BH_params - self.mid_point = mid_point - self.fit_result, self.fit_h, self.chi = self.fit() - - def fit(self): - print('-------------------------------------------------------------') - - def func1(params): - y_sum = self.y[self.mid_point]*np.sum([ - params[i]*(self.x/self.x[self.mid_point])**i - for i in range(self.N)], axis=0) - message = 'test' - const, h = constraint(params, message) - if const == 1: - return np.sum((self.y-y_sum)**2) - if const == -1: - return np.sum((self.y-y_sum)**2)+1e80 - - def constraint(params, message): - - m = np.arange(0, self.N, 1) - deriv = [] - for j in range(len(m)): - if m[j] >= 2: - dif = [] - for i in range(self.N-m[j]): - if i <= (self.N-m[j]): - dif_m_bit = ( - self.y[self.mid_point] / - self.x[self.mid_point]) * \ - np.math.factorial(m[j]+i) / \ - np.math.factorial(i) * \ - params[m[j]+i]*(self.x)**i / \ - (self.x[self.mid_point])**(i+1) - dif.append(dif_m_bit) - dif = np.array(dif) - derivative = dif.sum(axis=0) - deriv.append([m[j], derivative]) - deriv = np.array(deriv) - derive = [] - for i in range(deriv.shape[0]): - derive.append(deriv[i, 1]) - derive = np.array(derive) - if message == 'summary': - print('min derivative', derive.min()) - h = np.abs(derive).min()/2 - if message == 'test': - h = None - pass - pass_fail = [] # 1==pass, 0==fail - for i in range(derive.shape[0]): - if np.any(derive[i, :] == 1e-7): - pass_fail.append(0) - elif np.any(derive[i, :] > -1e-7) and \ - np.any(derive[i, :] < 1e-7): - pass_fail.append(0) - else: - pass_fail.append(1) - pass_fail = np.array(pass_fail) - - if np.any(pass_fail == 0): - const = -1 # failed - else: - const = 1 # satisfied - - return const, h - - def fitting(params): - if self.normalisation is True: - y_sum = self.true_y[self.mid_point]*np.sum([ - params[i]*(self.true_x/self.true_x[self.mid_point])**i - for i in range(self.N)], axis=0) - else: - y_sum = self.y[self.mid_point]*np.sum([ - params[i]*(self.x/self.x[self.mid_point])**i - for i in range(self.N)], axis=0) - return y_sum - - params0 = self.BH_params - - res = minimize( - func1, params0, - options={ - 'maxiter': 100000, 'adaptive': True, 'fatol': 1e-7, - 'xatol': 1e-7}, method='Nelder-Mead') - print(res) - - parameters = res.x - if self.normalisation is True: - for i in range(len(parameters)): - if i == 0: - parameters[i] = parameters[i] * \ - (1+self.true_y.mean()/self.true_y[self.mid_point]) \ - - self.true_y.mean()/(self.true_y[self.mid_point]) - else: - parameters[i] = parameters[i] * \ - (1+self.true_y.mean()/self.true_y[self.mid_point]) - - fitted_y = fitting(parameters) - if self.normalisation is True: - print('chi NM', np.sum((self.true_y-fitted_y)**2)) - chi = np.sum((self.true_y-fitted_y)**2) - else: - print('chi NM', np.sum((self.y-fitted_y)**2)) - chi = np.sum((self.y-fitted_y)**2) - - message = 'summary' - const, h = constraint(parameters, message) - if const != 1: - print('Error: condition violated') - sys.exit(1) - - return parameters, h, chi diff --git a/Basin-hopping_Nelder-Mead/times_chis.py b/Basin-hopping_Nelder-Mead/times_chis.py deleted file mode 100644 index bc8f232..0000000 --- a/Basin-hopping_Nelder-Mead/times_chis.py +++ /dev/null @@ -1,120 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from maxsmooth.DCF import smooth -from BHNM.msf_fit import max_fit_BH, max_fit_NM -import time - - -def fit(params, mid_point, x, y, N): - y_sum = y[mid_point]*np.sum([ - params[i]*(x/x[mid_point])**i - for i in range(N)], axis=0) - return y_sum - - -"""x = np.linspace(1, 5, 100) -noise = np.random.normal(0,0.5,len(x)) -y = 0.6+2*x+4*x**(3)+9*x**(4)+noise""" - -x = np.loadtxt('x_data_for_comp.txt') -y = np.loadtxt('y_data_for_comp.txt') - -N = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] - -# Testing all signs -times_qp = [] -chi_qp = [] -for i in range(len(N)): - s = time.time() - result = smooth( - x, y, N[i], model_type='normalised_polynomial', fit_type='qp') - e = time.time() - times_qp.append(e-s) - chi_qp.append(result.optimum_chi) - -# Using the maxsmooth sign navigating algorithm -times_qpsf = [] -chi_qpsf = [] -for i in range(len(N)): - s = time.time() - result = smooth(x, y, N[i], model_type='normalised_polynomial') - e = time.time() - times_qpsf.append(e-s) - chi_qpsf.append(result.optimum_chi) - -np.savetxt('times_qp_for_comp.txt', times_qp) -np.savetxt('times_qpsf_for_comp.txt', times_qpsf) -np.savetxt('chi_qp_for_comp.txt', chi_qp) -np.savetxt('chi_qpsf_for_comp.txt', chi_qpsf) - -# Basin-hopping followed by a Nelder-Mead routine -N_BHNM = np.arange(3, 8, 1) -mid_point = len(x)//2 -BHNM_chis_defualt = [] -BHNM_times_defualt = [] -h = [] -for i in range(len(N_BHNM)): - s = time.time() - BH = max_fit_BH( - x, y, N_BHNM[i], mid_point, step_size=10*N_BHNM[i], - interval=50, temp=5 * N_BHNM[i]) - NM = max_fit_NM(x, y, N_BHNM[i], BH.fit_result, mid_point) - fitted_y = fit(NM.fit_result, mid_point, x, y, N_BHNM[i]) - e = time.time() - print( - 'N:', N_BHNM[i], 'TIME:', e-s, 'CHI_RATIO:', - np.sum((y-fitted_y)**2)/chi_qpsf[i]) - print('CHI_BHNM:', np.sum((y-fitted_y)**2), 'CHI_QP:', chi_qpsf[i]) - chi = np.sum((y-fitted_y)**2) - BHNM_chis_defualt.append(chi) - BHNM_times_defualt.append(e-s) - -np.save('BHNM_times.npy', BHNM_times_defualt) -np.save('BHNM_chis.npy', BHNM_chis_defualt) - -chi_qp_plotting = [] -for i in range(len(N)): - if N[i] <= N_BHNM.max(): - chi_qp_plotting.append(chi_qpsf[i]) - -ratio = [] -for i in range(len(BHNM_chis_defualt)): - ratio.append(BHNM_chis_defualt[i]/chi_qp_plotting[i]) - -plt.subplot(111) -plt.plot(N_BHNM, ratio, ls='-', c='k') -plt.xlabel('N', fontsize=12) -plt.ylabel( - r'$\frac{X^2_{Basinhopping + Nelder-Mead}}{X^2_{QP Sampling Sign Space}}$', - fontsize=12) -plt.tight_layout() -plt.savefig('chi_ratio.pdf') -plt.close() - -plt.subplot(111) -plt.plot(N, times_qp, ls='-', label='QP Testing All Sign Combinations', c='g') -plt.plot(N, times_qpsf, ls='-', label='QP Sampling Sign Space', c='k') -plt.plot( - N_BHNM, BHNM_times_defualt, - ls='-', label='Basinhopping + Nelder-Mead\n', c='r') -plt.xlabel('N', fontsize=12) -plt.ylabel(r'$t$ [s]', fontsize=12) -plt.yscale('log') -plt.legend(loc=0) -plt.tight_layout() -plt.savefig('Times.pdf') -plt.close() - -plt.subplot(111) -plt.plot(N, chi_qp, ls='-', label='QP Testing All Sign Combinations', c='g') -plt.plot(N, chi_qpsf, ls='-', label='QP Sampling Sign Space', c='k') -plt.plot( - N_BHNM, BHNM_chis_defualt, - ls='-', label='Basinhopping + Nelder-Mead', c='r') -plt.xlabel('N', fontsize=12) -plt.ylabel(r'$X^2$', fontsize=12) -plt.yscale('log') -plt.legend(loc=0) -plt.tight_layout() -plt.savefig('chi.pdf') -plt.close() diff --git a/Basin-hopping_Nelder-Mead/x_data_for_comp.txt b/Basin-hopping_Nelder-Mead/x_data_for_comp.txt deleted file mode 100644 index 9f718a4..0000000 --- a/Basin-hopping_Nelder-Mead/x_data_for_comp.txt +++ /dev/null @@ -1,100 +0,0 @@ -1.000000000000000000e+00 -1.040404040404040442e+00 -1.080808080808080884e+00 -1.121212121212121104e+00 -1.161616161616161547e+00 -1.202020202020201989e+00 -1.242424242424242431e+00 -1.282828282828282873e+00 -1.323232323232323315e+00 -1.363636363636363757e+00 -1.404040404040403978e+00 -1.444444444444444420e+00 -1.484848484848484862e+00 -1.525252525252525304e+00 -1.565656565656565746e+00 -1.606060606060605966e+00 -1.646464646464646631e+00 -1.686868686868686851e+00 -1.727272727272727293e+00 -1.767676767676767735e+00 -1.808080808080808177e+00 -1.848484848484848619e+00 -1.888888888888888840e+00 -1.929292929292929504e+00 -1.969696969696969724e+00 -2.010101010101010388e+00 -2.050505050505050608e+00 -2.090909090909090828e+00 -2.131313131313131493e+00 -2.171717171717171713e+00 -2.212121212121211933e+00 -2.252525252525252597e+00 -2.292929292929293261e+00 -2.333333333333333481e+00 -2.373737373737373701e+00 -2.414141414141414366e+00 -2.454545454545454586e+00 -2.494949494949494806e+00 -2.535353535353535470e+00 -2.575757575757576134e+00 -2.616161616161616355e+00 -2.656565656565656575e+00 -2.696969696969697239e+00 -2.737373737373737459e+00 -2.777777777777777679e+00 -2.818181818181818343e+00 -2.858585858585859008e+00 -2.898989898989899228e+00 -2.939393939393939448e+00 -2.979797979797980112e+00 -3.020202020202020332e+00 -3.060606060606060996e+00 -3.101010101010101216e+00 -3.141414141414141437e+00 -3.181818181818182101e+00 -3.222222222222222321e+00 -3.262626262626262985e+00 -3.303030303030303205e+00 -3.343434343434343425e+00 -3.383838383838384090e+00 -3.424242424242424310e+00 -3.464646464646464974e+00 -3.505050505050505194e+00 -3.545454545454545858e+00 -3.585858585858586078e+00 -3.626262626262626299e+00 -3.666666666666666963e+00 -3.707070707070707183e+00 -3.747474747474747847e+00 -3.787878787878788067e+00 -3.828282828282828731e+00 -3.868686868686868952e+00 -3.909090909090909172e+00 -3.949494949494949836e+00 -3.989898989898990056e+00 -4.030303030303031164e+00 -4.070707070707070940e+00 -4.111111111111110716e+00 -4.151515151515152269e+00 -4.191919191919192045e+00 -4.232323232323232709e+00 -4.272727272727273373e+00 -4.313131313131313149e+00 -4.353535353535353813e+00 -4.393939393939394478e+00 -4.434343434343434254e+00 -4.474747474747474918e+00 -4.515151515151515582e+00 -4.555555555555555358e+00 -4.595959595959596911e+00 -4.636363636363636687e+00 -4.676767676767676463e+00 -4.717171717171718015e+00 -4.757575757575757791e+00 -4.797979797979798455e+00 -4.838383838383839120e+00 -4.878787878787878896e+00 -4.919191919191919560e+00 -4.959595959595960224e+00 -5.000000000000000000e+00 diff --git a/Basin-hopping_Nelder-Mead/y_data_for_comp.txt b/Basin-hopping_Nelder-Mead/y_data_for_comp.txt deleted file mode 100644 index 0b4c8c2..0000000 --- a/Basin-hopping_Nelder-Mead/y_data_for_comp.txt +++ /dev/null @@ -1,100 +0,0 @@ -1.538480190059443231e+01 -1.803994844410931719e+01 -2.074192622407534614e+01 -2.340100984140073592e+01 -2.621510063558880432e+01 -2.887939230577280370e+01 -3.267760037624015723e+01 -3.624293586100588982e+01 -4.183230771978232099e+01 -4.472484203235710254e+01 -4.901901109503933185e+01 -5.458384801381982498e+01 -6.044395282904604727e+01 -6.710163163497058747e+01 -7.331305934512305100e+01 -8.038417701150292771e+01 -8.828937593277244389e+01 -9.658444786378277058e+01 -1.042045848601506322e+02 -1.146416307471176879e+02 -1.235112892305587451e+02 -1.345268586166006060e+02 -1.450430823504431430e+02 -1.581643332442427266e+02 -1.717913514750528066e+02 -1.835712719659944128e+02 -1.979150031939569203e+02 -2.139361084172272456e+02 -2.295680700846963020e+02 -2.465373692074055327e+02 -2.641143352719346922e+02 -2.831127401702224233e+02 -3.022997542458672910e+02 -3.228565015249954513e+02 -3.445193178888176249e+02 -3.676949607788102412e+02 -3.913593215276684418e+02 -4.168242323983324695e+02 -4.426368502745826845e+02 -4.702519040672153210e+02 -4.999275768833090865e+02 -5.289023404506694988e+02 -5.603229923493009892e+02 -5.931106443932800403e+02 -6.271303491261907084e+02 -6.633693290327342993e+02 -7.017142035218076899e+02 -7.397376073564449825e+02 -7.806386443959225971e+02 -8.228136223978054886e+02 -8.656723069434885929e+02 -9.102137208256813210e+02 -9.580326031355238001e+02 -1.007291992119634074e+03 -1.058029537451610622e+03 -1.111047519335722654e+03 -1.166443910326044715e+03 -1.222809821319707908e+03 -1.281110152112206606e+03 -1.342530443746999026e+03 -1.405175264221236603e+03 -1.471403152526458143e+03 -1.536878719515193097e+03 -1.608101102592653660e+03 -1.679812150650684544e+03 -1.754709199343822320e+03 -1.832464343650941828e+03 -1.911709046912803842e+03 -1.994382101504000275e+03 -2.078654055364936994e+03 -2.166598274438367753e+03 -2.255622982675686217e+03 -2.348654835621773600e+03 -2.444463611754800695e+03 -2.543845966417291038e+03 -2.645305246365588118e+03 -2.749656581282604748e+03 -2.858029640524120168e+03 -2.968665196804036896e+03 -3.083208241631706187e+03 -3.198777704047548468e+03 -3.320655724687552720e+03 -3.444593388278271505e+03 -3.572310906936728770e+03 -3.704479259388221635e+03 -3.838077949702294973e+03 -3.975746853596534947e+03 -4.118423306260904610e+03 -4.263847070864699162e+03 -4.413647032393576410e+03 -4.567425210209468787e+03 -4.724884307810286373e+03 -4.886477595104389366e+03 -5.051962838680952700e+03 -5.220691820972367168e+03 -5.395668600079392490e+03 -5.574189572375433272e+03 -5.756885988219393766e+03 -5.943798781023513584e+03 -6.134840236402888877e+03 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfba632 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# maxsmooth: Derivative Constrained Function Fitting + +## Introduction + +**maxsmooth:** Derivative Constrained Function Fitting +**Author:** Harry Thomas Jones Bevins +**Version:** 2.0.0 +**Homepage:** https://github.com/htjb/maxsmooth +**Documentation:** https://maxsmooth.readthedocs.io/ + +![github CI](https://github.com/htjb/maxsmooth/workflows/CI/badge.svg?event=push) +![Test Coverage Status](https://codecov.io/gh/htjb/maxsmooth/branch/master/graph/badge.svg) +![Documentation Status](https://readthedocs.org/projects/maxsmooth/badge/?version=latest) +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![PyPI](https://pypi.in/v/maxsmooth/badge.svg) +![ASCL](https://img.shields.io/badge/ascl-2008.018-blue.svg?colorB=262255) +![JOSS](https://joss.theoj.org/papers/7f53a67e2a3e8f021d4324de96fb59c8/status.svg) + +## Installation + +The software can be pip installed from the PYPI repository like so: + +```bash +pip install maxsmooth +``` + +or alternatively it can be installed from the git repository via: + +```bash +git clone https://github.com/htjb/maxsmooth.git +cd maxsmooth +pip install . +``` + +## Derivative Constrained Functions and `maxsmooth` + +`maxsmooth` is open source Python software (Python 3+) for fitting derivative constrained functions (DCFs) such as Maximally Smooth Functions (MSFs) to data sets. + +MSFs are functions with no zero crossings in derivatives of order m ≥ 2 within the domain of interest. More generally, for DCFs the minimum constrained derivative order m can take on any value or a set of specific higher order derivatives may be constrained. + +They are designed to prevent the loss of signals when fitting out dominant smooth foregrounds or large magnitude signals that mask signals of interest. “Smooth” here refers to foregrounds that follow power-law structures in the band of interest. In some cases DCFs can highlight systematics in the data. + +`maxsmooth` uses quadratic programming via `CVXOPT` to fit data subject to a fixed linear constraint, Ga ≤ 0, where Ga is a matrix of derivatives. The constraint on an MSF is not explicitly linear and each constrained derivative can be positive or negative. `maxsmooth` tests the ≤ 0 constraint multiplied by a positive or negative sign. + +For an Nth-order polynomial `maxsmooth` can test every available sign combination, but by default it implements a sign navigating algorithm. This is detailed in the `maxsmooth` paper and summarized in the software documentation. + +The available sign combinations act as discrete parameter spaces all with global minima, and `maxsmooth` is capable of finding the minimum of these by cascading followed by directional exploration. The searched region is limited to capture enough of the neighbourhood to confidently return the global minimum. + +The method relies on the problem being “well defined”, but not all problems satisfy this. In such cases it is possible to test every available sign combination on the constrained derivatives. See the paper for definitions. + +`maxsmooth` features a built-in library of DCFs and allows users to define their own. Inflection points and zero crossings in higher order derivatives can also be included. + +## Licence and Citation + +The software is free to use under the MIT open source license. For academic use please cite the `maxsmooth` papers: + +**MNRAS Paper (the “maxsmooth paper”):** + +> H. T. J. Bevins et al., *maxsmooth: Rapid maximally smooth function fitting with applications in Global 21-cm cosmology*, MNRAS, 2021, stab152. https://doi.org/10.1093/mnras/stab152 + +BibTeX: + +```bibtex +@article{10.1093/mnras/stab152, + author = {Bevins, H T J and Handley, W J and Fialkov, A and Acedo, E de Lera and Greenhill, L J and Price, D C}, + title = "{maxsmooth: rapid maximally smooth function fitting with applications in Global 21-cm cosmology}", + journal = {Monthly Notices of the Royal Astronomical Society}, + year = {2021}, + month = {01}, + issn = {0035-8711}, + doi = {10.1093/mnras/stab152}, + url = {https://doi.org/10.1093/mnras/stab152}, + note = {stab152}, + eprint = {https://academic.oup.com/mnras/advance-article-pdf/doi/10.1093/mnras/stab152/35931358/stab152.pdf}, +} +``` + +**JOSS Paper:** + +> Bevins, H. T., (2020). *maxsmooth: Derivative Constrained Function Fitting*, Journal of Open Source Software, 5(54), 2596. https://doi.org/10.21105/joss.02596 + +BibTeX: + +```bibtex +@article{Bevins2020, + doi = {10.21105/joss.02596}, + url = {https://doi.org/10.21105/joss.02596}, + year = {2020}, + publisher = {The Open Journal}, + volume = {5}, + number = {54}, + pages = {2596}, + author = {Harry T. j. Bevins}, + title = {maxsmooth: Derivative Constrained Function Fitting}, + journal = {Journal of Open Source Software} +} +``` + +## Contributing + +Contributions to `maxsmooth` are welcome via: + +- Opening an issue for new features or bugs. +- Submitting a pull request (ideally with prior discussion). + +## Documentation + +Documentation: https://maxsmooth.readthedocs.io/ + +You can also compile it locally by running + +```bash +pip install ".[docs]" +mkdocs serve +``` + diff --git a/README.rst b/README.rst deleted file mode 100644 index 4e07d20..0000000 --- a/README.rst +++ /dev/null @@ -1,260 +0,0 @@ -================================================== -maxsmooth: Derivative Constrained Function Fitting -================================================== - - A jax version of maxsmooth is being developed in a - separate `branch `__. - -Introduction ------------- - -:maxsmooth: Derivative Constrained Function Fitting -:Author: Harry Thomas Jones Bevins -:Version: 1.2.3 -:Homepage: https://github.com/htjb/maxsmooth -:Documentation: https://maxsmooth.readthedocs.io/ - -.. image:: https://github.com/htjb/maxsmooth/workflows/CI/badge.svg?event=push - :target: https://github.com/htjb/maxsmooth/actions - :alt: github CI -.. image:: https://codecov.io/gh/htjb/maxsmooth/branch/master/graph/badge.svg - :target: https://codecov.io/gh/htjb/maxsmooth - :alt: Test Coverage Status -.. image:: https://readthedocs.org/projects/maxsmooth/badge/?version=latest - :target: https://maxsmooth.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status -.. image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: https://github.com/htjb/maxsmooth/blob/master/LICENSE - :alt: License information -.. image:: https://pypip.in/v/maxsmooth/badge.svg - :target: https://pypi.org/project/maxsmooth/#description - :alt: Latest PyPI version -.. image:: https://img.shields.io/badge/ascl-2008.018-blue.svg?colorB=262255 - :target: http://ascl.net/2008.018 - :alt: Astrophysics Source Code Library -.. image:: https://joss.theoj.org/papers/7f53a67e2a3e8f021d4324de96fb59c8/status.svg - :target: https://joss.theoj.org/papers/7f53a67e2a3e8f021d4324de96fb59c8 - :alt: JOSS paper -.. image:: https://mybinder.org/badge_logo.svg - :target: https://mybinder.org/v2/gh/htjb/maxsmooth/master?filepath=example_notebooks%2F -.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4059339.svg - :target: https://doi.org/10.5281/zenodo.4059339 - -Installation -~~~~~~~~~~~~ -In the following two sections we highlight the purpose of ``maxsmooth`` and -show an example. To install the software follow these instructions: - -The software can be pip installed from the PYPI repository like so, - -.. code:: - - pip install maxsmooth - -or alternatively it can be installed from the git repository via, - -.. code:: - - git clone https://github.com/htjb/maxsmooth.git - cd maxsmooth - python setup.py install --user - -Derivative Constrained Functions and ``maxsmooth`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``maxsmooth`` is an open source software, written in Python (supporting version 3 upwards), -for fitting derivative constrained -functions (DCFs) such as Maximally Smooth Functions -(MSFs) to data sets. MSFs are functions for which there are no zero -crossings in derivatives of order m >= 2 within the domain of interest. -More generally for DCFs the minimum -constrained derivative order, m can take on any value or a set of -specific high order derivatives can be constrained. -They are designed to prevent the loss of -signals when fitting out dominant smooth foregrounds or large magnitude signals that -mask signals of interest. Here "smooth" means that the foregrounds follow power -law structures in the band of interest. -In some cases DCFs can be used to -highlight systematics in the data. - -``maxsmooth`` uses quadratic programming implemented with ``CVXOPT`` to fit -data subject to a fixed linear constraint, Ga <= 0, where the product -Ga is a matrix of derivatives. -The constraint on an MSF are not explicitly -linear and each constrained derivative can be positive or negative. -``maxsmooth`` is, however, designed to test the <= 0 constraint multiplied -by a positive or negative sign. Where a positive sign in front of the m\ :sup:`th` -order derivative forces the derivative -to be negative for all x. For an N\ :sup:`th` order polynomial ``maxsmooth`` can test -every available sign combination but by default it implements a sign navigating algorithm. -This is detailed in the ``maxsmooth`` paper (see citation), is summarized -below and in the software documentation. - -The available sign combinations act as discrete parameter spaces all with -global minima and ``maxsmooth`` is capable of finding the minimum of these global -minima by implementing a cascading algorithm which is followed by a directional -exploration. The cascading routine typically finds an approximate to the global -minimum and then the directional exploration is a complete search -of the sign combinations in the neighbourhood -of that minimum. The searched region is limited by factors -that encapsulate enough of the neighbourhood to confidently return the global minimum. - -The sign navigating method is reliant on the problem being "well defined" but this -is not always the case and it is in these instances it is possible to run the code testing -every available sign combination on the constrained derivatives. For a definition of -a "well defined" problem and it's counter part see the ``maxsmooth`` paper and the -documentation. - -``maxsmooth`` features a built in library of DCFs or -allows the user to define their own. The addition of possible inflection points -and zero crossings in higher order derivatives is also available to the user. -The software has been designed with these two -applications in mind and is a simple interface. - -Example Fit -~~~~~~~~~~~ - -Shown below is an example MSF fit performed with ``maxsmooth`` to data that -follows a y = x\ :sup:`-2.5` power law with a randomly generated Gaussian -noise with a standard deviation 0.02. The top panel shows the data and the -bottom panel shows the residual -after subtraction of the MSF fit alongside the actual noise in the data. -The software using the default built-in DCF model is shown to be -capable of recovering the random noise. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/README.png - :width: 400 - :align: center - -Further examples can be found in the Documentation (https://maxsmooth.readthedocs.io/) -and in the github repository in the files 'example_codes/' and -'example_notebooks/' (notebooks can also be accessed online -`here `__). - -Licence and Citation -~~~~~~~~~~~~~~~~~~~~ - -The software is free to use on the MIT open source license. However if you use -the software for academic purposes we request that you cite the ``maxsmooth`` -papers. They are detailed below. - -MNRAS paper (referred to throughout the documentation as the ``maxsmooth`` -paper), - - H. T. J. Bevins et al., `maxsmooth: Rapid maximally smooth function fitting with - applications in Global 21-cm cosmology `__, - Monthly Notices of the Royal Astronomical Society, 2021;, stab152, https://doi.org/10.1093/mnras/stab152 - -Below is the BibTex citation, - -.. code:: bibtex - - @article{10.1093/mnras/stab152, - author = {Bevins, H T J and Handley, W J and Fialkov, A and Acedo, E de Lera and Greenhill, L J and Price, D C}, - title = "{maxsmooth: rapid maximally smooth function fitting with applications in Global 21-cm cosmology}", - journal = {Monthly Notices of the Royal Astronomical Society}, - year = {2021}, - month = {01}, - issn = {0035-8711}, - doi = {10.1093/mnras/stab152}, - url = {https://doi.org/10.1093/mnras/stab152}, - note = {stab152}, - eprint = {https://academic.oup.com/mnras/advance-article-pdf/doi/10.1093/mnras/stab152/35931358/stab152.pdf}, - } - -JOSS paper, - - Bevins, H. T., (2020). maxsmooth: Derivative Constrained Function Fitting. Journal of Open Source Software, 5(54), 2596, https://doi.org/10.21105/joss.02596 - -and the BibTex, - -.. code:: bibtex - - @article{Bevins2020, - doi = {10.21105/joss.02596}, - url = {https://doi.org/10.21105/joss.02596}, - year = {2020}, - publisher = {The Open Journal}, - volume = {5}, - number = {54}, - pages = {2596}, - author = {Harry T. j. Bevins}, - title = {maxsmooth: Derivative Constrained Function Fitting}, - journal = {Journal of Open Source Software} - } - - -Contributing -~~~~~~~~~~~~ - -Contributions to ``maxsmooth`` are welcome and can be made via: - -- Opening an issue to purpose new features/report bugs. -- Making a pull request. Please consider opening an issue to discuss - any proposals beforehand and ensure that your PR will be accepted. - -An example contribution may be the addition of a basis function into the -standard library. - -Documentation -~~~~~~~~~~~~~ -The documentation is available at: https://maxsmooth.readthedocs.io/ - -Alternatively, it can be compiled locally from the git repository and requires -`sphinx `__ to be installed. -You can do this via: - -.. code:: - - cd docs/ - make SOURCEDIR=source html - -or - -.. code:: - - cd docs/ - make SOURCEDIR=source latexpdf - -The resultant docs can be found in the docs/_build/html/ and docs/_build/latex/ -respectively. - -Requirements -~~~~~~~~~~~~ - -To run the code you will need the following additional packages: - -- `matplotlib `__ -- `numpy `__ -- `CVXOPT `__ -- `scipy `__ -- `progressbar `__ - -When installing via pip or from source using the setup.py file -the above packages will also be installed if absent. - -To compile the documentation locally you will need: - -- `sphinx `__ -- `numpydoc `__ - -To run the test suit you will need: - -- `pytest `__ - -Basin-hopping/Nelder-Mead Code -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the ``maxsmooth`` MNRAS paper and JOSS paper we provide a comparison of -``maxsmooth`` to a Basin-hopping/Nelder-Mead approach for fitting DCFs. For -completeness we provide in this repo the code used to make this comparison -in the file 'Basin-hopping_Nelder_Mead/'. - -The code times_chis.py is used to call ``maxsmooth`` and the Basin-hopping -methods (in the file 'BHNM/'). It will plot the recorded times and objective -function evaluations. - -The Basin-hopping/Nelder-Mead code is designed to fit MSFs and is not -generalised to all types of DCF. It is also not documented, however there are -minor comments in the script and it should be self explanatory. Questions -on this are welcome and can be posted as an issue or by contacting the author. diff --git a/bin/check_version.py b/bin/check_version.py new file mode 100644 index 0000000..3d87d16 --- /dev/null +++ b/bin/check_version.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +"""Check Version. + +Verify version has been incremented. +""" + +import subprocess +import sys + +from packaging import version + +# Filestructure +readme_file = "README.md" + + +# Utility functions +def run_on_commandline(*args: dict) -> str: + """Run the given arguments as a command on the command line.""" + return subprocess.run(args, text=True, capture_output=True).stdout + + +def unit_incremented(version_a: str, version_b: str) -> bool: + """Check if version_a is one version larger than version_b. + + Parameters + ---------- + version_a: str + New version, version number + version_b: str + Old version, version number + + Returns: + ------- + unit_incremented: bool + Whether, version number has been unit incremented + """ + # Convert to version objects + version_a = version.parse(version_a) + version_b = version.parse(version_b) + + # Check increment for pre-release versions + if version_a.pre is not None and version_b.pre is not None: + # Matching pre-release levels + if version_a.pre[0] == version_b.pre[0]: + return (version_a.pre[1] == (int(version_b.pre[1]) + 1)) and ( + version_a.base_version == version_b.base_version + ) + # Differing pre-release levels + else: + return ( + version_a.pre[1] == 0 + and version_a.pre[0] > version_b.pre[0] + and version_a.base_version == version_b.base_version + ) + + # New pre-release level + elif version_a.pre is not None: + return ( + version_a.base_version > version_b.base_version + and version_a.pre[1] == 0 + ) + + # Full release + elif version_b.pre is not None: + return version_a.base_version == version_b.base_version + + # Standard version major, minor and micro increments + else: + return ( + version_a.micro == version_b.micro + 1 + and version_a.minor == version_b.minor + and version_a.major == version_b.major + or version_a.micro == 0 + and version_a.minor == version_b.minor + 1 + and version_a.major == version_b.major + or version_a.micro == 0 + and version_a.minor == 0 + and version_a.major == version_b.major + 1 + ) + + +def get_current_version() -> str: + """Get current version of package from README.md.""" + current_version = run_on_commandline("grep", "Version:", readme_file) + current_version = current_version.split("**")[-1].strip() + return current_version + + +def main() -> None: + """Check version is consistent and incremented correctly.""" + # Get current version from readme + current_version = get_current_version() + + # Get previous version from main branch of code + run_on_commandline("git", "fetch", "origin", "master") + readme_contents = run_on_commandline( + "git", "show", "remotes/origin/master:" + readme_file + ) + + previous_version = None + for line in readme_contents.splitlines(): + if "Version:" in line: + previous_version = line.split("**")[-1].strip() + break + + if previous_version is None: + print("Could not find version in README.md on master branch") + print("Trying README.rst...") + readme_rst = "README.rst" + readme_contents = run_on_commandline( + "git", "show", "remotes/origin/master:" + readme_rst + ) + for line in readme_contents.splitlines(): + if ":Version:" in line: + previous_version = line.split(":")[-1].strip() + break + if previous_version is None: + sys.stderr.write( + "Could not find version in README.md" + + "or README.rst on master branch.\n" + ) + sys.exit(1) + + # Check versions have been incremented + if not unit_incremented(current_version, previous_version): + sys.stderr.write( + "Version must be incremented by one:\n" + f"HEAD: {current_version},\n" + f"master: {previous_version}.\n" + ) + sys.exit(1) + + # No issues found, exit happily :) + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100755 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..2b0735d --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,9 @@ +# API Reference + +::: maxsmooth.qp + +::: maxsmooth.utils + +::: maxsmooth.models + +::: maxsmooth.derivatives \ No newline at end of file diff --git a/docs/images/Basis_functions.png b/docs/images/Basis_functions.png deleted file mode 100644 index 89f35d0..0000000 Binary files a/docs/images/Basis_functions.png and /dev/null differ diff --git a/docs/images/Gradients_fits.png b/docs/images/Gradients_fits.png deleted file mode 100644 index 0a71684..0000000 Binary files a/docs/images/Gradients_fits.png and /dev/null differ diff --git a/docs/images/Parameter_plot.png b/docs/images/Parameter_plot.png deleted file mode 100644 index e568885..0000000 Binary files a/docs/images/Parameter_plot.png and /dev/null differ diff --git a/docs/images/Parameter_plot_extended.png b/docs/images/Parameter_plot_extended.png deleted file mode 100644 index 3939d2f..0000000 Binary files a/docs/images/Parameter_plot_extended.png and /dev/null differ diff --git a/docs/images/README.png b/docs/images/README.png deleted file mode 100644 index 88acbdd..0000000 Binary files a/docs/images/README.png and /dev/null differ diff --git a/docs/images/chi_dist_theory.png b/docs/images/chi_dist_theory.png deleted file mode 100644 index 4d1812c..0000000 Binary files a/docs/images/chi_dist_theory.png and /dev/null differ diff --git a/docs/images/chi_distribution.png b/docs/images/chi_distribution.png deleted file mode 100644 index 889b4b8..0000000 Binary files a/docs/images/chi_distribution.png and /dev/null differ diff --git a/docs/images/combined_chi.png b/docs/images/combined_chi.png deleted file mode 100644 index f28f6b2..0000000 Binary files a/docs/images/combined_chi.png and /dev/null differ diff --git a/docs/images/inflection_point_example.png b/docs/images/inflection_point_example.png deleted file mode 100644 index 6f1da43..0000000 Binary files a/docs/images/inflection_point_example.png and /dev/null differ diff --git a/docs/images/inflection_point_example_fits.png b/docs/images/inflection_point_example_fits.png deleted file mode 100644 index 03dbc17..0000000 Binary files a/docs/images/inflection_point_example_fits.png and /dev/null differ diff --git a/docs/images/inflection_point_example_res.png b/docs/images/inflection_point_example_res.png deleted file mode 100644 index 8949003..0000000 Binary files a/docs/images/inflection_point_example_res.png and /dev/null differ diff --git a/docs/images/routine.png b/docs/images/routine.png deleted file mode 100644 index 7424275..0000000 Binary files a/docs/images/routine.png and /dev/null differ diff --git a/docs/images/simple_program_csf_residuals.png b/docs/images/simple_program_csf_residuals.png deleted file mode 100644 index 0659e55..0000000 Binary files a/docs/images/simple_program_csf_residuals.png and /dev/null differ diff --git a/docs/images/simple_program_data.png b/docs/images/simple_program_data.png deleted file mode 100644 index f15ba39..0000000 Binary files a/docs/images/simple_program_data.png and /dev/null differ diff --git a/docs/images/simple_program_msf_residuals.png b/docs/images/simple_program_msf_residuals.png deleted file mode 100644 index e0a7174..0000000 Binary files a/docs/images/simple_program_msf_residuals.png and /dev/null differ diff --git a/docs/images/simple_program_psf1_residuals.png b/docs/images/simple_program_psf1_residuals.png deleted file mode 100644 index f66baa3..0000000 Binary files a/docs/images/simple_program_psf1_residuals.png and /dev/null differ diff --git a/docs/images/simple_program_psf2_residuals.png b/docs/images/simple_program_psf2_residuals.png deleted file mode 100644 index a0a5816..0000000 Binary files a/docs/images/simple_program_psf2_residuals.png and /dev/null differ diff --git a/docs/images/turning_point_example.png b/docs/images/turning_point_example.png deleted file mode 100644 index 34c9cc0..0000000 Binary files a/docs/images/turning_point_example.png and /dev/null differ diff --git a/docs/images/turning_point_example_res.png b/docs/images/turning_point_example_res.png deleted file mode 100644 index fd2ac37..0000000 Binary files a/docs/images/turning_point_example_res.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..96d83c6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +{% include-markdown "../README.md" %} \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100755 index 2119f51..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index b0b66d3..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx -sphinx_rtd_theme -numpydoc==1.1.0 diff --git a/docs/source/CHANGELOG.rst b/docs/source/CHANGELOG.rst deleted file mode 100644 index ac1a9ce..0000000 --- a/docs/source/CHANGELOG.rst +++ /dev/null @@ -1,22 +0,0 @@ -Unreleased changes are not yet included in the pip install but are pushed to the -github. - -Unrealeased -~~~~~~~~~~~ - -Version 1.1.0 -~~~~~~~~~~~~~ - -- Two bug fixes in param_plotter() -- Extension of param_plotter() function to plot data, fit and residuals - alongside the parameter space if required. -- Extension of param_plotter() to allow for highlighting of central - regions in each panel if required. -- Inclusion of some theory into the documentation - -Version 1.2.0 -~~~~~~~~~~~~~ - -- Minor bug fix in param_plotter() -- Extension of the basis_test() function to allow users to compare different - types of DCF not just MSFs. diff --git a/docs/source/best_basis.rst b/docs/source/best_basis.rst deleted file mode 100644 index 088ff58..0000000 --- a/docs/source/best_basis.rst +++ /dev/null @@ -1,45 +0,0 @@ -This function can be used to identify which of the built in DCFs -fits the data best before running joint fits. - -To use it we begin by loading in the data, - -.. code:: - - import numpy as np - - x = np.load('Data/x.npy') - y = np.load('Data/y.npy') - -and then importing the basis_test() function. - -.. code:: - - from maxsmooth.best_basis import basis_test - -To call the function we use, - -.. code:: - - basis_test(x, y, base_dir='examples/', N=np.arange(3, 16, 1)) - -The function only requires the data but we can provide it with a base directory, -fit type and range of DCF orders to test. By defualt it uses the sign navigating -algorithm and tests :math:`{N = 3 - 13}`. Here we test the range -:math:``{N = 3 - 15}``. -The resultant graph is saved in the -base directory and the example generated here is shown below. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/Basis_functions.png - :width: 400 - :align: center - -The graph shows us which basis is the optimum for solving this problem from the -built in library (that which can reach the minimum :math:``{\chi^2}``). If we -were to go to higher N we would also find that the :math:``{\chi^2}`` value -would stop decreasing in value. The value of N for which this occurs at is the -optimum DCF order. (See the ``maxsmooth`` paper for a real world application -of this concept.) - -We can also provide this function with additional arguments such as the -fit type, minimum constrained derivative, directional exploration limits -ect. (see the ``maxsmooth`` Functions section). diff --git a/docs/source/chi_dist_example.rst b/docs/source/chi_dist_example.rst deleted file mode 100644 index d2246a3..0000000 --- a/docs/source/chi_dist_example.rst +++ /dev/null @@ -1,59 +0,0 @@ -This example will show you how to generate a plot of the :math:`{\chi^2}` -distribution as a function of the discrete sign combinations on the constrained -derivatives. - -First you will need to import your data and fit this using ``maxsmooth`` as -was done in the simple example code. - -.. code:: - - import numpy as np - - x = np.load('Data/x.npy') - y = np.load('Data/y.npy') - - from maxsmooth.DCF import smooth - - N = 10 - result = smooth(x, y, N, base_dir='examples/', - data_save=True, fit_type='qp') - -Here we have used some additional keyword arguments for the 'smooth' fitting -function. 'data_save' ensures that the files containing the tested sign combinations -and the corresponding objective function evaluations exist in the base directory -which we have changed to 'base_dir='examples/''. These files are essential for -the plotting the :math:`{\chi^2}` distribution and are not saved by ``maxsmooth`` -without 'data_save=True'. We have also set the 'fit_type' to 'qp' rather than the -default 'qp-sign_flipping'. This ensures that all of the available sign -combinations are tested rather than a sampled set giving us a full picture of the -distribution when we plot it. We have used the default DCF model to fit this data. - -We can import the 'chi_plotter' like so, - -.. code:: - - from maxsmooth.chidist_plotter import chi_plotter - -and produce the fit which gets placed in the base directory with the following -code, - -.. code:: - - chi_plotter(N, base_dir='examples/', fit_type='qp') - -We pass the same 'base_dir' as before so that the plotter can find the correct output -files. We also give the function the same 'fit_type' used for the fitting which -ensures that the files can be read. - -The resultant plot is shown below and the yellow star shows the global minimum. -This can be used to determine how well -the sign sampling approach using a descent and directional exploration -can find the global minimum. If the distribution looks like noise then it is -unlikely the sign sampling algorithm will consistently find the global minimum. -Rather it will likely repeatedly return the local minima found after the descent -algorithm and you should use the 'qp' method testing all available sign combinations -in any future fits to the data with this DCF model. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/chi_distribution.png - :width: 400 - :align: center diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100755 index 79f9fc5..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,111 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import sys -import os -sys.path.append(os.path.abspath('../../')) - -def get_version(short=False): - with open('../../README.rst') as f: - for line in f: - if ':Version:' in line: - ver = line.split(':')[2].strip() - if short: - subver = ver.split('.') - return '%s.%s' % tuple(subver[:2]) - else: - return ver - - -# -- Project information ----------------------------------------------------- - -project = 'maxsmooth' -copyright = '2020, Harry Thomas Jones Bevins' -author = 'Harry Thomas Jones Bevins' - -# The short X.Y version -version = get_version(True) -# The full version, including alpha/beta/rc tags -release = get_version() - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.imgconverter', - 'numpydoc' -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'maxsmooth.tex', 'maxsmooth Documentation', - 'Harry Thomas Jones Bevins', 'manual'), -] diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100755 index 75332ea..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Welcome to the ``maxsmooth`` documentation! -=========================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - Introduction - maxsmooth diff --git a/docs/source/intro.rst b/docs/source/intro.rst deleted file mode 100755 index 3d85661..0000000 --- a/docs/source/intro.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. title:: Introduction - -.. include:: ../../README.rst diff --git a/docs/source/maxsmooth.rst b/docs/source/maxsmooth.rst deleted file mode 100755 index 7f3a087..0000000 --- a/docs/source/maxsmooth.rst +++ /dev/null @@ -1,93 +0,0 @@ -.. toctree:: - :maxdepth: 6 - -``maxsmooth`` Theory and Algorithm ----------------------------------- - -.. include:: theory.rst - -``maxsmooth`` Example Codes ---------------------------- - -This section is designed to introduce the user to the software and the form -in which it is run. It provides basic examples of data fitting with a built in -MSF model and a user defined model. - -There are also examples of functions that can be used pre-fitting and post-fitting -for various purposes including; determination of the best DCF model from the -built in library for the problem being fitted, analysis of the :math:`{\chi^2}` -distribution as a function of the discrete sign spaces and analysis of the -parameter space surrounding the optimum results. - -The data used for all of this examples is available -`here `__. - -The example codes can be found -`here `__ and -corresponding Jupyter Notebooks are provided -`here `__. - -Simple Example code -~~~~~~~~~~~~~~~~~~~ -.. include:: simple_program.rst - -Turning Points and Inflection Points -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. include:: turning_points.rst - -New Basis Example -~~~~~~~~~~~~~~~~~ - -.. include:: new_basis_example.rst - -Best Basis Example -~~~~~~~~~~~~~~~~~~ - -.. include:: best_basis.rst - -:math:`{\chi^2}` Distribution Example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. include:: chi_dist_example.rst - -Parameter Plotter Example -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. include:: param_plotter_example.rst - -``maxsmooth`` Functions ------------------------ - -This section details the specifics of the built in functions in ``maxsmooth`` including -the relevant keyword arguments and default parameters for all. Where keyword arguments -are essential for the functions to run this is stated. - -smooth() -~~~~~~~~ - -.. automodule:: maxsmooth.DCF - :members: smooth - -best_basis() -~~~~~~~~~~~~ - -.. automodule:: maxsmooth.best_basis - :members: basis_test - -chidist_plotter() -~~~~~~~~~~~~~~~~~ - -.. automodule:: maxsmooth.chidist_plotter - :members: chi_plotter - -parameter_plotter() -~~~~~~~~~~~~~~~~~~~ - -.. automodule:: maxsmooth.parameter_plotter - :members: param_plotter - -Change Log ----------- - -.. include:: CHANGELOG.rst diff --git a/docs/source/new_basis_example.rst b/docs/source/new_basis_example.rst deleted file mode 100644 index 8f0ca5f..0000000 --- a/docs/source/new_basis_example.rst +++ /dev/null @@ -1,169 +0,0 @@ -This example code illustrates how to define your own basis function for the -DCF model. -It implements a modified version of the built in normalized polynomial model -but the structure is the same for more elaborate models. - -As always we need to import the data, define an order :math:`{N}` -and import the function fitting routine, smooth(). - -.. code:: - - import numpy as np - from maxsmooth.DCF import smooth - - x = np.load('Data/x.npy') - y = np.load('Data/y.npy') - - N=10 - -There are several requirements needed to define a new basis function completely -for ``maxsmooth`` to be able to fit it. They are as summarized below and then -examples of each are given in more detail, - - * **args:** Additional non-standard arguments needed in the definition of the - basis. The standard arguments are the data (x and y), the order of the fit N, - the pivot point about which a model can be fit, - the derivative order :math:`{m}` and the params. While the - pivot point is not strictly needed it is a required argument for the - functions defining a new basis to help the user in their definition. - - * **basis_functions:** This function defines the basis of the DCF model, - :math:`{\phi}` where the model can be generally defined as, - - .. math:: - - y = \sum_{k = 0}^N a_k \phi_k(x) - - where :math:`{a_k}` are the fit parameters. - - * **model:** This is the function described by the equation above. - - * **derivative:** This function defines the :math:`{m^{th}}` order derivative. - - * **derivative_pre:** This function defines the prefactors, - :math:`{\mathbf{G}}` on the derivatives where ``CVXOPT``, the quadratic - programming routine used, evaluates the constraints as, - - .. math:: - - \mathbf{Ga} \leq \mathbf{h} - - where :math:`{\mathbf{a}}` is the matrix of parameters and :math:`{\mathbf{h}}` - is the matrix of constraint limits. For more details on this see the ``maxsmooth`` - paper. - - -We can begin defining our new basis function by defining the additional arguments -needed to fit the model as a list, - -.. code:: - - arguments = [x[-1]*10, y[-1]*10] - -The next step is to define the basis functions :math:`{\phi}`. This needs to be -done in a function that has the arguments *(x, y, pivot_point, N, \*args)*. 'args' -is optional but since we need them for this basis we are passing it in. - -The basis functions, :math:`{\phi}`, should be an array of dimensions len(x) -by N and consequently evaluated at each N and x data point as shown below. - -.. code:: - - def basis_functions(x, y, pivot_point, N, *args): - - phi = np.empty([len(x), N]) - for h in range(len(x)): - for i in range(N): - phi[h, i] = args[1]*(x[h]/args[0])**i - - return phi - -We can define the model that we are fitting in a function like that shown below. -This is used for evaluating :math:`{\chi^2}` and returning the optimum fitted model -once the code has finished running. It requires the arguments -*(x, y, pivot_point, N, params, \*args)* in that order and again where 'args' is optional. -'params' is the parameters of the fit, :math:`{\mathbf{a}}` which should have length -:math:`{N}`. - -The function should return the fitted estimate of y. - -.. code:: - - def model(x, y, pivot_point, N, params, *args): - - y_sum = args[1]*np.sum([ - params[i]*(x/args[0])**i - for i in range(N)], axis=0) - - return y_sum - -Next we have to define a function for the derivatives of the model which -takes arguments *(m, x, y, N, pivot_point, params, *args)* where :math:`{m}` is -the derivative order. The function should return the :math:`{m^{th}}` order -derivative evaluation and is used for checking that the constraints have been -met and returning the derivatives of the optimum fit to the user. - -.. code:: - - def derivative(m, x, y, N, pivot_point, params, *args): - - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = args[1]*np.math.factorial(m+i) / \ - np.math.factorial(i) * \ - params[int(m)+i]*(x)**i / \ - (args[0])**(i + 1) - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - -Finally we have to define :math:`{\mathbf{G}}` which is used by ``CVXOPT`` to -build the derivatives and constrain the functions. It takes arguments -*(m, x, y, N, pivot_point, \*args)* and should return the prefactor on the -:math:`{m^{th}}` order derivative. For a more thorough definition of the -prefactor on the derivative and an explanation of how the problem is -constrained in quadratic programming see the ``maxsmooth`` paper. - -.. code:: - - def derivative_pre(m, x, y, N, pivot_point, *args): - - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = args[1]*np.math.factorial(m+i) / \ - np.math.factorial(i) * \ - (x)**i / \ - (args[0])**(i + 1) - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - -With our functions and additional arguments defined we can pass these -to the ``maxsmooth`` smooth() function as is shown below. This overwrites the -built in DCF model but you are still able to modify the fit type i.e. testing all -available sign combinations or sampling them. - -.. code:: - - result = smooth(x, y, N, - basis_functions=basis_functions, model=model, - derivatives=derivative, der_pres=derivative_pre, args=arguments) - -The output of the fit can be accessed as before, - -.. code:: - - print('Objective Funtion Evaluations:\n', result.optimum_chi) - print('RMS:\n', result.rms) - print('Parameters:\n', result.optimum_params) - print('Fitted y:\n', result.y_fit) - print('Sign Combinations:\n', result.optimum_signs) - print('Derivatives:\n', result.derivatives) diff --git a/docs/source/param_plotter_example.rst b/docs/source/param_plotter_example.rst deleted file mode 100644 index 0401370..0000000 --- a/docs/source/param_plotter_example.rst +++ /dev/null @@ -1,84 +0,0 @@ -We can assess the parameter space around the optimum solution -found using ``maxsmooth`` with the param_plotter() function. -This can help us identify how well a problem can be solved using the -sign navigating approach employed by ``maxsmooth`` or simply -be used to identify correlations between the foreground parameters. -For more details on this see the ``maxsmooth`` paper. - -We begin by importing and fitting the data as with the chi_plotter() -function illustrated above. - -.. code:: - - import numpy as np - - x = np.load('Data/x.npy') - y = np.load('Data/y.npy') - - from maxsmooth.DCF import smooth - - N = 5 - result = smooth(x, y, N, base_dir='examples/', fit_type='qp') - -We have changed the order of the fit to 5 to illustrate that for -order :math:`{N \leq 5}` and fits with derivatives :math:`{m \geq 2}` constrained -the function will plot each region of the graph corresponding to -different sign combinations in a different colourmap. Recall that -by default the function smooth() fits a maximally smooth function (MSF) with -derivatives of order :math:`{m \geq 2}`. If the constraints are -different or the order is greater than 5 then the viable regions will have -a single colourmap. Invalid regions are plotted as black shaded colourmaps -and the contour lines are contours of :math:`{\chi^2}`. - -Specifically, invalid regions violate the condition - -.. math:: - - \pm_m \frac{\delta^m y}{\delta x^m} \leq 0 - -where :math:`{m}` represents the derivative order, :math:`{y}` is the dependent -variable and :math:`{x}` is the independent variable. Violation of the -condition means that one or more of the constrained derivatives crosses 0 in the -band of interest. For an MSF, as mentioned, :math:`{m \geq 2}` and the sign :math:`{\pm_m}` -applies to specific derivative orders. For this specific example there are -3 constrained derivatives, :math:`{m = 2, 3, 4}` and consequently 3 signs to -optimise for alongside the parameters :math:`{a_k}`. The coloured valid regions -therefore correspond to a specific combination of :math:`{\pm_m}` for the problem. -:math:`{\pm_m}` is also referred to as :math:`{\mathbf{s}}` in the theory -section and the ``maxsmooth`` paper. - -We can import the function like so, - -.. code:: - - from maxsmooth.parameter_plotter import param_plotter - -and access it using, - -.. code:: - - param_plotter(result.optimum_params, result.optimum_signs, - x, y, N, base_dir='examples/') - -The function takes in the optimum parameters and signs found after the fit -as well as the data and order of the fit. There are a number of keyword arguments -detailed in the following section and the resultant fit is shown below. The -function by default samples the parameter ranges 50% either side of the optimum -and calculates 50 samples for each parameter. In each panel the two -labelled parameters are varied while the others are maintained at their optimum -values. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/Parameter_plot.png - -We are also able to plot the data, fit and residuals alongside the parameter -plot and this can be done by setting data_plot=True. We can also highlight the -central region in each panel of the parameter space by setting center_plot=True. - -.. code:: - - param_plotter(result.optimum_params, result.optimum_signs, - x, y, N, base_dir='examples/', data_plot=True, center_plot=True) - -which gives us the graph below. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/Parameter_plot_extended.png diff --git a/docs/source/simple_program.rst b/docs/source/simple_program.rst deleted file mode 100755 index 4a2226a..0000000 --- a/docs/source/simple_program.rst +++ /dev/null @@ -1,179 +0,0 @@ -.. highlight:: python - -In order to run the ``maxsmooth`` software using the built -in DCF models for a simple fit the user can follow the simple structure detailed here. - -An important point to make is that by default ``maxsmooth`` fits a -Maximally Smooth Function or MSF to the data. An MSF, as stated in -the introduction to the documentation, is a function which has -derivatives of order :math:`{m \geq 2}` constrained so that they do not cross -0. This means that they do not have inflection points or non smooth -structure produced by higher order derivatives. More generally a DCF -follows the constraint, - -.. math: - - \frac{\delta^m y}{\delta x^m} \leq 0 ~~\mathrm{or}~~ \frac{\delta^m y}{\delta x^m} \geq 0 $ - -for every constrained order :math:`{m}`. The set of :math:`{m}` can be any set of -derivative orders as long as those derivatives exist for the function. - -This means we can use ``maxsmooth`` to produce different DCF -models. MSFs are one of two special cases of DCF and we can also -have a Completely Smooth Function (CSF) with orders :math:`{m \geq 1}` -constrained. Alternatively we can have Partially Smooth Functions -(PSF) which are much more general and can have arbitrary sets of -derivatives constrained. We illustrate how this is implemented -towards the end of this example but we begin with the default case -fitting a MSF. - -The user should begin by importing the `smooth` class from `maxsmooth.DCF`. - -.. code:: - - from maxsmooth.DCF import smooth - -The user should then import the data they wish to fit. - -.. code:: - - import numpy as np - - x = np.load('Data/x.npy') - y = np.load('Data/y.npy') + np.random.normal(0, 0.02, len(x)) - -and define the polynomial orders they wish to fit. - -.. code:: - - N = [3, 4, 5, 6, 7, 8, 9, 10, 11] - for i in range(len(N)): - `act on N[i]` - -or for example, - -.. code:: - - N = 15 - -We can also plot the data to illustrate what is happening. -Here the data is a scaled :math:`{x^{-2.5}}` power law and I have added gaussian -noise in with a standard deviation of 0.02. - -.. code:: bash - - import matplotlib.pyplot as plt - - plt.plot(x, y) - plt.xlabel('x') - plt.ylabel('y') - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/simple_program_data.png - :width: 400 - :align: center - -`smooth` can be called as is shown below. It takes the x and y data as standard -inputs as well as the order of the fit. There are a set of keyword arguments -also available that change the type of function being fitted and these are -detailed in the documentation. - -.. code:: - - result = smooth(x, y, N) - -and it's resulting attributes can be accessed by writing -:code:`result.attribute_name`. For example printing the outputs is done like -so, - -.. code:: - - print('Objective Funtion Evaluations:\n', result.optimum_chi) - print('RMS:\n', result.rms) - print('Parameters:\n', result.optimum_params) - print('Fitted y:\n', result.y_fit) - print('Sign Combinations:\n', result.optimum_signs) - print('Derivatives:\n', result.derivatives) - - plt.plot(x, y - result.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/simple_program_msf_residuals.png - :width: 400 - :align: center - -To fit the data with a CSF we can use the 'constraints' keyword -argument in smooth(). 'constraints' sets the minimum constrained -derivative for the function which for a CSF we want to be one. - -.. code:: bash - - res = smooth(x, y, N, constraints=1) - -Note in the printed results the number of constrained derivatives has -increased by 1 and the only derivative that is allowed to cross through 0 -(Zero Crossings Used?) is the the :math:`{0^{th}}` order i.e. the data. - -.. code:: bash - - plt.plot(x, y - res.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/simple_program_csf_residuals.png - :width: 400 - :align: center - -A Partially Smooth Function can have derivatives constrained via :math:`{m \geq a}` -where :math:`{a}` is -any order above 2 or it can have a set of derivatives that are allowed to cross -zero. For the first case we can once again use the 'constraints' keyword -argument. For example we can constrain derivatives with orders :math:`{\geq 3}` which will -allow the :math:`{1^{st}}` and :math:`{2^{nd}}` order derivatives to cross zero. -This is useful when our -data features an inflection point we want to model with our fit. - -.. code:: bash - - res = smooth(x, y, N, constraints=3) - - plt.plot(x, y - res.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/simple_program_psf1_residuals.png - :width: 400 - :align: center - -To allow a particular set of derivatives to cross zero we use the -'zero_crossings' keyword. In the example below we are lifting the constraints -on the :math:`{3^{rd}}`, :math:`{4^{th}}` and :math:`{5^{th}}` order derivatives -but our minimum constrained derivative is still set at the default 2. Therefore -this PSF has derivatives of order :math:`{m = [2, 6, 7, 8, 9]}` -constrained via the condition at the begining of this example code. - -.. code:: - - res = smooth(x, y, N, zero_crossings=[3, 4, 5]) - - plt.plot(x, y - res.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/simple_program_psf2_residuals.png - :width: 400 - :align: center - -While PSFs can seem like an attractive way to improve the quality of fit they -are less 'smooth' than a MSF or CSF and consequently they can introduce -additional turning points in to your residuals obscuring any signals of -intrest. diff --git a/docs/source/theory.rst b/docs/source/theory.rst deleted file mode 100644 index fca1b77..0000000 --- a/docs/source/theory.rst +++ /dev/null @@ -1,180 +0,0 @@ -This section has been adapted from section 4 of the ``maxsmooth`` paper -in order to explain how the algorithm works. What follows is a discussion of -the fitting problem and the -``maxsmooth`` algorithm. To state concisely the problem being fitted we have - -.. math:: - - &\min_{a,~s}~~\frac{1}{2}~\mathbf{a}^T~\mathbf{Q}~\mathbf{a}~+~\mathbf{q}^T~\mathbf{a}, \\ - &\mathrm{s.t.}~~\mathbf{G(s)~a} \leq \mathbf{0}. - -where :math:`{\mathbf{s}}` are the ``maxsmooth`` signs corresponding to the -signs on the derivatives. :math:`{\mathbf{G}}` is a matrix of prefactors on the derivatives, -:math:`{\mathbf{a}}` are the parameters we are optimising for and their -product gives the derivatives we are constraining with each fit. -:math:`{\mathbf{Q}}` is the dot product of the matrix of basis functions and -its transpose and :math:`\mathbf{q}` is the negative of the transposed data, -:math:`\mathbf{y}` dotted with the basis functions. For more details on this -equation see the ``maxsmooth`` paper. -A `problem' in this context is the combination of the data, order, basis -function and constraints on the DCF. - -With ``maxsmooth`` we can test all possible sign combinations on the constrained derivatives. -This is a -reliable method and, provided the problem can be solved with quadratic programming, -will always give the correct global minimum. When the problem we are interested -in is "well defined", we can develop a quicker algorithm that searches or navigates -through the discrete ``maxsmooth`` sign spaces to find the global minimum. -Each sign space is a discrete parameter space with its own global minimum. -Using quadratic programming on a fit with a specific sign combination will -find this global minimum, and we are interested in finding the minimum -of these global minima. - -A "well defined" problem is one in which the discrete sign spaces have large -variance in their minimum :math:`{\chi^2}` values and the sign space for the -global minimum is easily identifiable. In contrast we can have an "ill defined" -problem in which the variance in minimum :math:`{\chi^2}` across all sign -combinations is small. This concept of "well defined" and "ill defined" problems -is explored further in the following two sections. - -Well Defined Problems and Discrete Sign Space Searches -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :math:`{\chi^2}` Distribution -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -We investigate the distribution of :math:`{\chi^2}` values, shown in the figure below, -for a 10 :math:`{^{th}}` order y-log(x) space MSF fit to a :math:`{y = x^{-2.5}}` -power law plus gaussian noise. - -In the figure, a combination of all positive derivatives~(negative signs) and -all negative derivatives~(positive signs) corresponds to sign combination numbers -255 and 0 respectively. Specifically, the ``maxsmooth`` signs, :math:`{\mathbf{s}}`, -are related to the sign combination number by its :math:`{C}` bit binary representation, -here :math:`{C = (N -2)}`. In binary the sign combination numbers run from -00000000 to 11111111. Each bit represents the sign on the :math:`{m^{th}}` -order derivative with a 1 representing a negative ``maxsmooth`` sign. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/chi_dist_theory.png - :width: 400 - :align: center - -The distribution appears to be composed of smooth steps or shelves; however, -when each shelf is studied closer, we find a series of peaks and troughs. This can -be seen in the subplot of the above figure which shows the distribution in the -neighbourhood of the global minimum found in the large or `global' well. This type -of distribution with a large variance in :math:`{\chi^2}` is characteristic of a "well defined" -problem. We use this example :math:`{\chi^2}` distribution to motivate the ``maxsmooth`` -algorithm outlined in the following section. - -The ``maxsmooth`` Sign Navigating Algorithm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Exploration of the discrete sign spaces for high :math:`{N}` can be achieved by -exploring the spaces around an iteratively updated optimum sign combination. -The ``maxsmooth`` algorithm begins with a randomly generated set of signs for -which the objective function is evaluated and the optimum parameters are found. -We flip each individual sign one at a time beginning with the lowest order -constrained derivative first. When the objective function is evaluated to be lower -than that for the optimum sign combination, we replace it with the new set and repeat -the process in a `cascading' routine until the objective function stops decreasing in value. - -The local minima shown in the :math:`{\chi^2}` distribution above mean that the -cascading algorithm is not sufficient to consistently find the global minimum. -We can demonstrate this by performing 100 separate runs of the cascading -algorithm on :math:`{y = x^{-2.5} + \mathrm{noise}}`, and we use a y-log(x) space -:math:`{10^{th}}` order MSF again. We find the true global minimum 79 -times and a second local minimum 21 times. - -To prevent the routine terminating in a local minimum we perform a complete search -of the sign spaces surrounding the minimum found after the cascading routine. -We refer to this search as a directional exploration and impose limits on its -extent. In each direction we limit the number of sign combinations to explore and -we limit the maximum allowed increase in :math:`{\chi^2}` value. These limits can -be modified by the user. We prevent repeated calculations of the minimum for given -signs and treat the minimum of all tested signs as the global minimum. - -We run the consistency test again, with the full ``maxsmooth`` algorithm, and find -that for all 100 trial fits we find the same :math:`{\chi^2}` found when testing -all sign combinations. In the figure below, the red arrows show the approximate path -taken through the discrete sign spaces against the complete distribution of :math:`{\chi^2}`. -Point (1a) shows the random starting point in the algorithm, and point (1b) shows a rejected sign -combination evaluated during the cascade from point (1a) to (2). Point (2), therefore, -corresponds to a step through the cascade. Point (3) marks the end of the cascade -and the start of the left directional exploration. Finally, point (4) shows the end -of the right directional exploration where the calculated :math:`{\chi^2}` -value exceeds the limit on the directional exploration. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/routine.png - :width: 400 - :align: center - -The global well tends to be associated with signs that are all positive, -all negative or alternating. We see this in the figure above where the minimum falls -at sign combination number 169 and number 170, characteristic of the derivatives for -a :math:`{x^{-2.5}}` power law, corresponds to alternating positive and negative -derivatives from order :math:`{m = 2}`. Standard patterns of derivative signs can be seen -for all data following approximate power laws. All positive derivatives, all negative -and alternating signs correspond to data following the approximate power laws -:math:`{y\approx x^{k}}`, :math:`{y\approx -x^{k}}`, :math:`{y\approx x^{-k}}` and -:math:`{y\approx -x^{-k}}`. - -The ``maxsmooth`` algorithm assumes that the global well is present in the :math:`{\chi^2}` -distribution and this is often the case. The use of DCFs is primarily driven by a -desire to constrain previously proposed polynomial models to foregrounds. As a result -we would expect that the data being fitted could be described by one of the four -approximate power laws highlighted above and that the global minimum will fall -around an associated sign combination. In rare cases the global well is not clearly -defined and this is described in the following subsection. - -Ill Defined Problems and their Identification -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -We can illustrate an "ill defined" problem, with a small variation in -:math:`{\chi^2}` across the ``maxsmooth`` sign spaces, by adding a non-smooth signal -of interest into the foreground model, :math:`{x^{-2.5}}` and fitting this with -a 10 :math:`{^{th}}` order log(y)-log(x) space MSF. We add an additional noise of -:math:`{0.020}` to the mock data. The resultant :math:`{\chi^2}` distribution with its -global minimum is shown in the top panel of the figure below. - -The global minimum, shown as a black data point, cannot be found using the -``maxsmooth`` algorithm. The cascading algorithm may terminate in any of the -approximately equal minima and the directional exploration will then quickly -terminate because of the limits imposed. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/combined_chi.png - :width: 400 - :align: center - -If we repeat the above fit and perform it with a y-x space MSF we find that the -problem is well defined with a larger :math:`{\chi^2}` variation across sign -combinations. This is shown in the bottom panel of the above figure. The results, -when using the log(y)-log(x) space MSF, are significantly better than when using -y-x space MSF meaning it is important to be able to solve "ill defined" problems. -This can be done by testing all ``maxsmooth`` signs but knowing when this is -necessary is important if you are expecting to run multiple DCF fits to the -same data set. We can focus on diagnosing whether a DCF fit to the data is -"ill defined" because a joint fit to the same data set of a DCF and signal -of interest will also feature an "ill defined" :math:`{\chi^2}` distribution. - -We can identify an "ill defined" problem by producing the equivalent of -the above figure using ``maxsmooth`` and visually assessing the :math:`{\chi^2}` -distribution for a DCF fit. Alternatively, we can use the parameter space plots, -detailed in the ``maxsmooth`` paper and later in this documentation, -to identify whether the constraints are weak or not, and if a local minima is -returned from the sign navigating routine then the minimum in these plots -will appear off centre. - -Assessment of the first derivative of the data can also help to identify an -"ill defined" problem. For the example problem this is shown in the figure below -where the derivatives have been approximated using :math:`{\Delta y/ \Delta x}`. -Higher order derivatives of the data will have similarly complex or simplistic -structures in the respective spaces. There are many combinations of parameters -that will provide smooth fits with similar :math:`{\chi^2}` values in logarithmic -space leading to the presence of local minima. This issue will also be present -in any data set where the noise or signal of interest are of a similar magnitude -to the foreground in y - x space. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/Gradients_fits.png - :width: 400 - :align: center diff --git a/docs/source/turning_points.rst b/docs/source/turning_points.rst deleted file mode 100644 index 1b84a84..0000000 --- a/docs/source/turning_points.rst +++ /dev/null @@ -1,121 +0,0 @@ -This example will walk the user through implementing DCF fits to data sets with -turning points and inflection points. It builds on the details in the -'Simple Example Code' and uses the 'constraints' keyword argument introduced -there. The 'constraints' keyword argument is used to adjust the type of DCF that -is being fitted. Recall that by default ``maxsmooth`` implements a Maximally -Smooth Function or MSF with constraints=2 i.e. derivatives of order :math:`{m \geq 2}` -constrained so that they do not cross zero. This allows for turning points in the -DCF as illustrated below. - -We start by generating some noisy data that we know will include a turning point -and defining the order of the DCF we would like to fit. - -.. code:: bash - - import numpy as np - - x = np.linspace(-10, 10, 100) - noise = np.random.normal(0, 0.02, 100) - y = x**(2) + noise - - N = 10 - -We can go ahead and plot this data just to double check it features a turning -point. - -.. code:: bash - - import matplotlib.pyplot as plt - - plt.plot(x, y) - plt.xlabel('x', fontsize=12) - plt.ylabel('y', fontsize=12) - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/turning_point_example.png - :width: 400 - :align: center - -As already stated ``maxsmooth`` does not constrain the first derivative of the -DCF by default so we can go ahead and fit the data. - -.. code:: bash - - from maxsmooth.DCF import smooth - - res = smooth(x, y, N) - -If we than plot the resultant residuals we will see that despite the data -having a turning point present we have recovered the Gaussian noise. - -.. code:: bash - - plt.plot(x, y- res.y_fit, label='Recovered Noise') - plt.plot(x, noise, label='Actual Noise') - plt.ylabel(r'$\delta y$', fontsize=12) - plt.xlabel('x', fontsize=12) - plt.legend() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/turning_point_example_res.png - :width: 400 - :align: center - -To illustrate what happens when there is an inflection point in the data we can -define some sinusoidal data as so. - -.. code:: bash - - x = np.linspace(1, 5, 100) - noise = np.random.normal(0, 0.02, 100) - y = np.sin(x) + noise - - N = 10 - - plt.plot(x, y) - plt.xlabel('x', fontsize=12) - plt.ylabel('y', fontsize=12) - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/inflection_point_example.png - :width: 400 - :align: center - -If we proceed to fit this with smooth() in its default settings we will get a -poor fit as by default the second derivative is constrained. We need to lift this -constraint to allow for the prominent inflection point to be modelled. We do this -by setting the keyword argument constraints=3 creating a Partially Smooth Function -or PSF. - -.. code:: bash - - res_msf = smooth(x, y, N) - res_psf = smooth(x, y, N, constraints=3) - - plt.plot(x, y, label='Data') - plt.plot(x, res_msf.y_fit, label=r'MSF fit, $m \geq 2$') - plt.plot(x, res_psf.y_fit, label=r'PSF fit, $m \geq 3$') - plt.xlabel('x', fontsize=12) - plt.ylabel('y', fontsize=12) - plt.legend() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/inflection_point_example_fits.png - :width: 400 - :align: center - -Finally, we can plot the residuals to further see that by lifting the constraint on the -second derivative we have allowed an inflection point in the data. - -.. code:: - - plt.plot(x, y- res_psf.y_fit, label='Recovered Noise') - plt.plot(x, noise, label='Actual Noise') - plt.ylabel(r'$\delta y$', fontsize=12) - plt.xlabel('x', fontsize=12) - plt.legend() - plt.show() - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/inflection_point_example_res.png - :width: 400 - :align: center diff --git a/docs/tutorials.md b/docs/tutorials.md new file mode 100644 index 0000000..e69de29 diff --git a/example_codes/Data/x.npy b/example_codes/Data/x.npy deleted file mode 100755 index 8e1e392..0000000 Binary files a/example_codes/Data/x.npy and /dev/null differ diff --git a/example_codes/Data/y.npy b/example_codes/Data/y.npy deleted file mode 100755 index b648c7c..0000000 Binary files a/example_codes/Data/y.npy and /dev/null differ diff --git a/example_codes/best_basis_example.py b/example_codes/best_basis_example.py deleted file mode 100644 index 033772d..0000000 --- a/example_codes/best_basis_example.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -This function can be used to identify which of the built in DCFs -fits the data best before running joint fits. - -To use it we begin by loading in the data, -""" - -import numpy as np - -x = np.load('Data/x.npy') -y = np.load('Data/y.npy') - -""" -and then importing the basis_test() function. -""" - -from maxsmooth.best_basis import basis_test - -""" -To call the function we use (this will take a long time to run but -the resultant graph saved in the base_dir is the important -result(see below)), -""" - -basis_test(x, y, base_dir='examples/', N=np.arange(3, 16, 1)) - -""" -The function only requires the data but we can provide it with a base directory, -fit type and range of DCF orders to test. By defualt it uses the sign navigating -algorithm and tests :math:`{N = 3 - 13}`. Here we test the range -:math:``{N = 3 - 15}``. -The resultant graph is saved in the -base directory and the example generated here is shown below. - -The graph shows us which basis is the optimum for solving this problem from the -built in library (that which can reach the minimum :math:``{\chi^2}``). If we -were to go to higher N we would also find that the :math:``{\chi^2}`` value -would stop decreasing in value. The value of N for which this occurs at is the -optimum DCF order. (See the ``maxsmooth`` paper for a real world application -of this concept.) - -We can also provide this function with additional arguments such as the -fit type, minimum constrained derivative, directional exploration limits -ect. (see the ``maxsmooth`` Functions section). -""" diff --git a/example_codes/chi_dist_example.py b/example_codes/chi_dist_example.py deleted file mode 100644 index 5b5cf06..0000000 --- a/example_codes/chi_dist_example.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -This example will show you how to generate a plot of the :math:`{\chi^2}` -distribution as a function of the descrete sign combinations on the constrained -derivatives. - -First you will need to import your data and fit this using ``maxsmooth`` as -was done in the simple example code. - -""" - -import numpy as np - -x = np.load('Data/x.npy') -y = np.load('Data/y.npy') - -from maxsmooth.DCF import smooth - -N = 10 -result = smooth(x, y, N, base_dir='examples/', - data_save=True, fit_type='qp') - -""" -Here we have used some additional keyword arguments for the 'smooth' fitting -function. 'data_save' ensures that the files containing the tested sign combinations -and the corresponding objective function evaluations exist in the base directory -which we have changed to 'base_dir='examples/''. These files are essential for -the plotting the :math:`{\chi^2}` distribution and are not saved by ``maxsmooth`` -without 'data_save=True'. We have also set the 'fit_type' to 'qp' rather than the -default 'qp-sign_flipping'. This ensures that all of the available sign -combinations are tested rather than a sampled set giving us a full picture of the -distribution when we plot it. We have used the default DCF model to fit this data. - -We can import the 'chi_plotter' like so, -""" - -from maxsmooth.chidist_plotter import chi_plotter - -""" -and produce the fit which gets placed in the base directory with the following -code, -""" - -chi_plotter(N, base_dir='examples/', fit_type='qp') - -""" -We pass the same 'base_dir' as before so that the plotter can find the correct output -files. We also give the function the same 'fit_type' used for the fitting which -ensures that the files can be read. -""" diff --git a/example_codes/new_basis_program.py b/example_codes/new_basis_program.py deleted file mode 100755 index 24f0952..0000000 --- a/example_codes/new_basis_program.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -This example code illustrates how to define your own basis function for the -DCF model. -It implements a modified version of the built in normalised polynomial model -but the structure is the same for more elaborate models. - -As always we need to import the data, define an order :math:`{N}` -and import the function fitting routine, smooth(). -""" - -import numpy as np -from maxsmooth.DCF import smooth - -x = np.load('Data/x.npy') -y = np.load('Data/y.npy') - -N=10 - -""" -There are several requirements needed to define a new basis function completely -for ``maxsmooth`` to be able to fit it. They are as summarised below and then -examples of each are given in more detail, - - * **args:** Additional non-standard arguments needed in the definition of the - basis. The standard arguments are the data (x and y), the order of the fit N, - the pivot point about which a model can be fit, - the derivative order :math:`{m}` and the params. While the - pivot point is not strictly needed it is a required argument for the - functions defining a new basis to help the user in their definition. - - * **basis_functions:** This function defines the basis of the DCF model, - :math:`{\phi}` where the model can be generally defined as, - - .. math:: - - y = \sum_{k = 0}^N a_k \phi_k(x) - - where :math:`{a_k}` are the fit parameters. - - * **model:** This is the function described by the equation above. - - * **derivative:** This function defines the :math:`{m^{th}}` order derivative. - - * **derivative_pre:** This function defines the prefactors, - :math:`{\mathbf{G}}` on the derivatives where ``CVXOPT``, the quadratic - programming routine used, evaluates the constraints as, - - .. math: - - \mathbf{Ga} \leq \mathbf{h} - - where :math:`{\mathbf{a}}` is the matrix of parameters and :math:`{\mathbf{h}}` - is the matrix of constraint limits. For more details on this see the ``maxsmooth`` - paper. - - -We can begin defining our new basis function by defining the aditional arguments -needed to fit the model as a list, -""" -arguments = [x[-1]*10, y[-1]*10] - -""" -The next step is to define the basis functions :math:`{\phi}`. This needs to be -done in a function that has the arguments *(x, y, pivot_point, N, \*args)*. 'args' -is optional but since we need them for this basis we are passing it in. - -The basis functions, :math:`{\phi}`, should be an array of dimensions len(x) -by N and consequently evaluated at each N and x data point as shown below. -""" - -def basis_functions(x, y, pivot_point, N, *args): - - phi = np.empty([len(x), N]) - for h in range(len(x)): - for i in range(N): - phi[h, i] = args[1]*(x[h]/args[0])**i - - return phi - -""" -We can define the model that we are fitting in a function like that shown below. -This is used for evaluating :math:`{\chi^2}` and returning the optimum fitted model -once the code has finished running. It requires the arguments -*(x, y, pivot_point, N, params, \*args)* in that order and again where 'args' is optional. -'params' is the parameters of the fit, :math:`{\mathbf{a}}` which should have length -:math:`{N}`. - -The function should return the fitted estimate of y. -""" - -def model(x, y, pivot_point, N, params, *args): - - y_sum = args[1]*np.sum([ - params[i]*(x/args[0])**i - for i in range(N)], axis=0) - - return y_sum - -""" -Next we have to define a function for the derivatives of the model which -takes arguments *(m, x, y, N, pivot_point, params, *args)* where :math:`{m}` is -the derivative order. The function should return the :math:`{m^{th}}` order -derivative evaluation and is used for checking that the constraints have been -met and returning the derivatives of the optimum fit to the user. -""" - -def derivative(m, x, y, N, pivot_point, params, *args): - - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = args[1]*np.math.factorial(m+i) / \ - np.math.factorial(i) * \ - params[int(m)+i]*(x)**i / \ - (args[0])**(i + 1) - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - -""" -Finally we have to define :math:`{\mathbf{G}}` which is used by ``CVXOPT`` to -build the derivatives and constrain the functions. It takes arguments -*(m, x, y, N, pivot_point, \*args)* and should return the prefactor on the -:math:`{m^{th}}` order derivative. For a more thorough definition of the -prefactor on the derivative and an explination of how the problem is -constrained in quadratic programming see the ``maxsmooth`` paper. -""" - -def derivative_pre(m, x, y, N, pivot_point, *args): - - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = args[1]*np.math.factorial(m+i) / \ - np.math.factorial(i) * \ - (x)**i / \ - (args[0])**(i + 1) - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - -""" -With our functions and additional arguments defined we can pass these -to the ``maxsmooth`` smooth() function as is shown below. This overwrites the -built in DCF model but you are still able to modify the fit type i.e. testing all -available sign combinations or sampling them. -""" - -result = smooth(x, y, N, - basis_functions=basis_functions, model=model, - derivatives=derivative, der_pres=derivative_pre, args=arguments) - -""" -The output of the fit can be accessed as before, -""" - -print('Objective Funtion Evaluations:\n', result.optimum_chi) -print('RMS:\n', result.rms) -print('Parameters:\n', result.optimum_params[2]) -print('Fitted y:\n', result.y_fit) -print('Sign Combinations:\n', result.optimum_signs) -print('Derivatives:\n', result.derivatives) diff --git a/example_codes/param_plotter_example.py b/example_codes/param_plotter_example.py deleted file mode 100644 index 6f20709..0000000 --- a/example_codes/param_plotter_example.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -We can assess the parameter space around the optimum solution -found using ``maxsmooth`` with the param_plotter() function. -This can help us identify how well a problem can be solved using the -sign sampling approach employed by ``maxsmooth`` or simply -be used to identify correlations between the foreground parameters. -For more details on this see the ``maxsmooth`` paper. - -We begin by importing and fitting the data as with the chi_plotter() -function illustrated above. -""" - -import numpy as np - -x = np.load('Data/x.npy') -y = np.load('Data/y.npy') - -from maxsmooth.DCF import smooth - -N = 5 -result = smooth(x, y, N, base_dir='examples/', fit_type='qp') - -""" -We have changed the order of the fit to 5 to illustrate that for -order :math:`{N \leq 5}` and fits with derivatives :math:`{m \geq 2}` constrained -the function will plot each region of the graph corresponding to -different sign functions in a different colourmap. If the constraints are -different or the order is greater than 5 then the viable regions will have -a single colourmap. Invalid regions are plotted as black shaded colourmaps -and the contour lines are contours of :math:`{\chi^2}`. - -Specifically, invalid regions violate the condition - -.. math:: - - \pm_m \frac{\delta^m y}{\delta x^m} \leq 0 - -where :math:`{m}` represents the derivative order, :math:`{y}` is the dependent -variable and :math:`{x}` is the independent variable. Violation of the -condition means that one or more of the constrained derivatives crosses 0 in the -band of interest. For an MSF, as mentioned, :math:`{m \geq 2}` and the sign :math:`{\pm_m}` -applies to specific derivative orders. For this specific example there are -3 constrained derivatives, :math:`{m = 2, 3, 4}` and consequently 3 signs to -optimise for alongside the parameters :math:`{a_k}`. The coloured valid regions -therefore correspond to a specific combination of :math:`{\pm_m}` for the problem. -:math:`{\pm_m}` is also referred to as :math:`{\mathbf{s}}` in the theory -section and the ``maxsmooth`` paper. - -We can import the function like so, -""" - -from maxsmooth.parameter_plotter import param_plotter - -""" -and access it using, -""" - -param_plotter(result.optimum_params, result.optimum_signs, - x, y, N, base_dir='examples/') - -""" -The function takes in the optimum parameters and signs found after the fit -as well as the data and order of the fit. There are a number of keyword arguments -detailed in the following section and the resultant fit is shown below. The -function by default samples the parameter ranges 50% either side of the optimum -and calculates 50 spamples for each parameter. In each panel the two -labelled parameters are varied while the others are maintained at their optimum -values. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/Parameter_plot.png - -We are also able to plot the data, fit and residuals alongside the parameter -plot and this can be done by setting data_plot=True. We can also highlight the -central region in each panel of the parameter space by setting center_plot=True. -""" - -param_plotter(result.optimum_params, result.optimum_signs, - x, y, N, base_dir='examples/', data_plot=True, center_plot=True) - -""" -which gives us the graph below. - -.. image:: https://github.com/htjb/maxsmooth/raw/master/docs/images/Parameter_plot_extended.png -""" diff --git a/example_codes/simple_program.py b/example_codes/simple_program.py deleted file mode 100755 index 2898c2a..0000000 --- a/example_codes/simple_program.py +++ /dev/null @@ -1,233 +0,0 @@ -""" -This section is designed to introduce the user to the software and the form -in which it is run. In order to run the `maxsmooth` software using the built -in DCFs the user can follow the simple structure detailed here. - -An important point to make is that by default ``maxsmooth`` fits a -Maximally Smooth Function or MSF to the data. An MSF, as stated in -the introduction to the documentation, is a function which has -derivatives of order :math:`{m \geq 2}` constrained so that they do not cross -0. This means that they do not have inflection points or non smooth -structure produced by higher order derivatives. More generally a DCF -follows the constraint, - -.. math: - - \frac{\delta^m y}{\delta x^m} \leq 0 ~~\mathrm{or}~~ \frac{\delta^m y}{\delta x^m} \geq 0 $ - -for every constrained order :math:`{m}`. The set of :math:`{m}` can be any set of -derivative orders as long as those derivatives exist for the function. - -This means we can use ``maxsmooth`` to produce different DCF -models. MSFs are one of two special cases of DCF and we can also -have a Completely Smooth Function (CSF) with orders :math:`{m \geq 1}` -constrained. Alternatively we can have Partially Smooth Functions -(PSF) which are much more general and can have arbitrary sets of -derivatives constrained. We illustrate how this is implemented -towards the end of this example but we begin with the default case -fitting a MSF. - -The user should begin by importing the `smooth` class from `maxsmooth.DCF`. - -.. code:: bash - from maxsmooth.DCF import smooth - -""" -from maxsmooth.DCF import smooth - -""" -The user should then import the data they wish to fit. - -.. code:: bash - - import numpy as np - - x = np.load('Data/x.npy') - y = np.load('Data/y.npy') + np.random.normal(0, 0.02, len(x)) - -and define the polynomial orders they wish to fit. - -.. code:: bash - - N = [3, 4, 5, 6, 7, 8, 9, 10, 11] - for i in range(len(N)): - `act on N[i]` - -or for example, - -.. code:: bash - - N = 15 - -We can also plot the data to illustrate what is happening. -Here the data is a scaled :math:`{x^{-2.5}}` power law and I have added gaussian -noise in with a standard deviation of 0.02. - -.. code:: bash - - import matplotlib.pyplot as plt - - plt.plot(x, y) - plt.xlabel('x') - plt.ylabel('y') - plt.show() - -""" -import numpy as np - -x = np.load('Data/x.npy') -y = np.load('Data/y.npy') + np.random.normal(0, 0.02, len(x)) - -N = 15 - -import matplotlib.pyplot as plt - -plt.plot(x, y) -plt.xlabel('x') -plt.ylabel('y') -plt.show() - -""" -`smooth` can be called as is shown below. It takes the x and y data as standard -inputs as well as the order of the fit. There are a set of keyword arguments -also available that change the type of function being fitted and these are -detailed in the documentation. - -.. code:: bash - - result = smooth(x, y, N) - -and it's resulting attributes can be accessed by writing -:code:`result.attribute_name`. For example printing the outputs is done like -so, - -.. code:: bash - - print('Objective Funtion Evaluations:\n', result.Optimum_chi) - print('RMS:\n', result.rms) - print('Parameters:\n', result.Optimum_params) - print('Fitted y:\n', result.y_fit) - print('Sign Combinations:\n', result.Optimum_signs) - print('Derivatives:\n', result.derivatives) - - plt.plot(x, y - result.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() - -""" - -result = smooth(x, y, N) - -print('Accessing Fit Attributes:') -print('Objective Funtion Evaluations:\n', result.optimum_chi) -print('RMS:\n', result.rms) -#print('Parameters:\n', result.Optimum_params) -#print('Fitted y:\n', result.y_fit) -print('Sign Combinations:\n', result.optimum_signs) -#print('Derivatives:\n', result.derivatives) - -plt.plot(x, y - result.y_fit) -plt.xlabel('x', fontsize=12) -plt.ylabel(r'$\delta y$', fontsize=12) -plt.tight_layout() -plt.show() - -""" -To fit the data with a CSF we can use the 'constraints' keyword -argument in smooth(). 'constraints' sets the minimum constrained -derivative for the function which for a CSF we want to be one. - -.. code:: bash - - res = smooth( - x, y, N, constraints=1) -""" - -res = smooth( - x, y, N, constraints=1) - -""" -Note in the printed results the number of constrained derivatives has -increased by 1 and the only derivative that is allowed to cross through 0 -(Zero Crossings Used?) is the the :math:`{0^{th}}` order i.e. the data. - -.. code:: bash - - plt.plot(x, y - res.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() -""" - -plt.plot(x, y - res.y_fit) -plt.xlabel('x', fontsize=12) -plt.ylabel(r'$\delta y$', fontsize=12) -plt.tight_layout() -plt.show() - -""" -A Partially Smooth Function can have derivatives constrained via :math:`{m \geq a}` -where :math:`{a}` is -any order above 2 or it can have a set of derivatives that are allowed to cross -zero. For the first case we can once again use the 'constraints' keyword -argument. For example we can constrain derivatives with orders :math:`{\geq 3}` which will -allow the :math:`{1^{st}}` and :math:`{2^{nd}}` order derivatives to cross zero. -This is useful when our -data features an inflection point we want to model with our fit. - -.. code:: bash - - res = smooth(x, y, N, constraints=3) - - plt.plot(x, y - res.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() - -""" - -res = smooth(x, y, N, constraints=3) - -plt.plot(x, y - res.y_fit) -plt.xlabel('x', fontsize=12) -plt.ylabel(r'$\delta y$', fontsize=12) -plt.tight_layout() -plt.show() - -""" -To allow a particular set of derivatives to cross zero we use the -'zero_crossings' keyword. In the example below we are lifting the constraints -on the :math:`{3^{rd}}`, :math:`{4^{th}}` and :math:`{5^{th}}` order derivatives -but our minimum constrained derivative is still set at the default 2. Therefore -this PSF has derivatives of order :math:`{m = [2, 6, 7, 8, 9]}` -constrained via the condition at the begining of this example code. - -.. code:: - - res = smooth(x, y, N, zero_crossings=[3, 4, 5]) - - plt.plot(x, y - res.y_fit) - plt.xlabel('x', fontsize=12) - plt.ylabel(r'$\delta y$', fontsize=12) - plt.tight_layout() - plt.show() -""" - -res = smooth(x, y, N, zero_crossings=[3, 4, 5]) - -plt.plot(x, y - res.y_fit) -plt.xlabel('x', fontsize=12) -plt.ylabel(r'$\delta y$', fontsize=12) -plt.tight_layout() -plt.show() - -""" -While PSFs can seem like an attractive way to improve the quality of fit they -are less 'smooth' than a MSF or CSF and consequently they can introduce -additional turning points in to your residuals obscuring any signals of -intrest. -""" diff --git a/example_codes/turning_points.py b/example_codes/turning_points.py deleted file mode 100644 index 4f90281..0000000 --- a/example_codes/turning_points.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -This example will walk the user through implementing DCF fits to data sets with -turning points and inflection points. It builds on the details in the -'Simple Example Code' and uses the 'constraints' keyword argument introduced -there. The 'constraints' keyword argument is used to adjust the type of DCF that -is being fitted. Recall that by default ``maxsmooth`` implements a Maximally -Smooth Function or MSF with constraints=2 i.e. derivatives of order :math:`{m \geq 2}` -constrained so that they do not cross zero. This allows for turning points in the -DCF as illustrated below. - -We start by generating some noisy data that we know will include a turning point -and defining the order of the DCF we would like to fit. - -.. code:: bash - - import numpy as np - - x = np.linspace(-10, 10, 100) - noise = np.random.normal(0, 0.02, 100) - y = x**(2) + noise - - N = 10 - -""" -import numpy as np - -x = np.linspace(-10, 10, 100) -noise = np.random.normal(0, 0.02, 100) -y = x**(2) + noise - -N = 10 - -""" - -We can go ahead and plot this data just to double check it features a turning -point. - -.. code:: bash - - import matplotlib.pyplot as plt - - plt.plot(x, y) - plt.xlabel('x', fontsize=12) - plt.ylabel('y', fontsize=12) - plt.show() - -""" - -import matplotlib.pyplot as plt - -plt.plot(x, y) -plt.xlabel('x', fontsize=12) -plt.ylabel('y', fontsize=12) -plt.show() - -""" -As already stated ``maxsmooth`` does not constrain the first derivative of the -DCF by default so we can go ahead and fit the data. - -.. code:: bash - - from maxsmooth.DCF import smooth - - res = smooth(x, y, N) - -""" - -from maxsmooth.DCF import smooth - -res = smooth(x, y, N) - -""" -If we than plot the resultant residuals we will see that despite the data -having a turning point present we have recovered the Gaussian noise. - -.. code:: bash - - plt.plot(x, y- res.y_fit, label='Recovered Noise') - plt.plot(x, noise, label='Actual Noise') - plt.ylabel(r'$\delta y$', fontsize=12) - plt.xlabel('x', fontsize=12) - plt.legend() - plt.show() - -""" - -plt.plot(x, y- res.y_fit, label='Recovered Noise') -plt.plot(x, noise, label='Actual Noise') -plt.ylabel(r'$\delta y$', fontsize=12) -plt.xlabel('x', fontsize=12) -plt.legend() -plt.show() - -""" -To illustrate what happens when there is an inflection point in the data we can -define some sinusoidal data as so. - -.. code:: bash - - x = np.linspace(1, 5, 100) - noise = np.random.normal(0, 0.02, 100) - y = np.sin(x) + noise - - N = 10 - - plt.plot(x, y) - plt.xlabel('x', fontsize=12) - plt.ylabel('y', fontsize=12) - plt.show() - -""" -x = np.linspace(1, 5, 100) -noise = np.random.normal(0, 0.02, 100) -y = np.sin(x) + noise - -N = 10 - -plt.plot(x, y) -plt.xlabel('x', fontsize=12) -plt.ylabel('y', fontsize=12) -plt.show() - -""" -If we proceed to fit this with smooth() in its default settings we will get a -poor fit as by default the second derivative is constrained. We need to lift this -constraint to allow for the prominent inflection point to be modelled. We do this -by setting the keyword argument constraints=3 creating a Partially Smooth Function -or PSF. - -.. code:: bash - - res_msf = smooth(x, y, N) - res_psf = smooth(x, y, N, constraints=3) - - plt.plot(x, y, label='Data') - plt.plot(x, res_msf.y_fit, label=r'MSF fit, $m \geq 2$') - plt.plot(x, res_psf.y_fit, label=r'PSF fit, $m \geq 3$') - plt.xlabel('x', fontsize=12) - plt.ylabel('y', fontsize=12) - plt.legend() - plt.show() - -""" - -res_msf = smooth(x, y, N) -res_psf = smooth(x, y, N, constraints=3) - -plt.plot(x, y, label='Data') -plt.plot(x, res_msf.y_fit, label=r'MSF fit, $m \geq 2$') -plt.plot(x, res_psf.y_fit, label=r'PSF fit, $m \geq 3$') -plt.xlabel('x', fontsize=12) -plt.ylabel('y', fontsize=12) -plt.legend() -plt.show() - -""" -Finally, we can plot the residuals to further see that by lifting the constraint on the -second derivative we have allowed an inflection point in the data. - -.. code:: - - plt.plot(x, y- res_psf.y_fit, label='Recovered Noise') - plt.plot(x, noise, label='Actual Noise') - plt.ylabel(r'$\delta y$', fontsize=12) - plt.xlabel('x', fontsize=12) - plt.legend() - plt.show() - -""" - -plt.plot(x, y - res_psf.y_fit, label='Recovered Noise') -plt.plot(x, noise, label='Actual Noise') -plt.ylabel(r'$\delta y$', fontsize=12) -plt.xlabel('x', fontsize=12) -plt.legend() -plt.show() diff --git a/example_notebooks/Data/x.npy b/example_notebooks/Data/x.npy deleted file mode 100755 index 8e1e392..0000000 Binary files a/example_notebooks/Data/x.npy and /dev/null differ diff --git a/example_notebooks/Data/y.npy b/example_notebooks/Data/y.npy deleted file mode 100755 index b648c7c..0000000 Binary files a/example_notebooks/Data/y.npy and /dev/null differ diff --git a/example_notebooks/best_basis_example.ipynb b/example_notebooks/best_basis_example.ipynb deleted file mode 100644 index 6dec7fb..0000000 --- a/example_notebooks/best_basis_example.ipynb +++ /dev/null @@ -1,1512 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Best Basis Function Example\n", - "\n", - "This function can be used to identify which of the built in DCFs\n", - "fits the data best before running joint fits.\n", - "\n", - "To use it we begin by loading in the data," - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "x = np.load('Data/x.npy')\n", - "y = np.load('Data/y.npy')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and then importing the basis_test() function." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from maxsmooth.best_basis import basis_test" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To call the function we use (this will take a long time to run but the resultant graph saved in the base_dir is the important result(see below))," - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.03592109680175781\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [-1]\n", - "Objective Function Value: 1497655.9820663128\n", - "Parameters: [[ 6.10582544e+03 -9.31235111e+01 3.66394611e-01]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.0417943000793457\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 1]\n", - "Objective Function Value: 630224.6415792715\n", - "Parameters: [[ 8.38821635e+03 -1.68289622e+02 1.14247268e+00 -2.53882819e-03]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.1751255989074707\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 211039.40071597954\n", - "Parameters: [[ 1.16900137e+04 -2.99708886e+02 2.97634623e+00 -1.32282055e-02\n", - " 2.20470092e-05]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.2592034339904785\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 47901.207481682766\n", - "Parameters: [[ 1.65445670e+04 -5.25932298e+02 6.94621286e+00 -4.63081000e-02\n", - " 1.54360381e-04 -2.05813906e-07]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.6042420864105225\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [-1 1 -1 1 -1]\n", - "Objective Function Value: 13609.251008807014\n", - "Parameters: [[ 2.20332485e+04 -8.28924147e+02 1.35734950e+01 -1.20252438e-01\n", - " 6.01262685e-04 -1.60336853e-06 1.78152216e-09]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.6632649898529053\n", - "Polynomial Order: 8\n", - "Number of Constrained Derivatives: 6\n", - "Signs : [-1 1 -1 1 -1 1]\n", - "Objective Function Value: 2900.7373462253577\n", - "Parameters: [[ 2.91306119e+04 -1.27413327e+03 2.50059677e+01 -2.76870964e-01\n", - " 1.84592628e-03 -7.38418827e-06 1.64103760e-08 -1.56299428e-11]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.319108486175537\n", - "Polynomial Order: 9\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 758.0432645753858\n", - "Parameters: [[ 3.64793808e+04 -1.80544023e+03 4.11385681e+01 -5.46361938e-01\n", - " 4.56599979e-03 -2.44589063e-05 8.18866109e-08 -1.56653779e-10\n", - " 1.31110351e-13]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.0699102878570557\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 1 -1 1 -1 -1]\n", - "Objective Function Value: 110.66485798872728\n", - "Parameters: [[ 4.80575449e+04 -2.73313091e+03 7.30162116e+01 -1.16470058e+00\n", - " 1.20545148e-02 -8.34219495e-05 3.85032270e-07 -1.14204346e-09\n", - " 1.97519989e-12 -1.51765259e-15]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.5789389610290527\n", - "Polynomial Order: 11\n", - "Number of Constrained Derivatives: 9\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 1]\n", - "Objective Function Value: 7.393381422307085\n", - "Parameters: [[ 6.61048115e+04 -4.38869929e+03 1.39334974e+02 -2.69510151e+00\n", - " 3.46370773e-02 -3.06670194e-04 1.88684080e-06 -7.95039645e-09\n", - " 2.19369588e-11 -3.57785869e-14 2.61871132e-17]]\n", - "Method: qp-sign_flipping\n", - "Model: polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "Unable to fit with N = 12 and polynomial.\n", - "#############################################################\n", - "Unable to fit with N = 13 and polynomial.\n", - "#############################################################\n", - "Unable to fit with N = 14 and polynomial.\n", - "#############################################################\n", - "Unable to fit with N = 15 and polynomial.\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.009327888488769531\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [-1]\n", - "Objective Function Value: 1497655.9820663128\n", - "Parameters: [[ 12.36642292 -18.95601043 7.4959134 ]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.0300595760345459\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 1]\n", - "Objective Function Value: 630306.4266929075\n", - "Parameters: [[ 16.98854164 -34.25487279 23.37145059 -5.21966227]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.11980867385864258\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 211039.40189998347\n", - "Parameters: [[ 23.67634888 -61.00806017 60.89181689 -27.1997117 4.55618066]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.1789262294769287\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 47901.35857551199\n", - "Parameters: [[ 33.50848936 -107.05748708 142.10947803 -95.21813512 31.89967827\n", - " -4.2747717 ]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.24662494659423828\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [-1 1 -1 1 -1]\n", - "Objective Function Value: 13609.355167771822\n", - "Parameters: [[ 44.62494552 -168.73359707 277.6937056 -247.26109239 124.25494289\n", - " -33.30199816 3.71891001]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.3192000389099121\n", - "Polynomial Order: 8\n", - "Number of Constrained Derivatives: 6\n", - "Signs : [-1 1 -1 1 -1 1]\n", - "Objective Function Value: 2903.3198075710584\n", - "Parameters: [[ 58.98546017 -259.27232927 511.36748101 -569.00750534 381.2492113\n", - " -153.26861131 34.23145903 -3.27658333]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.5624468326568604\n", - "Polynomial Order: 9\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 809.693959133224\n", - "Parameters: [[ 73.30225501 -363.25231163 828.53418495 -1101.26443095\n", - " 920.97552878 -493.66766067 165.3869673 -31.66138003\n", - " 2.65177368]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.6008212566375732\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 1 -1 1 -1 1]\n", - "Objective Function Value: 185.52271180814768\n", - "Parameters: [[ 91.46795521 -507.30324742 1317.98818902 -2039.73047767\n", - " 2044.78588402 -1369.43660676 611.61090642 -175.6035025\n", - " 29.41130122 -2.18936377]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.4114351272583008\n", - "Polynomial Order: 11\n", - "Number of Constrained Derivatives: 9\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 47.76711940672956\n", - "Parameters: [[ 1.10433404e+02 -6.74694461e+02 1.96060962e+03 -3.45618181e+03\n", - " 4.03623060e+03 -3.24266050e+03 1.81047132e+03 -6.93183037e+02\n", - " 1.74170087e+02 -2.59331608e+01 1.73760024e+00]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.7065720558166504\n", - "Polynomial Order: 12\n", - "Number of Constrained Derivatives: 10\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 1]\n", - "Objective Function Value: 11.447228023254151\n", - "Parameters: [[ 1.31665870e+02 -8.78310689e+02 2.82091833e+03 -5.57484226e+03\n", - " 7.42493812e+03 -6.95125927e+03 4.65431693e+03 -2.22648468e+03\n", - " 7.45559866e+02 -1.66438985e+02 2.22935804e+01 -1.35732196e+00]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 3.734036445617676\n", - "Polynomial Order: 13\n", - "Number of Constrained Derivatives: 11\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 1 1]\n", - "Objective Function Value: 11.444112769545562\n", - "Parameters: [[ 1.31670056e+02 -8.78235592e+02 2.82006690e+03 -5.57126086e+03\n", - " 7.41642436e+03 -6.93819598e+03 4.64057945e+03 -2.21631806e+03\n", - " 7.40244334e+02 -1.64510032e+02 2.18301067e+01 -1.29088543e+00\n", - " -4.30795984e-03]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 4.387500286102295\n", - "Polynomial Order: 14\n", - "Number of Constrained Derivatives: 12\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 1 -1 -1]\n", - "Objective Function Value: 11.306401917997064\n", - "Parameters: [[ 1.31886176e+02 -8.80399438e+02 2.82969289e+03 -5.59647987e+03\n", - " 7.45988774e+03 -6.99026897e+03 4.68517988e+03 -2.24389363e+03\n", - " 7.52468646e+02 -1.68292222e+02 2.25991379e+01 -1.38038730e+00\n", - " -6.33485842e-04 1.64143756e-04]]\n", - "Method: qp-sign_flipping\n", - "Model: normalised_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "Unable to fit with N = 15 and normalised_polynomial.\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.013198614120483398\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [-1]\n", - "Objective Function Value: 262026.00445670684\n", - "Parameters: [[ 453.94789325 -3304.94760396 12848.48072557]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.03512096405029297\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 1]\n", - "Objective Function Value: 20748.38577973272\n", - "Parameters: [[ 479.80346345 -2805.11166824 9869.35270721 -18917.2855155 ]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.09961462020874023\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 587.9790178918192\n", - "Parameters: [[ 493.93218499 -2795.75641106 8052.98018567 -18377.67801161\n", - " 26418.68826622]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.24476146697998047\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 11.028501878393179\n", - "Parameters: [[ 494.06573938 -2844.80441354 8092.4182417 -15433.61338446\n", - " 25979.08832504 -29830.77776964]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.2329564094543457\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [-1 1 -1 1 -1]\n", - "Objective Function Value: 0.89536054363089\n", - "Parameters: [[ 493.80154836 -2844.28156168 8166.45601207 -15486.90142631\n", - " 23131.20524579 -30571.97932195 19721.62457774]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.560697078704834\n", - "Polynomial Order: 8\n", - "Number of Constrained Derivatives: 6\n", - "Signs : [-1 1 -1 1 -1 -1]\n", - "Objective Function Value: 0.9406631923547155\n", - "Parameters: [[ 493.80023085 -2844.34963757 8166.99878245 -15472.00785243\n", - " 23124.44852986 -31080.54700831 18878.50472739 1147.79642835]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.0979912281036377\n", - "Polynomial Order: 9\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 1 -1 1 -1 -1 1]\n", - "Objective Function Value: 0.40074503248872084\n", - "Parameters: [[ 493.73147909 -2843.98620783 8186.49710985 -15478.81636806\n", - " 22406.49923193 -31992.90214159 21526.38394887 979.17792012\n", - " -124.11814625]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.904883861541748\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 1 -1 1 1 1]\n", - "Objective Function Value: 0.11696819370957201\n", - "Parameters: [[ 4.93727942e+02 -2.84305353e+03 8.18748736e+03 -1.55936628e+04\n", - " 2.22192752e+04 -2.90869629e+04 2.86921055e+04 -1.43285866e+03\n", - " -1.62422320e+02 -1.71812810e+01]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.0069191455841064\n", - "Polynomial Order: 11\n", - "Number of Constrained Derivatives: 9\n", - "Signs : [-1 1 -1 1 -1 1 -1 -1 1]\n", - "Objective Function Value: 0.04254069968463619\n", - "Parameters: [[ 4.93730270e+02 -2.84262833e+03 8.18634589e+03 -1.56466747e+04\n", - " 2.22340146e+04 -2.76210614e+04 3.02713341e+04 -6.44160832e+03\n", - " 2.76020635e+02 2.90907510e+01 -2.91912817e+00]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.904545783996582\n", - "Polynomial Order: 12\n", - "Number of Constrained Derivatives: 10\n", - "Signs : [-1 1 -1 1 -1 -1 -1 -1 1 1]\n", - "Objective Function Value: 0.1475139705449417\n", - "Parameters: [[ 4.93726414e+02 -2.84317750e+03 8.18805379e+03 -1.55773958e+04\n", - " 2.22078641e+04 -2.95572415e+04 2.81824224e+04 1.82379274e+02\n", - " 1.08308532e+02 2.36440277e+01 -2.36075430e+00 -2.06538597e-01]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 8.326946020126343\n", - "Polynomial Order: 13\n", - "Number of Constrained Derivatives: 11\n", - "Signs : [-1 1 -1 1 -1 -1 1 1 1 1 -1]\n", - "Objective Function Value: 0.14565940838527172\n", - "Parameters: [[ 4.93726433e+02 -2.84316906e+03 8.18804865e+03 -1.55784804e+04\n", - " 2.22066486e+04 -2.95272306e+04 2.82509030e+04 1.04044343e+02\n", - " -5.67972646e+01 -1.64494837e+01 -2.21997909e+00 -1.91311147e-01\n", - " 1.60193547e-02]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 10.218279600143433\n", - "Polynomial Order: 14\n", - "Number of Constrained Derivatives: 12\n", - "Signs : [-1 1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1]\n", - "Objective Function Value: 0.1435267077058029\n", - "Parameters: [[ 4.93726509e+02 -2.84316083e+03 8.18801509e+03 -1.55795097e+04\n", - " 2.22075817e+04 -2.94984276e+04 2.82708985e+04 2.83685456e-06\n", - " 5.98531490e-06 1.04120998e-05 1.33300887e-05 1.17140643e-05\n", - " 6.31795645e-06 1.57945090e-06]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 21.247979164123535\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 13\n", - "Signs : [-1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1]\n", - "Objective Function Value: 0.1435267076082476\n", - "Parameters: [[ 4.93726509e+02 -2.84316083e+03 8.18801509e+03 -1.55795097e+04\n", - " 2.22075817e+04 -2.94984276e+04 2.82708985e+04 1.51859740e-07\n", - " -4.08281683e-07 1.22320235e-06 -3.13468739e-06 6.06576535e-06\n", - " -8.12554140e-06 6.68259104e-06 -2.53948471e-06]]\n", - "Method: qp-sign_flipping\n", - "Model: log_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.010521650314331055\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [1]\n", - "Objective Function Value: 0.00020869518403223415\n", - "Parameters: [[ 7.37478779 -2.16653462 -0.08532772]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.0279848575592041\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 -1]\n", - "Objective Function Value: 7.945962178249629e-05\n", - "Parameters: [[ 7.65919825 -2.33438787 -0.13846147 0.03268283]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.0577082633972168\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 -1 -1]\n", - "Objective Function Value: 2.9519589230167703e-05\n", - "Parameters: [[ 8.1053049 -3.37301465 0.73354588 -0.28265339 0.04163454]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.1259753704071045\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 4.7866686530078086e-05\n", - "Parameters: [[ 11.97420861 -12.17151273 8.79231969 -4.01638997 0.92111621\n", - " -0.08465043]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.19004392623901367\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [ 1 1 1 -1 1]\n", - "Objective Function Value: 1.6199486746273087e-05\n", - "Parameters: [[ 7.20425842e+00 -1.25851203e+00 -1.29710110e+00 7.07429895e-01\n", - " -2.08993686e-01 3.19345356e-02 -2.13868931e-03]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.17575573921203613\n", - "Polynomial Order: 8\n", - "Number of Constrained Derivatives: 6\n", - "Signs : [-1 -1 -1 -1 1 -1]\n", - "Objective Function Value: 2.991082688352879e-06\n", - "Parameters: [[ 7.09636858e+00 -5.38298606e-01 -2.60833141e+00 1.83164538e+00\n", - " -7.27250867e-01 1.61416203e-01 -1.86362727e-02 9.57733373e-04]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.40761327743530273\n", - "Polynomial Order: 9\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 -1 -1 -1 -1 -1 -1]\n", - "Objective Function Value: 2.916516148138412e-06\n", - "Parameters: [[ 7.65649928e+00 -2.41017151e+00 9.12117226e-03 -1.38044695e-01\n", - " 1.25217126e-01 -4.83460774e-02 8.88898310e-03 -9.96226129e-04\n", - " 1.27830333e-04]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.4307398796081543\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [ 1 1 1 -1 1 -1 -1 -1]\n", - "Objective Function Value: 1.2877057064021909e-06\n", - "Parameters: [[ 7.47086108e+00 -1.85778092e+00 -7.70612804e-01 5.04276938e-01\n", - " -1.93072803e-01 4.35258774e-02 -5.62976045e-03 4.83383793e-04\n", - " -6.67385637e-05 7.58001926e-06]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.0655951499938965\n", - "Polynomial Order: 11\n", - "Number of Constrained Derivatives: 9\n", - "Signs : [ 1 1 1 1 1 1 1 1 -1]\n", - "Objective Function Value: 1.22651845499378e-07\n", - "Parameters: [[ 7.66499179e+00 -2.48235449e+00 1.81251403e-01 -3.93440648e-01\n", - " 3.70220793e-01 -1.90713385e-01 5.67481709e-02 -1.01862703e-02\n", - " 1.34677822e-03 -1.51264234e-04 5.05051666e-06]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.048088788986206\n", - "Polynomial Order: 12\n", - "Number of Constrained Derivatives: 10\n", - "Signs : [ 1 -1 1 -1 1 1 -1 -1 -1 1]\n", - "Objective Function Value: 8.493867562022565e-08\n", - "Parameters: [[ 7.15806022e+00 -1.37250138e+00 -6.97182324e-01 -1.47831045e-01\n", - " 4.06514813e-01 -2.21656535e-01 5.82978574e-02 -8.21292230e-03\n", - " 8.48742170e-04 -1.23176678e-04 1.25412339e-05 -3.80576240e-07]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.382296085357666\n", - "Polynomial Order: 13\n", - "Number of Constrained Derivatives: 11\n", - "Signs : [-1 1 -1 -1 1 -1 -1 -1 -1 -1 -1]\n", - "Objective Function Value: 3.055324946151109e-08\n", - "Parameters: [[ 7.68301809e+00 -2.42129733e+00 -1.21920620e-01 8.87258608e-02\n", - " -3.34399386e-02 6.34430704e-03 -6.34173320e-04 1.16288529e-04\n", - " -1.87369472e-05 -4.83937608e-06 1.61605413e-06 -1.47912020e-07\n", - " 1.28395199e-08]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.7251393795013428\n", - "Polynomial Order: 14\n", - "Number of Constrained Derivatives: 12\n", - "Signs : [-1 1 1 -1 -1 1 1 1 -1 -1 -1 -1]\n", - "Objective Function Value: 5.052210796660854e-09\n", - "Parameters: [[ 7.70249307e+00 -2.51259896e+00 2.59066033e-02 -3.20493055e-02\n", - " 2.47887894e-02 -1.25645968e-02 4.15038782e-03 -7.67885898e-04\n", - " 1.33219021e-05 2.76349336e-05 -5.86505921e-06 5.71531717e-07\n", - " -5.14761554e-08 4.14750450e-09]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 7.8902459144592285\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 13\n", - "Signs : [ 1 1 1 1 1 1 1 -1 1 1 1 1 1]\n", - "Objective Function Value: 3.9988211853475377e-10\n", - "Parameters: [[ 7.58636610e+00 -1.95062337e+00 -1.17750057e+00 1.44976795e+00\n", - " -1.12449659e+00 5.65617501e-01 -1.82219282e-01 3.53990174e-02\n", - " -3.54739168e-03 1.05465960e-04 2.96900506e-06 1.38590451e-07\n", - " -9.43923255e-08 7.60301203e-09 -5.70637108e-10]]\n", - "Method: qp-sign_flipping\n", - "Model: loglog_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.027128934860229492\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [-1]\n", - "Objective Function Value: 1497655.982161551\n", - "Parameters: [[ 763.36083633 -993.22266888 611.8830205 ]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.04029440879821777\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 1]\n", - "Objective Function Value: 630224.644018191\n", - "Parameters: [[ 763.14201809 -989.77981622 635.9784479 -127.32301211]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.09873127937316895\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 211039.44680535453\n", - "Parameters: [[ 762.89171918 -987.2431695 631.33445872 -221.1328652 31.62203043]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.24682950973510742\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 47901.37420552028\n", - "Parameters: [[ 762.65780486 -985.2412307 614.13585004 -286.76873371 73.79956068\n", - " -8.20815904]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.22072792053222656\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [-1 1 -1 1 -1]\n", - "Objective Function Value: 13609.35540586465\n", - "Parameters: [[ 762.41854572 -984.57593824 607.13354585 -297.9602614 104.54925968\n", - " -21.31483194 1.93965164]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.3488931655883789\n", - "Polynomial Order: 8\n", - "Number of Constrained Derivatives: 6\n", - "Signs : [-1 1 -1 1 -1 1]\n", - "Objective Function Value: 2902.5169943483393\n", - "Parameters: [[ 7.62202860e+02 -9.84364529e+02 6.06047007e+02 -2.91056878e+02\n", - " 1.24809920e+02 -3.52279535e+01 5.95267808e+00 -4.58356651e-01]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.9981844425201416\n", - "Polynomial Order: 9\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 809.7014776580646\n", - "Parameters: [[ 7.62172847e+02 -9.84136933e+02 6.05895906e+02 -2.90214281e+02\n", - " 1.23706030e+02 -4.32258796e+01 1.02246841e+01 -1.47600455e+00\n", - " 9.84987472e-02]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.253005027770996\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 1 -1 1 -1 1]\n", - "Objective Function Value: 184.60371570390348\n", - "Parameters: [[ 7.62151272e+02 -9.83971886e+02 6.05787919e+02 -2.89820202e+02\n", - " 1.21756217e+02 -4.80855392e+01 1.42165672e+01 -2.89702908e+00\n", - " 3.65135611e-01 -2.15000653e-02]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.3548920154571533\n", - "Polynomial Order: 11\n", - "Number of Constrained Derivatives: 9\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 47.76005929110385\n", - "Parameters: [[ 7.62130157e+02 -9.83944413e+02 6.05680989e+02 -2.89775487e+02\n", - " 1.21403604e+02 -4.76818815e+01 1.64710837e+01 -4.22631277e+00\n", - " 7.56892998e-01 -8.46701216e-02 4.46078295e-03]]\n", - "Method: qp-sign_flipping\n", - "Model: legendre\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "Unable to fit with N = 12 and legendre.\n", - "#############################################################\n", - "Unable to fit with N = 13 and legendre.\n", - "#############################################################\n", - "Unable to fit with N = 14 and legendre.\n", - "#############################################################\n", - "Unable to fit with N = 15 and legendre.\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.0695505142211914\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [-1]\n", - "Objective Function Value: 428891.93680877023\n", - "Parameters: [[ 3.4436808 -21.98172663 40.93989028]]\n", - "Method: qp-sign_flipping\n", - "Model: exponential\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.20555758476257324\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 1]\n", - "Objective Function Value: 53253.64560572796\n", - "Parameters: [[ -1.7465104 19.99091056 -66.61150925 87.73786416]]\n", - "Method: qp-sign_flipping\n", - "Model: exponential\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.6782536506652832\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 1775.362379316301\n", - "Parameters: [[ 2.60479997 -31.96386002 156.00379446 -317.45254094 264.8566373 ]]\n", - "Method: qp-sign_flipping\n", - "Model: exponential\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.456045389175415\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 66.83794134233632\n", - "Parameters: [[ -3.1025033 47.47378033 -273.65180112 811.34191126\n", - " -1176.70940405 716.89176045]]\n", - "Method: qp-sign_flipping\n", - "Model: exponential\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.4152514934539795\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [-1 1 -1 1 -1]\n", - "Objective Function Value: 2.9736970661570026\n", - "Parameters: [[ 2.73959566e+00 -4.72620629e+01 3.51586559e+02 -1.33920007e+03\n", - " 2.89206760e+03 -3.30189577e+03 1.62078163e+03]]\n", - "Method: qp-sign_flipping\n", - "Model: exponential\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Unable to fit with N = 8 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 9 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 10 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 11 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 12 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 13 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 14 and exponential.\n", - "#############################################################\n", - "Unable to fit with N = 15 and exponential.\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.009341716766357422\n", - "Polynomial Order: 3\n", - "Number of Constrained Derivatives: 1\n", - "Signs : [-1]\n", - "Objective Function Value: 1497655.9820663123\n", - "Parameters: [[ 4.47491378e+02 -1.94744934e+01 3.66394611e-01]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.0363459587097168\n", - "Polynomial Order: 4\n", - "Number of Constrained Derivatives: 2\n", - "Signs : [-1 1]\n", - "Objective Function Value: 630224.6415792727\n", - "Parameters: [[ 4.37189076e+02 -1.55772024e+01 3.76977518e-01 -2.53882819e-03]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.09820365905761719\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 211039.4007159821\n", - "Parameters: [[ 4.52551586e+02 -1.27670850e+01 3.24057966e-01 -4.36486241e-03\n", - " 2.20470092e-05]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.2899622917175293\n", - "Polynomial Order: 6\n", - "Number of Constrained Derivatives: 4\n", - "Signs : [-1 1 -1 1]\n", - "Objective Function Value: 47901.20745363379\n", - "Parameters: [[ 4.77573114e+02 -1.11422011e+01 2.49549184e-01 -5.04191673e-03\n", - " 5.09336964e-05 -2.05813906e-07]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.4737884998321533\n", - "Polynomial Order: 7\n", - "Number of Constrained Derivatives: 5\n", - "Signs : [-1 1 -1 1 -1]\n", - "Objective Function Value: 13607.916758044285\n", - "Parameters: [[ 4.91677255e+02 -1.13296255e+01 2.05474005e-01 -4.32009713e-03\n", - " 6.54646582e-05 -5.29078312e-07 1.78165121e-09]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.8551750183105469\n", - "Polynomial Order: 8\n", - "Number of Constrained Derivatives: 6\n", - "Signs : [-1 1 -1 1 -1 1]\n", - "Objective Function Value: 2900.709307721885\n", - "Parameters: [[ 4.97990177e+02 -1.20537193e+01 1.86573401e-01 -3.28053519e-03\n", - " 6.62909463e-05 -8.03769342e-07 5.41424050e-09 -1.56303390e-11]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.078805685043335\n", - "Polynomial Order: 9\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 772.9425170829429\n", - "Parameters: [[ 4.96107972e+02 -1.23233383e+01 1.99987304e-01 -2.88274586e-03\n", - " 5.24141034e-05 -8.55021954e-07 8.71944769e-09 -5.07957333e-11\n", - " 1.29420835e-13]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.2513985633850098\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 1 -1 1 -1 -1]\n", - "Objective Function Value: 114.35715789988733\n", - "Parameters: [[ 4.93945308e+02 -1.24015979e+01 2.14191333e-01 -2.88025094e-03\n", - " 3.92954289e-05 -7.14295143e-07 1.07936641e-08 -1.04844768e-10\n", - " 5.92294586e-13 -1.48096272e-15]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.429729700088501\n", - "Polynomial Order: 11\n", - "Number of Constrained Derivatives: 9\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 1]\n", - "Objective Function Value: 6.119688829041132\n", - "Parameters: [[ 4.93522916e+02 -1.22743729e+01 2.16147753e-01 -3.24964824e-03\n", - " 4.04428700e-05 -4.67654196e-07 7.63661430e-09 -1.31195193e-10\n", - " 1.53730044e-12 -1.03192627e-14 2.97716087e-17]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.5962612628936768\n", - "Polynomial Order: 12\n", - "Number of Constrained Derivatives: 10\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 -1]\n", - "Objective Function Value: 0.5185149451757762\n", - "Parameters: [[ 4.93749194e+02 -1.22715588e+01 2.13554583e-01 -3.23392984e-03\n", - " 4.49032230e-05 -5.26185438e-07 5.53170212e-09 -8.55052709e-11\n", - " 1.56464678e-12 -1.95516746e-14 1.34648804e-16 -3.88201795e-19]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 5.939860820770264\n", - "Polynomial Order: 13\n", - "Number of Constrained Derivatives: 11\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 -1 1]\n", - "Objective Function Value: 0.024664728941439496\n", - "Parameters: [[ 4.93753802e+02 -1.22835521e+01 2.13667592e-01 -3.17664105e-03\n", - " 4.40184810e-05 -5.95055793e-07 6.97643747e-09 -6.18842112e-11\n", - " 7.63057079e-13 -1.72076467e-14 2.62995092e-16 -2.04365609e-18\n", - " 6.35919153e-21]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 11.123537302017212\n", - "Polynomial Order: 14\n", - "Number of Constrained Derivatives: 12\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 -1 -1 1]\n", - "Objective Function Value: 0.0017622106858247515\n", - "Parameters: [[ 4.93739171e+02 -1.22820508e+01 2.13925336e-01 -3.18871280e-03\n", - " 4.33358180e-05 -5.67263587e-07 7.50752375e-09 -8.77624134e-11\n", - " 7.14746927e-13 -7.11913360e-15 1.82932845e-16 -3.20542666e-18\n", - " 2.70044794e-20 -8.85381348e-23]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 10.028169631958008\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 13\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 0.00010176158680053807\n", - "Parameters: [[ 4.93741961e+02 -1.22812852e+01 2.13851335e-01 -3.19393950e-03\n", - " 4.36671579e-05 -5.58340158e-07 6.95734293e-09 -9.19852388e-11\n", - " 1.12410726e-12 -8.39375126e-15 4.93063647e-17 -1.78002845e-18\n", - " 3.93656284e-20 -3.62369116e-22 1.23144096e-24]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "basis_test(x, y, base_dir='examples/', N=np.arange(3, 16, 1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The function only requires the data but we can provide it with a base directory,\n", - "fit type and range of DCF orders to test. By defualt it uses the sign navigating\n", - "algorithm and tests $N = 3 - 13$. Here we test the range\n", - "$N = 3 - 15$.\n", - "The resultant graph is saved in the\n", - "base directory and the example generated here is shown below." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from IPython.display import IFrame\n", - "IFrame('./examples/Basis_functions.pdf', width=500, height=350)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The graph shows us which basis is the optimum for solving this problem from the\n", - "built in library (that which can reach the minimum $\\chi^2$). If we\n", - "were to go to higher N we would also find that the $\\chi^2$ value\n", - "would stop decreasing in value. The value of $N$ for which this occurs at is the\n", - "optimum DCF order. (See the ``maxsmooth`` paper for a real world application\n", - "of this concept.)\n", - "\n", - "We can also provide this function with additional arguments such as the\n", - "fit type, minimum constrained derivative, directional exploration limits\n", - "ect. (see the ``maxsmooth`` Functions section of the documentation)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/example_notebooks/chi_dist_example.ipynb b/example_notebooks/chi_dist_example.ipynb deleted file mode 100644 index 2faa2ff..0000000 --- a/example_notebooks/chi_dist_example.ipynb +++ /dev/null @@ -1,171 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# $\\chi^2$ Distribution Example\n", - "\n", - "This example will show you how to generate a plot of the $\\chi^2$\n", - "distribution as a function of the descrete sign combinations on the constrained derivatives.\n", - "\n", - "First you will need to import your data and fit this using ``maxsmooth`` as was done in the simple example code." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 13.362407922744751\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 1 -1 1 -1 -1]\n", - "Objective Function Value: 114.35715789988733\n", - "Parameters: [[ 4.93945308e+02 -1.24015979e+01 2.14191333e-01 -2.88025094e-03\n", - " 3.92954289e-05 -7.14295143e-07 1.07936641e-08 -1.04844768e-10\n", - " 5.92294586e-13 -1.48096272e-15]]\n", - "Method: qp\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "x = np.load('Data/x.npy')\n", - "y = np.load('Data/y.npy')\n", - "\n", - "from maxsmooth.DCF import smooth\n", - "\n", - "N = 10\n", - "\n", - "result = smooth(x, y, N, base_dir='examples/',\n", - " data_save=True, fit_type='qp')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we have used some additional keyword arguments for the 'smooth' fitting function. 'data_save' ensures that the files containing the tested sign combinations and the corresponding objective function evaluations exist in the base directory which we have changed to 'base_dir='examples/''. These files are essential for the plotting the $\\chi^2$ distribution and are not saved by ``maxsmooth`` without 'data_save=True'. We have also set the 'fit_type' to 'qp' rather than the default 'qp-sign_flipping'. This ensures that all of the available sign combinations are tested rather than a sampled set giving us a full picture of the distribution when we plot it. We have used the default DCF model to fit this data.\n", - "\n", - "We can import the 'chi_plotter' like so," - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from maxsmooth.chidist_plotter import chi_plotter" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and produce the fit which gets placed in the base directory with the following code," - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "chi_plotter(N, base_dir='examples/', fit_type='qp')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We pass the same 'base_dir' as before so that the plotter can find the correct output files. We also give the function the same 'fit_type' used for the fitting which ensures that the files can be read.\n", - "\n", - "The resultant plot is stored in 'base_dir' and the yellow star shows the global minimum. This can be used to determine how well\n", - "the sign sampling approach using a descent and directional exploration\n", - "can find the global minimum. If the distribution looks like noise then it is unlikely the sign sampling algorithm will consistently find the global minimum. Rather it will likely repeatedly return the local minima found after the descent algorithm and you should use the 'qp' method testing all available sign combinations in any future fits to the data with this DCF model.\n", - "\n", - "We can visualise the plot in this notebook with the code below." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from IPython.display import IFrame\n", - "IFrame('./examples/chi_distribution.pdf', width=500, height=350)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/example_notebooks/new_basis_program.ipynb b/example_notebooks/new_basis_program.ipynb deleted file mode 100644 index e2676f8..0000000 --- a/example_notebooks/new_basis_program.ipynb +++ /dev/null @@ -1,280 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# New Basis Example\n", - "\n", - "This example code illustrates how to define your own basis function for the DCF model. It implements a modified version of the built in normalised polynomial model but the structure is the same for more elaborate models.\n", - "\n", - "As always we need to import the data, define an order $N$ and import the function fitting routine, smooth()." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from maxsmooth.DCF import smooth\n", - "\n", - "x = np.load('Data/x.npy')\n", - "y = np.load('Data/y.npy')\n", - "\n", - "N = 10" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are several requirements needed to define a new basis function completely for ``maxsmooth`` to be able to fit it. They are as summarised below and then examples of each are given in more detail,\n", - "\n", - "- **args:** Additional non-standard arguments needed in the definition of the basis. The standard arguments are the data (x and y), the order of the fit N, the pivot point about which a model can be fit, the derivative order $m$ and the params. While the pivot point is not strictly needed it is a required argument for the functions defining a new basis to help the user in their definition.\n", - "\n", - "- **basis_functions:** This function defines the basis of the DCF model, $\\phi$ where the model can be generally defined as, \n", - " \n", - " $y = \\sum_{k = 0}^N a_k \\phi_k(x)$ \n", - " \n", - " where $a_k$ are the fit parameters.\n", - "\n", - "- **model:** This is the function described by the equation above.\n", - "\n", - "- **derivative:** This function defines the $m^{th}$ order derivative.\n", - "\n", - "- **derivative_pre:** This function defines the prefactors, $\\mathbf{G}$ on the derivatives where ``CVXOPT``, the quadratic programming routine used, evaluates the constraints as,\n", - "\n", - " $\\mathbf{Ga} \\leq \\mathbf{h}$\n", - " \n", - " where $\\mathbf{a}$ is the matrix of parameters and $\\mathbf{h}$ is the matrix of constraint limits. For more details on this see the ``maxsmooth``paper.\n", - "\n", - "\n", - "We can begin defining our new basis function by defining the aditional arguments needed to fit the model as a list," - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "arguments = [x[-1]*10, y[-1]*10]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next step is to define the basis functions $\\phi$. This needs to be done in a function that has the arguments *(x, y, pivot_point, N, *args)*. 'args' is optional but since we need them for this basis we are passing it in.\n", - "\n", - "The basis functions, $\\phi$, should be an array of dimensions len(x)\n", - "by N and consequently evaluated at each N and x data point as shown below." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def basis_functions(x, y, pivot_point, N, *args):\n", - "\n", - " phi = np.empty([len(x), N])\n", - " for h in range(len(x)):\n", - " for i in range(N):\n", - " phi[h, i] = args[1]*(x[h]/args[0])**i\n", - "\n", - " return phi" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can define the model that we are fitting in a function like that shown below. This is used for evaluating $\\chi^2$ and returning the optimum fitted model once the code has finished running. It requires the arguments *(x, y, pivot_point, N, params, \\*args)* in that order and again where 'args' is optional. 'params' is the parameters of the fit, $\\mathbf{a}$ which should have length $N$.\n", - "\n", - "The function should return the fitted estimate of y." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "def model(x, y, pivot_point, N, params, *args):\n", - "\n", - " y_sum = args[1]*np.sum([\n", - " params[i]*(x/args[0])**i\n", - " for i in range(N)], axis=0)\n", - "\n", - " return y_sum" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next we have to define a function for the derivatives of the model which takes arguments *(m, x, y, N, pivot_point, params, *args)* where $m$ is the derivative order. The function should return the $m^{th}$ order derivative evaluation and is used for checking that the constraints have been met and returning the derivatives of the optimum fit to the user." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "def derivative(m, x, y, N, pivot_point, params, *args):\n", - "\n", - " mth_order_derivative = []\n", - " for i in range(N):\n", - " if i <= m - 1:\n", - " mth_order_derivative.append([0]*len(x))\n", - " for i in range(N - m):\n", - " mth_order_derivative_term = args[1]*np.math.factorial(m+i) / \\\n", - " np.math.factorial(i) * \\\n", - " params[int(m)+i]*(x)**i / \\\n", - " (args[0])**(i + 1)\n", - " mth_order_derivative.append(\n", - " mth_order_derivative_term)\n", - "\n", - " return mth_order_derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally we have to define $\\mathbf{G}$ which is used by ``CVXOPT`` to\n", - "build the derivatives and constrain the functions. It takes arguments\n", - "*(m, x, y, N, pivot_point, \\*args)* and should return the prefactor on the $m^{th}$ order derivative. For a more thorough definition of the\n", - "prefactor on the derivative and an explination of how the problem is\n", - "constrained in quadratic programming see the ``maxsmooth`` paper." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def derivative_pre(m, x, y, N, pivot_point, *args):\n", - "\n", - " mth_order_derivative = []\n", - " for i in range(N):\n", - " if i <= m - 1:\n", - " mth_order_derivative.append([0]*len(x))\n", - " for i in range(N - m):\n", - " mth_order_derivative_term = args[1]*np.math.factorial(m+i) / \\\n", - " np.math.factorial(i) * \\\n", - " (x)**i / \\\n", - " (args[0])**(i + 1)\n", - " mth_order_derivative.append(\n", - " mth_order_derivative_term)\n", - "\n", - " return mth_order_derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With our functions and additional arguments defined we can pass these\n", - "to the ``maxsmooth`` smooth() function as is shown below. This overwrites the built in DCF model but you are still able to modify the fit type i.e. testing all available sign combinations or sampling them." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.430711507797241\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 1 -1 -1 -1 -1 1 -1]\n", - "Objective Function Value: 211043.08775526902\n", - "Parameters: [[ 6.44274707e+00 -2.47768148e+02 3.69078272e+03 -2.46050489e+04\n", - " 6.15121485e+04 -3.93622291e+00 2.73964765e+01 6.32967370e+00\n", - " -7.88321214e-01 8.20907171e-02]]\n", - "Method: qp-sign_flipping\n", - "Model: user_defined\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - } - ], - "source": [ - "result = smooth(x, y, N,\n", - " basis_functions=basis_functions, model=model,\n", - " derivatives=derivative, der_pres=derivative_pre, args=arguments)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The output of the fit can be accessed as before," - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Objective Funtion Evaluations:\n", - " 211043.08775526902\n", - "RMS:\n", - " 45.93942617787786\n", - "Sign Combinations:\n", - " [-1 1 -1 -1 -1 -1 1 -1]\n" - ] - } - ], - "source": [ - "print('Objective Funtion Evaluations:\\n', result.optimum_chi)\n", - "print('RMS:\\n', result.rms)\n", - "#print('Parameters:\\n', result.optimum_params[2])\n", - "#print('Fitted y:\\n', result.y_fit)\n", - "print('Sign Combinations:\\n', result.optimum_signs)\n", - "#print('Derivatives:\\n', result.derivatives)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/example_notebooks/param_plotter_example.ipynb b/example_notebooks/param_plotter_example.ipynb deleted file mode 100644 index ffbfb48..0000000 --- a/example_notebooks/param_plotter_example.ipynb +++ /dev/null @@ -1,260 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Parameter Plotter Example\n", - "\n", - "We can assess the parameter space around the optimum solution\n", - "found using ``maxsmooth`` with the param_plotter() function.\n", - "This can help us identify how well a problem can be solved using the\n", - "sign sampling approach employed by ``maxsmooth`` or simply\n", - "be used to identify correlations between the foreground parameters.\n", - "For more details on this see the ``maxsmooth`` paper.\n", - "\n", - "We begin by importing and fitting the data as with the chi_plotter()\n", - "function illustrated above." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 0.13113069534301758\n", - "Polynomial Order: 5\n", - "Number of Constrained Derivatives: 3\n", - "Signs : [-1 1 -1]\n", - "Objective Function Value: 211039.4007159821\n", - "Parameters: [[ 4.52551586e+02 -1.27670850e+01 3.24057966e-01 -4.36486241e-03\n", - " 2.20470092e-05]]\n", - "Method: qp\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "x = np.load('Data/x.npy')\n", - "y = np.load('Data/y.npy')\n", - "\n", - "from maxsmooth.DCF import smooth\n", - "\n", - "N = 5\n", - "result = smooth(x, y, N, base_dir='examples/', fit_type='qp')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We have changed the order of the fit to 5 to illustrate that for\n", - "order $N \\leq 5$ and fits with derivatives $m \\geq 2$ constrained\n", - "the function will plot each region of the graph corresponding to\n", - "different sign functions in a different colourmap. If the constraints are different or the order is greater than 5 then the viable regions will have a single colourmap. Invalid regions are plotted as black shaded colourmaps and the contour lines are contours of $\\chi^2$\n", - "\n", - "Specifically, invalid regions violate the condition\n", - "\n", - " $\\pm_m \\frac{\\delta^m y}{\\delta x^m} \\leq 0$\n", - "\n", - "where $m$ represents the derivative order, $y$ is the dependent\n", - "variable and $x$ is the independent variable. Violation of the\n", - "condition means that one or more of the constrained derivatives crosses 0 in the band of interest. For an MSF, as mentioned, $m \\geq 2$ and the sign $\\pm_m$ applies to specific derivative orders. For this specific example there are 3 constrained derivatives, $m = 2, 3, 4$ and consequently 3 signs to optimise for alongside the parameters $a_k$. The coloured valid regions therefore correspond to a specific combination of $\\pm_m$ for the problem. $\\pm_m$ is also referred to as $\\mathbf{s}$ in the theory section and the ``maxsmooth`` paper.\n", - "\n", - "We can import the function like so," - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from maxsmooth.parameter_plotter import param_plotter" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and access it using," - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[########################################################################] 100%\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "param_plotter(result.optimum_params, result.optimum_signs,\n", - " x, y, N, base_dir='examples/')" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from IPython.display import IFrame\n", - "IFrame('./examples/Parameter_plot.pdf', width=800, height=600)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The function takes in the optimum parameters and signs found after the fit as well as the data and order of the fit. There are a number of keyword arguments detailed in the following section and the resultant fit is shown below. The function by default samples the parameter ranges 50% either side of the optimum and calculates 50 spamples for each parameter. The resultant graph is saved into the base_dir and in each panel the two labelled parameters are varied while the others are maintained at their optimum values." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We are also able to plot the data, fit and residuals alongside the parameter plot and this can be done by setting data_plot=True. We can also highlight the central region in each panel of the parameter space by setting center_plot=True." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[########################################################################] 100%\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "param_plotter(result.optimum_params, result.optimum_signs,\n", - " x, y, N, base_dir='examples/', data_plot=True, center_plot=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from IPython.display import IFrame\n", - "IFrame('./examples/Parameter_plot.pdf', width=800, height=600)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/example_notebooks/simple_program.ipynb b/example_notebooks/simple_program.ipynb deleted file mode 100644 index 88f7ddd..0000000 --- a/example_notebooks/simple_program.ipynb +++ /dev/null @@ -1,394 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Simple ``maxsmooth`` Example Code\n", - "\n", - "This section is designed to introduce the user to the software and the form in which it is run. In order to run the ``maxsmooth`` software using the built in DCFs the user can follow the simple structure detailed here.\n", - "\n", - "An important point to make is that by default ``maxsmooth`` fits a Maximally Smooth Function or MSF to the data. An MSF, as stated in the introduction to the documentation, is a function which has derivatives of order $m \\geq 2$ constrained so that they do not cross 0. This means that they do not have inflection points or non smooth structure produced by higher order derivatives. More generally a DCF follows the constraint,\n", - "\n", - "$ \\frac{\\delta^m y}{\\delta x^m} \\leq 0 ~~\\mathrm{or}~~ \\frac{\\delta^m y}{\\delta x^m} \\geq 0 $\n", - "\n", - "for every constrained order $m$. The set of $m$ can be any set of derivative orders as long as those derivatives exist for the function.\n", - "\n", - "This means we can use ``maxsmooth`` to produce different DCF models. MSFs are one of two special cases of DCF and we can also have a Completely Smooth Function (CSF) with orders $m \\geq 1$ constrained. Alternatively we can have Partially Smooth Functions (PSF) which are much more general and can have arbitrary sets of derivatives constrained. We illustrate how this is implemented towards the end of this example but we begin with the default case fitting a MSF.\n", - "\n", - "The user should begin by importing the *smooth* class from *maxsmooth.DCF*." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from maxsmooth.DCF import smooth" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The user should then import the data they wish to fit and define the order of the function they wish to fit with. We can also plot the data to illustrate what is happening. Here the 'y' data is a scaled $x^{-2.5}$ power law and I have added gaussian noise in with a standard deviation of 0.02." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "import numpy as np\n", - "\n", - "x = np.load('Data/x.npy')\n", - "y = np.load('Data/y.npy') + np.random.normal(0, 0.02, len(x))\n", - "\n", - "N = 15\n", - "\n", - "import matplotlib.pyplot as plt\n", - "\n", - "plt.plot(x, y)\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "*smooth* can be called as is shown below. It takes the x and y data as standard inputs as well as the order of the fit. There are a set of keyword arguments also available that change the type of function being fitted and these are detailed in the documentation." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 13.211431741714478\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 13\n", - "Signs : [-1 1 -1 1 -1 1 -1 1 -1 1 1 -1 -1]\n", - "Objective Function Value: 0.049767752022560015\n", - "Parameters: [[ 4.93743954e+02 -1.22808736e+01 2.13803269e-01 -3.19414523e-03\n", - " 4.38004681e-05 -5.60542296e-07 6.84052878e-09 -8.93587837e-11\n", - " 1.15739748e-12 -9.46473558e-15 4.99311590e-17 -1.61222191e-18\n", - " 3.82618421e-20 -3.68041597e-22 1.28748236e-24]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - } - ], - "source": [ - "res = smooth(x, y, N)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can access the results of the fit and plot the residuals as shown below. Here a number of the attributes are commented out but included for the sake of completness." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Accessing Fit Attributes:\n", - "Objective Funtion Evaluations:\n", - " 0.049767752022560015\n", - "RMS:\n", - " 0.0223086871022389\n", - "Sign Combinations:\n", - " [-1 1 -1 1 -1 1 -1 1 -1 1 1 -1 -1]\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAagAAAEYCAYAAAAJeGK1AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOy9aZgk11km+p6IyK0ys6q6q6q7el9lyVpsS7TlReZiX9sgM4CAscGGYQSYaxjgPtyL54LnAh7wmMXcGXuYGbjgawN+MGAbM4BmLCwL2ZbB2tVCsraWeu9Wd3XXXpVLZGzn/og4ESciTyy5VlbXeZ9Hj6qyojMjMyPOd973e7/vI5RSSEhISEhIjBqUjT4BCQkJCQkJEWSAkpCQkJAYScgAJSEhISExkpABSkJCQkJiJCEDlISEhITESELb6BMYBUxPT9ODBw9u9GlISEhIbEk8+eSTC5TSmejjMkABOHjwIJ544omNPg0JCQmJLQlCyDnR41Lik5CQkJAYScgAJSEhISExkpABSkJCQkJiJCEDlISEhITESEIGKAkJCQmJkYQMUBISEhISI4mRDFCEkDsJIScIIScJIR8S/L1ACPm89/dHCSEHub+9hhDyMCHkOULItwghxWGeu4SEhIREfzByAYoQogL4fQDvAnAjgPcRQm6MHPZ+AMuU0qMAPgHgY96/1QB8FsDPUEpvAvBWAOaQTl1CQkJCoo8YuQAF4HYAJymlpymlBoDPAbgrcsxdAD7j/fxFAG8nhBAA3wngGUrp0wBAKV2klNpDOm+JEYbtUPzyF5/Bi3NrG30qEhISGTGKAWoPgAvc7xe9x4THUEotAKsApgC8CgAlhNxHCDlOCPmluBchhHyAEPIEIeSJ+fn5vr4BidHDSsPA55+4gH96eWGjT0VCQiIjRjFA9QINwFsA/Kj3/x8ghLxddCCl9JOU0mOU0mMzM20toCSuMViOOznasJ0NPhMJCYmsGMUA9QqAfdzve73HhMd4eacJAItw2dY3KKULlNIGgHsB3DbwM5YYeZheYDIsGaAkJDYLRjFAPQ7gOkLIIUJIHsB7AdwTOeYeAHd7P78bwFcppRTAfQBuIYSMeYHrOwA8P6Tzlhhh2IxByQAlIbFpMHLdzCmlFiHk5+EGGxXAH1NKnyOEfATAE5TSewB8GsCfEUJOAliCG8RAKV0mhHwcbpCjAO6llH5pQ96IxEjBtGWAkpDYbBi5AAUAlNJ74cpz/GMf5n7WAbwn5t9+Fq7VXELChy1zUBISmw6jKPFJSPQdLAdlygAlIbFpIAOUxJYAY1AtKfFJSGwayAAlsSVgOdLFJyGx2SADlMSWgDRJSEhsPsgAJbElIE0SEhKbDzJASWwJyEJdCYnNBxmgJLYEGIOSLj4Jic0DGaAktgRkDkpCYvNBBiiJLQHm4pM2cwmJzQMZoCS2BKRJQkJi80EGKIktASnxSUhsPsgAJbElYDuy1ZGERBSUUjQMa6NPIxYyQElselBK4U5biYdkUGFQSnH3Hz+GB1+S06S3Mh544SqOffQfsNo0N/pUhJABSmLT49989jh+9W+fTTzGknVQIeimgwdfmscTZ5c2+lQkNhDnlxpoGDbm1/WNPhUhRnLchoREJzizUEc9RaaQI9/DYJ9Dw7A3+EwkNhJN0/3+V5ujKfNJBiWx6aFbdmpuyfILdSkcJ1kO3ApgTJItUBJbE7r3/a/pUuKTkBgIdNOGZScHHZsLSqYjWRRjUE3JoLY02Pe/rksGJSExELQsB2YKK+IZlsxDBZ/BKDu4JAaPBmNQ0iQhITEYuAwqReLjGJYMUEHAljmorQ1dMigJicGBUgrddDLnoABplAC4HJQMUFsaTZmDkpAYHFhvvbQclCUlvhBalmRQEkGAWpcBSkKi/2iZXoeIFOMDz6BkNwnp4pNwwRj0mrSZS0j0H7rl3mCpDIoLYLKjOZ+DGs2FSWI40CWDkpAYHNgNlpqDkiaJEAwp8UmAz0GN5kZFBiiJTQ2dSXypDEoGKB6yDkoC4AKUtJlLSPQfjEGl28w5k4TMQflB2nKoDNhbGLJQV0JigPAlvpRCXWmSCIMP0pJFbV34JgmZg5KQ6D+Y4UHmoDoD/xk0zNHcPUsMFpRSNE0bhLi5yDQVYiMgA5TEpgZjUJSG++1FYTkUxZx7uUsXXzhASQa1NWHYDhwKTJULAEZT5pMBSmJTQ+cW2iQWZTkOxvLudBnJoMISn3TybU3ohnsN7ByXAaojEELuJIScIIScJIR8SPD3AiHk897fHyWEHIz8fT8hpEYI+bfDOmeJjYHOFZpaSQzKpijlVADSJAEAJs+gZLHulgT73neOFwGMZh5q5AIUIUQF8PsA3gXgRgDvI4TcGDns/QCWKaVHAXwCwMcif/84gL8f9LlKbDxa3OJqJjAjl0GpqcdtFUgGJREEKJdBjaLVfOQCFIDbAZyklJ6mlBoAPgfgrsgxdwH4jPfzFwG8nRBCAIAQ8v0AzgB4bkjnK7GBYHVQQHK7I8umGCt4Ep9kUJEc1OhJOxKDB+siMlNlDGr0roNRDFB7AFzgfr/oPSY8hlJqAVgFMEUIqQD4ZQC/kfYihJAPEEKeIIQ8MT8/35cTlxg+QhJfQrGu5VCUPQYlc1Bho4hkUFsTepRBSYlv4Ph1AJ+glNbSDqSUfpJSeoxSemxmZmbwZyYxELBefEBagHJQzMkAxWBKiW/Lo+mZJHZ4DGoUTRLaRp+AAK8A2Mf9vtd7THTMRUKIBmACwCKANwB4NyHkdwFMAnAIITql9L8N/rQlNgK8xJck3Vk2haYQ5FUFLSnxwbAcVIsa1nVL2sy3KFgOaqY6ujmoUQxQjwO4jhByCG4gei+AH4kccw+AuwE8DODdAL5KKaUAvp0dQAj5dQA1GZyubYRdfEkmCYqcqiCvKTCt5K4TWwGG7WCilMO6bkkGtUXBAlSloKJa0CSDygJKqUUI+XkA9wFQAfwxpfQ5QshHADxBKb0HwKcB/Bkh5CSAJbhBTGILgs+lJEp8tgNVIchrCgxbLsiG5aCUU5HXFNlJYouCjXsv5lRUi9pI5qBGLkABAKX0XgD3Rh77MPezDuA9Kc/x6wM5OYmRAs+gkgt1KTTVlfhkDsr9rPKagrG8KiW+LQrGoMbyGsZLuZGcCXWtmSQkthhCNvNEBuXloDQZoACXeeZUBWM5VUp8WxTsey8xBjWCU3VlgJLY1GiFXHzJhbqaqiCnElkHBVfiy2sKSnlVdpLYomDfe0FTMF7MjaTEJwOURE+glOKvn7wYChTDhG7afhPYpJEblkORUwjymgpDmiRg2A4KLEBJBrUlwe4dRSG+o3PUIAOURE947tIaPvhXT+N/PH15Q15fNx1UCjkAKQzKplAVxTNJSAZlWA7yqoKxnOZ3FJDYWmgatt+fcrwkGZTENQi26zp+fnlDXl83bVSLrtcnrZt5TiUoqAqMDWJ7owTTdnNQkkFtXTTNIEAxBuVW64wOZICS6AlNz6J8/NwGBSjLRqXAAlSySUKVJgkfLAc1lpcmiU6xUXJ2v9E0bZS89l/jxRxsh47ctSADlERPYO1SXrqyjlpr+FKRbjo+g4or1KWUejZzKfEx8CaJUVuURhkvX1nHTR++Dyfm1jf6VHqGbnABquTK5KOWh5IBSqInsPyFQ4FnLqwM/fV1k2NQMeYHNmk3pxDkVCI7ScA1Sfh1UNLFlxnPX16D5VC8OLe20afSM6ISHzB6DWNlgJLoCXyh7FMbEKBalhMEqBgGxQYZqqrn4pMMKjBJ5KVJohPMreoAgPn11gafSe9oGLbfQHm8yBiUDFAS1xDY7nvXRHHoeSjHoTAsBxUm8cXkoCyfQSmyk4QHxqBKORW66cBJsOhvFbz3kw/jb566mHjMZS9AXb0GApQuYlAjVqwrA5RET2D5izcdmcJTF1aG6gJiffjSXHzMfs5MEi0ZoDgG5S5QW13m000bj5xewtMXVhOPYwzq6po+jNMaKEImCS8HJSU+iWsKTdNGQVNw7MB2LNUNnFtsDO21db8bs3tzxbn42OM5laCgSZu5ZTtwKPwcFCBnQi3VDQBItdxfXm0CuDYYFF8HFeSgJIOSuIbQ9JxAtx2YBDDceig2rDCQ+MTMiJkkWKujJDv6VkAQsBU/B7HVa6EWa26AaqQwyWtJ4ovazIHRmwklA5RET2gaNsZyKq7bUUWloA03QHmNYisF9yaLa3VkRiS+JJMEpdTfJV+rYDk4l0G5wX2rj9xYrLsBJylQm7aD+Zp73JVrQOLjc1BFb/SKtJlLXFNomDaKeRWqQvDafRN46vzwnHxM4itqKnIqSWVQOZUgr6qwHeo/FsWDL83jLR/72jWxAMWh5c3D4iU+yaA8iS8hUF9Z00EpsG97Ceu6FXKwbjaYtgPTpn6AAoDxEZwJJQOURE/QDdtf5G7bvw0vzq0PzbbsB6icCk1R4k0SDmNQbqEugFgn3+VVHbZD/ZzEtQj23gteqyNABij2fSfl4phB4jV7XTn76trmlfmYKYZ9/4Ar80kGJXFNocElWm/dPwnboXjmYrITql9gEl8hp0BLyC1ZXKFuWoBiC9S17PTzc1AakSYJDwsZJD6Wf3odC1Drm5dl89N0GdyZUJJBSVxDcBOtbh7j1n3bAGBoMh/riVbMqcirSmyrI1YfpSoEeZUAQGwequmxv2u5VsrPQalqEKA2sVzVDyz5El86g3rtPhaghsOgWpaNd/+/D+Hxs0t9e06fQfES3wh2NJcBSqInuFZV9zLaVs6jWtSGlr/xGZTmMqi4Ql0m/eVUTuKLCVB1n0H1Z8H+ub84js89dr4vz9Uv8CYJtrlobvFuElkkvkurTZTzKo7MlAEMrxbq6loLT5xbxtN97NQSjHsPMygp8Un0DN20E0dLDBNN0/adYIB7wQ8rn8EzKE2Jd+cFNvN0iY+dez8YlG7auPdbl/HI6cWen6ufMHiTRG7wEh+ldOTGOESxkKEOam5Vx+xEEdvG8tAUgitDYlCrnuzWT9mZvc9iJAclJT6JnvFjn34Uv3XvCxt9GgDC/bwAuL3dhiQX8SaJXCKD4iU+91zjc1D9k/hOzddA6egVP7KJwjmV+EnyQQWoesvCa3/jK/jaiasDef5+YYnloEw7NpheXtWxe7IERSGYqRaGZpJgsltfA5RA4pMMSqIveHFuHa8sj0atjm7aIZmgmBseg2ISX1FTkEvIQQU283QXXz9NEiev1gCMXvEjY5oFTUFBU6CQwbn45tdbWNMtvHSlNpDn7xeYzdx2aCwTn1vVMTteBADsqBaGZpJg/fH6mRfVRTmoYg7NEVJnABmgNh1008a6bo1ER25KKRqGFbrI3fENG2AzV5X4Vke+zdwdtwEEMlcU/ZT4Ts3XAYxefzPeJEEIQSk3uJlQbEbYqHXJ5tE0bDQMG9OVgv97FJbt4Oq6jl0TboCaqRaH1tF8zZf4+vcdse87ZDMfwZlQMkBtMrCbYhRcZobX042/yIc5odVnUJ7EF98slutm7jMocTBr9NEkccpjUKujxqA4kwQAlPLawDYVdS9AjVqXbB6si8S+7SUAYrnz6noLDgV2TbrH7BwvDM3FNxCJzxBLfMBoMX4ZoEYYIi386ggFKNFFPlSJz7KRUwlUhUBT4nNQtsegNK9ZLBDv4mP5s/5KfKO1OAeuRpdNDnJTUfdyeqPGInkwB9/ebWMAxFZzVgM1O8EkviKW6sZQ7kOfQZkDkPgiJglAMiiJDHjq/DJe/eEv+7UXDPOe7j0KhaQiq+owJ7Tqpo2i5r52To3vJMGkPy2DScKvg+pRQrVsB2cW6lAVgqZpj8SGgiHKoAYZoNhiN0q78ihY/mnvNpcdiTZYrD8jk/h2jLty4EJt8CyKMfB+yvpxJglgtDYTMkCNKM4s1KGbDk5cWQ89PkoSn0jHHrbEV8gFAcqK6a/HdzPPbJLocbd6YbkJw3bw6l1VAMk5GN208aG/fmZoOY2WHZX4Bsd66y33eQe9K7cdiv/6wMtdyamL9XCAEl2/bKO4a9w9ZkfVDVDDqPljLtBWHzd+TSOQxxn8mVAjtJmQAWpEwW6SuUhnbV/iGwGTRJzEpw+xDopJdlpCDoo9rvGtjmJMEuxz7/XzZfLet+13u2skLZzPXVrD5x6/gMfO9K9TQBKCXnzu9zZI1uvnoAa8Kz8xt47/dP9L+Ifnr3T8bxc9FrQvReIby6sYL7ksY0fVZVLDyEOtDaIOyrSR1xSoCvEfqxTc97bekhKfRArY4n9pJSrxjQ6DEjWcHMuraCTUkvQTLdNB0eti4TaLTe7Fp6mBi8+MNUmw3Wpvn++peTdA3XbADVBJtVCrTXcH30+XVhLMKIPKaYOT+Ppgkvj4V07gK8/NJR7DrsX5LiS3pbqBvKZwLr72c2VFuoS41w+T+IYSoPT+u/j4URsMLEDVZYCSSANbMKKzidgNMazFLAksiIZzUFpiLUk/oZtBkXBeix+3wQKUyjGoluBYx6G+MzCOYWXFyas17KgWsMdzfSXJJsv1/u+Qk8A2N7xJYlCtjvrBoP7isfP4ckqAYkn/bmTSxbqB6XI+sXHupdWmn38CgKlyHoQA80OQ+AbVSSIaoMpegKpJk0QyCCF3EkJOEEJOEkI+JPh7gRDyee/vjxJCDnqPv5MQ8iQh5Fve///XYZ97v8AGyF1uM0mwALXxDIrdyMWIxAcMZ3yDbgUBSlPic1AscOUUxZe1RAyUl3Z6ZVAnr9ZwdEcl0PUTFugV36U1nE2HYTlQiJuTA9rzhmu6iRNz63H/vCOwANUw7NgNRBpalpN6PbG/dxWgai1sr+QTu2rMrerYNVHyf9dUBVPl4VjNB1Go2+Cm6TLkNTdHWxuhvowjF6AIISqA3wfwLgA3AngfIeTGyGHvB7BMKT0K4BMAPuY9vgDgeymltwC4G8CfDees+w/dZ1DhAMWq10dB4tN9F1+4Fx+Q3BW6f6/PSXwqif1Msvbi4xemXhggpRSnrtZwZKbiW3eTclArDSbxDYlB2Y7/OQDtJon/9tWTeM8fPtSX16pxclG3RgnTdlKvJ/b3blx1S3UDU+WCv2BHBxG6RbqtEIMChlcLNag6qCiDAoBqQZMSXwpuB3CSUnqaUmoA+ByAuyLH3AXgM97PXwTwdkIIoZQ+RSm95D3+HIASIaQwlLPuM3yJb6Xp53Mch2LBs8QatrPhDTgbApPEMOcLhWzmSnyro8Bmnuzi4xfpXjYAV9dbWG9ZOLqjggnfGRV/0680hi/x5dXg1o/mDb91cRVrutUXGZkPUN3KfEYGBtWLxLdQMzBVzvvXcfTana+1YDvUr4Fi2FEtDNzFZ9pOX4vHGXQBgwJcmU9KfMnYA+AC9/tF7zHhMZRSC8AqgKnIMf8SwHFKqfCKJYR8gBDyBCHkifn5+b6ceD/BCkbrhu0n2JcaBmyHYqZaAKWINQUMCyKTRGmYEh+Xg8pp8YW6TFrSvKJehUDo+Gtw3RR6CRbMwXd0RwXFnIKcShIX5+XGcE0SbQwqp/p5Q0qpX9rQj4WK3413Y5SwvG4laePV9R5NElOVPHKq+11FAxRTMXZzEh/gOvkGzaD43GU/C3WbApME4AWo1sbntxlGMUD1DELITXBlv5+OO4ZS+klK6TFK6bGZmZnhnVxG8As8M0qw3SFLvG+01Zwl1vkLvTRkia8QcvElmyQ0z1Kb18SjORp9YlB8gCKEpI4xWB1Ap4AkRBlUMBPKxnyt5XdWqPVB6qm1bIz3UADKvqesEt9Kw+wo0DcMC03TxvayK7SUcu2GkblIFwmGHeMFLHrsalBgm9PxotbfQt3IFAIGKfGl4xUA+7jf93qPCY8hhGgAJgAser/vBfA3AP41pfTUwM92QGgatr+IXPas5my3xgoKNzoP1TTtUG0RMFyJr8WZJHKJI98dqArxLcJ5VRHnoLydo6aQntjMqfkaqgXNL+YcL+USc1DLw85BWU7sd8abI/pRXFtvWdjtbai6aRjLvqfUAGUEnx3rDJEF7NipSh6Am0+NvhZjUNEc1I5qAQ4N6qgGAXbdzFQLfW91JJb41L5sTPqFUQxQjwO4jhByiBCSB/BeAPdEjrkHrgkCAN4N4KuUUkoImQTwJQAfopR+c2hnPAA0TBsHp93CQXaDzPsByn18owNUQ5BoLeWGN6FVNx2uUDdh5LtDffYEAHlNFQYDVgM1OZbrmUEd9tgTwEZpZ8lBDc/FN6wAVWtZ/sLejcTnBygj+fvgg0oneSjWRWKqzAJUeyeUpXoLmkL8fCLDzBCKdRnz3lEtomX1r77Qlfjal/9KMScZVBK8nNLPA7gPwAsAvkApfY4Q8hFCyPd5h30awBQh5CSAXwTArOg/D+AogA8TQv7Z+2/HkN9CR3j41CKev7TW9njTsHBgqgyFBBIfc/Dt8RjURtdCiXZhw5T4wgzKLdQV3cCWHQ5QBU3MoNg5T5RyPeegjs5U/N/Hi1qixDdsk4RpO8ip4RwU4LL2F7kA1R+Jz/I7gPci8WXNQQHJAWq1YeLBl4KcMxtUuN0LUKJmx6tNE+OlnL/hYAiKdQdnlGCf2bTH1uJKKTpFwwhPwmaoFFTZSSINlNJ7KaWvopQeoZT+pvfYhyml93g/65TS91BKj1JKb6eUnvYe/yiltEwpfR3338iO8jx5dR13/8lj+Pj9J9r+1jBsVAsado4X/W4S8+stVAoaJr2d3EgwqEiAGpbEZzsUpk05Fx/xH4/Csh2/5gdA7GgOds7bxvJdf7Zruomr6y0c3cEFqFIudnE2bccPBEPLQdlRBuUuVA3Dwom5dX/sRK3VW3siw3JgWA52VosgpLseb7zEl8QedNMG24MkGSX++vhF3P3Hj/l5QibxsS4SorZPa03Lz6Px2OkNLxzkZF3GOme88+vXPd80xTmocl7moCTgLpof/KtnYFiOUP5pGjaKeRWzE0WOQbUwUy0E3RA2Ogclkvjyw3HxBcMKA4kPEDsb2yW+mByUd86TY90zKJZQZ3lCAIkmCcaegOEx4labScL9zmotCy9dWcexA9sB9C7xsYWuWtRQLWiJMmccGINiG5I4NE3bn3a7kMCgmIx7n9eZgkl8jEGVBBLfmm62yXtAEDQGKfGxHBRja/24522HwrAcoYuvUnTbXg3S+NEJZIDaIHzyH0/j6QsrmCiJNd+maWMsp2L3RMlf9Oa9AFUYlQAlkviGZDPnp+kCQdseU5CHsmwKTY0EKAGDavo5qHzXny1b1Me5BW2ilMNa0xIyANaHD9h4k8SLc+toWQ6OHXT7B/YaoBgzrBQ1VIvxLDLtXBmSrqmmYWO8lMPkWC6RQRlekGMBaqluoJhT/M9gTNDZfc2T+KLIawrKeTW0yeg31nQTOTXIf/VjExPMghLkoFg/vhHpJiED1AbgxNw6/vP9L+O7b5nFd7xqpi1AUUrdAJVXsWuiiEurbrHufIRBbbTE1zTsUB8+AEEtyYBzULrFxgUo/usCENZCuQwquNRjXXyG60qsFDQYXS4E/qJcCCSh8ZJrERYFoGVvcVPIcHNQ0UJdAPjnCysAgNfsmUReVXpnUEbwWYx7QbpThAJUwjXFJKuZSiExB8We75mLq3hlpYmFWgtT5YKfXyrl1FA9HOBavVlHkCgmUhyavWKtaWK8mAs2pREZ+MW5Nfz1kxc7Mk+IZkExlEesYawMUEMGpRT/1xefRrWo4T/cdbOwME43HVDq1qfsmixBNx2sNEzMr7ewg2NQG10HJXLxAayWZLgMijEkUb83y3HaGVRMgCrlVRQ0petgwYpbq1zOgi1uIplv2ZOYZqqFDXPxMRb81PkVEAJct7OCSlHrOQfFFrlyQXONIr0yqIQAxbpzT6cEKNN2wLwO9z075xfpMpTyWptj0DVJtOeggPQSgl6x2nTlxYKXa41el3/6zbP44F89jV/922czy3Ls3hTloCoj1jBWBqghY6Vh4pmLq/ipbz+MqUoBlYLatlthOvlYXsVuz6J7eqGGWstyGVTKVNhhwXXxtd+4gxyAx782AP/GzSnxQTvq4straozE5zJCJgF2Y+lltT5hBhXfj481it05XtwwkwTbZCzUWjg4VUYxp6Ja7L3lDWNgjEF1w8j4rvNJ15RuOijlVcxUC8kSn+VgspTD9Tur+PJzc1isGX7+CRB3do+T+ABPvh3grKs13UK1FDCo6D1fN2yoCsGfP3oe/+azT6a6HQHxuHcGP0BJBrU1wWQPVndRLriFgfzuh+9xx6rXn76wCsBNzLLuCRttM3cZVPslNJbXBi/xmWGJL2BQIonPiUh84saydcPCWF5DQVO6biXF510YJhI6mrNGsTvHi9CHyaDUdhcfAFy/050AXClofTBJ2P5zVVOs9nEwO5D4SjkvQKUwqLym4LtunsXjZ5dwdrHeFqD4voS6aaNlObESnytdDlri02Lv+aZh41U7q/iN77sJ979wBT/26UdTmRRbX6LyPBBct/URaXckA9SQwb54pvWKkpL8DodV4T9z0c0P7Bgv+ovLRjMoN08mYFCCdjH9RivKoFgOqieThLvI5XuQUNmiXs7zEp/X6keQg1lpmNAUgqlyfmgMyrQd5DgGxc/Jun6WC1CCXfTVNT3z2IxA4lNdJ2MPdVBAci1U07BRyCmYqRbQMOzYHIrh1YDdedMsKHW/L2YxB1zZi9JAShOZXngMPAelu+wtTuLTvYLbu998EL985w14/OwyzizUE5+zacZLfOy67VXe7RdkgBoy2A57rOBeHKKkJL/Dma4UoCkEz1wMGNQomSREF3lpgCPEGRjbCEwSnosvi808wSQxllf9xaCbz7fWslDOq6FR2kkzoZYbJibHcijmxN0tBoGozRwIdtM3eAGqWmyX5OotC2/9j1/H3zwV7TwmBm8YGS/lUGtZcDq0L2d18bEcFLN+x43dYPm3V++qYv92tyNLlEHxr8WCj6gOChiOSWKilONKSyIMinPSHtiercNMkkkikPgkg9qSaBhhl1dSgCp5C93O8SJOe7uiHeOjYZKwbAeG7QhlAlG7mH6j5Ut8wcBCQNyl3DVJcBJfnEnCu9njFoMsqOlWSN4DkDgTarVpYHIs7xkzhifxFbRIgPI+x+v9ANVukitiLYUAACAASURBVFiotdAw7Mx1P7WISYJSdDwMrxMXH5P4gPhuEszBSAjBnTfPAgjkdoArNPdei20qkhhUw7BjGxX3AkqpVyQcn4PiaxGzMn/dSMhBFaWLb0vDlz3yTOJjRZLBzdc0mUnCPWb3pJuHUhWC7WP5YAEdkiQkQtIubCguPissU2hJDKrNJCHufN40LI9Bdc9Qay0rZJAA4DvAxC4+E5NeErxlDX7GF6W0zSQBuItVMafgwFQZAIQmieUOWzLVWxYKmoKcqiQ6GZPAL7ZxAYpS6rfdSgtQhhW0efre1+yGQoBD02X/78FEaCt0vqJCXf7xQeShWpa7CRwvabG1j7xRKauykmwzD4q2RwEyQA0ZQQ6KFQYmMCjvApr15tBMV/JQuHzBRjIo0SwohqFIfBGTRN6vgxIxqHAOKpcg8ZXzWk+dOtZbFiqRhHpBcxd/USeFlabpMigv9zHoGV+2Q0EpQr34AJflXLej6kuTzCTBB0xmic/K9PhgHQTpHhhUzKbH8GZGFXkGFSPxmTb1v99b9k7g8V95B44d3O7/fcwfPeK+bjDuIjlADULmW+WCY8FbC6KbUr7pa9bcdFKAKmgq8qoyMgFKLKxKDAzMDNFmkuAuiGbEZcOs5uzmYxfiRnaSaBrxF/kwJD6/DkpjDIqZJGJ68XGsJq8pIfsyQ9Org+rFhFLTTVQL7bdVXLujlYaBm3aPcztku43d9BNsUxN9jf/zna8K5aUqRQ2WQ9GyHJ9V+GNBMjL3Wsvyr3OfQXVolMjCoHQjkHu3jeWhkDQGFWxWpirhgdtBL8kwg4qvg3IfH0SA8l+7mOPueUEOqk3iSxtNEr+5BLyRG7IOavNDN2380B89jL964kL6wR6inQbKAhdflJ2wcQU7vPb+hJDYPMqwwM5RlIMq5bQh1EFFclDeoiOsg4qYJAoeg4rKab5Jgu1W+yTxAfEFnSsNE9vGckNrX8WumahJ4m3X78AdR6f936uCgLJU72xuVT3EoNzn69S6noVB8YxAVQimEop1XXlTvDADwT3HclCrXJAQYZAMis9/BTbz9hxUMR8JUFYyC48WuUdRHqGhhTJA9YCCpuBbF1dDIwrS0Gi5XZfZglQW5KCidQpsXMEMt9srqMNLqgPAQycX8D+fudR2jsUYk0Ra9+leERTqei4+JaHVkcBmDoTlNMehniNKi92tZoHIJAGICzp100bTtD2TRPdBsRP4ASqFpVUFHQU6nVvFB+tqMT4Pl3a+mkJQ0JRYm3mwoXPfU1K7I7cGjAj/BrT3klzTTeQ1JXYxH2iA8uTQiZJ4A2N7DJedc1bzVMOwkVNJm8zLUCloIyPxyQDVAwgh2DVZ9Ju5ZgGTPVjvL5HE5y/+GpP43ADFOhoD8U60QeEPvn4KH/vyi/7veoLEV8qrbsfkAebIdE8KUzxmlNPSWh2FXXxA2PHHM0K2W+3m812PY1BFrS3/wha1yTFuhzzg3J0v8cUsTgwsoPAL1VKHk3/rLdvfgHUt8Xm28KS8ph7JqSR1kzAFBhEeUZv5WtOKNUgAXAnBAHNQ40VNKOtH33fWDjNxozYYZIC6hsCauWZFvWWFijhLORUKCQco3bRRzAWL777tJRQ0BYdnArfRIALUSsPAOz/+IJ59ZbXtb5dWm7iy2vLrWJKq0YfR0bxlhq3Svs1clIOKSHw5QY6Jfz/d5vgopai1rFAfPgbRTCiW05ks5WMlvjXd9KW1fiArg2JBdj3EoDrPQTHDSDWhWDnxfL2AkuQMjRaezlQLsSM3WKFuHKIS35puxtZAAXyXkP4v6LzERwhpK0WIpgKyuvh0U9xDk2FTSnyEkHL6UVsPuyZKuLySnUE1jGBXCbgsrJwP71gaXssdhsmxPL7xS2/D9712j/9YIaYbQi94cW4dL1+t4bEzS6HHKaW4vKLDsB1/fk6SE2gYQwv1yC7QL9QV3JyuzbydQYUS8BwjLHbJoBqGDUoRw6Dac1BMMnNzUGKJ79f+9ln8xJ881tF5JCHOJBEFkyn5ALXUlYsvMLGU82p3DEpNYVBGe4Car7WEErMpKFLmEbj4ApNEXA0UEDg0B22SADxzj9l+zRajJomU7yeuwJ6hUhR3EdkIdMKgXiaE/CwhRDr/OOyeKOLqevb2L6IkenTHIuoSvnO8GOpOEL1Y+wEmVb6yEmaEq03TXxzYMUlOoGGMfWcsk0FLanXkOG2dJIAIg+Jqz5hU0imDYpuMqiChPuH1bOMXTcZIJniTROQzu7Km4+mLq/6xvSLOJBEFWxTXuYDS6Wj6qFrgdqfo3MWXU10GlZqDYgGqUoBpU2HQMCJtnqJgz9HwJT4z1iDBMFHKYXUAM6FWm2ao9VYh0uS4TeLLWqhriocVMlRGaKpuJwHqOwG8C8CLhJD3Duh8Nh1mJ0pwaPapmlF2BLhGCb45o2jOUhRx/eR6wWUWoJbDAYoPWEzOTCvUBQYs8VmOn6MDUlodxZgkWnESX5eFun73bqHEp8GhbvdphoBB5WNdWuy8Hj+73NG5xIHl3ZIWaUDc1boTF5/tUE8tCM/F6qYOqsAkvlSTRMCgALHVPNooNwrWl7DpS3zJOShAzI77gbWmFbK3F6IMqi0HlVHis8KbuygqRW3zNYullD5LKf1eAD8J4BcIIccJId85uFPbHNjldXm4nDEPVWuFb1qgPSnJ5hIlIa6fXC+Y895DlEHxEiZjUMGC3r4Ys8eGKvH5Lr4MNnORSYILUIWMUkkUPoOKkfiAcDKddWaYTJD42Gf46OnFjs4lDq2MDIoFWebio5QGDCoDM2ZlE9G5WD2ZJGKuJ1ZywJskgJgAlWKSAMJTddcSZkExDKofX3TUfCEXyUFFVAy2SUs1SRi2X0ohQtlbjzrtmzgIdGySoJR+g1L6JgAfBfCHhJAHCCGv7/+pbQ4wh93ljE6+OqfLM4xFKHUzJYkJeHS/zwHqcozExwffKIOK9nQDArvvYCU+JyLxJTGosIsv2STRfSeJWiKDarcjrzQN3wDAF+ryYIvQY2fDecFukdUkkVMVFHOKn4uoG7bP2LNcd/ywQgaRUST1fL2AUszFF39HTRKsO7nIyWfaNDU4j3mvRSn1TBIZJL4Bufj4186r4UGa0ffN6iNFReg8dK74WgS2Pg16ZE4WdGKS2EkIeRch5FcIIX8N4OMAdgMYB/BFQsifE0K2Jz/LtQc2rymrUaJhWBgT5KB4BpVV4ut3HRQLUEt1w6+kB4BLqzpyKsHebSUuB2W5DkSlvaaklAsnmgcBV6bgJT7m4mu/OU1HLPHxEil7v6V89734WHNVkUlC1LNtpe4W6boOLXErG3Zez76y2hfrL3tPoo1FFJVC0NGctTnKOppeFKCqxfCMqXu/dRn/z30vtv3b6PnmU3JQgUnCq4OKYVC2Q2E7NNHFB7i1fU3DrVEzbZpokgAGN7SQjdpgKOTCm1L2efBrRSGDstIybRQTvv9KwX3NUegm0QmDegXA7wI4CuABAO8BME4pfb332FkA/73fJzjqGC9qKOfVzFZzkUmiUgjvDkV5qijyPYwlj8PlVd0/t0sci7q80sTO8SJ2T5b8IMa3+Y9iOC4+x1/UAW4elIBB2YJxG0C8zVxTlcwLMY81boJsFEEdEGfbbhqYLLmdtJNyUDfvGYdDgSfP9Z6HYgwzbZF2z1nzTQ3MEp91ND0LRLzcGW339MlvnMb/940ziQYjX+LLkINiG5bxosuCowHKzOhgZIXmLF+WxqAGNfbd7WQeyUGJbOb8Ri1D+UlUHo9ilBrGdhKgJiilt1BKf4JS+geU0scppQYAUEpNSumvAHjdYE5zdEEIwexEtmJdy3agm+0jKqIuvmaWHFSfTRKG5WCh1sKt+ycBABc5o8SlFR27J0vYNVH05T6R05BhGC6+VsTFpyoEhLTnoCilXoAS2MxjAhTQ7pjKArbjFNdBtXdSYLOg3Ndrl/hYp4A7jk5DU0hf8lCsT1uWfn+VYsDsWb5sNuNo+uhgTsAzSXgNaNd0E89cXIFhO6FrLQqTufgSclBNM9wZgRCC8WJ7sSkL/rmEThIAMJbT0DAsnxWlmSQmvHH2aZNsO0VbDiqyKWUNbfm1IktuOiqPR1EdoZEbqVcpIeQXCSFHKaV1QshNhJAPE0I+SAi5SXD42wZwjiOP3ZMlXMoQoJim286gIiaJTDmo/pokrqy55/9tB7YBaHfu7Z4oYtdEyS/W1RMYlB+gBmySKER6quUUBUaEQbHmsdFxG0C0DiqQ+NgxnXZ1qAlkLQbRTKjVUIBqN0mwAD9VzuOWvRNt9WndIGsOCgiP3GAS3+xEMROzDD6L4DsaL+Z8d98jpxbB1vOTV2uxz9OyghyUHhMYRYygoLUPgGQMKk3eZBJfWqNY/32V2i353eCxM0v4xP0vgVIKx6FtNVgFTRW6+Pj3nmXjqlsphbr5dgfnRiELg/p3AF4hhFwH4CsAboAbiB4hhHyGEOJP+6KUPjWY0xxt7Joo4vJKusQn0uXZ7y3L8Xf/WXJQ0d1Ur5jzAtRr901CU4hvNXcciitrOnZ5DIoV6zYSzjFaS9IPrDQM/Po9z+HicgMAS/SGL19NJW0Mikl+vElClGNqGLZrMfaO64ah1loWijlFKJ/5nRR0nkEZ2DaWD50Tn2cJ8mIabj+0HU9fXEkce54FWeuggGDkBjtXwGVQhu2kOrzYtV4J5aCChrEPnVr0g+TJ+fgAxUwSY3mX0YrkQFFnBNH9YfgMKt0k0TTtoJNDBpME0Hs/vt//2kn83gMv488fPY+6YcGh4deOFudH66CAbB1m0iW+zRWgNEppE8CPAvhBSumPUEq/B8ABADMAfnWQJ7gZMDtRwnytlTpVk8keIomP/d1vADlkmznLLe2dLGF2ougzqIVaC6ZNsXui6BtC5lb1xGr0nKogp5K+SnzHzy/jTx86ix/4g4fw7Curwpsspypt4zaYaSKnprc6Gsupfo/EbjYA67rlJ5ij0FQFlUJQB0QpxUrTxITHoBQvOIYlHO96yal446EpmDbF8fO95aGy2swBN1nuS3x1A4QAO8bdayAteEe79gOczKmb+ObJBbzx8BRmqgWcSmBQhuWg4JkkAHdjEoXoWhQx4E5yUA3DDnrhZZD4gM7bOPHQTRuPnF6EphB89EvP46nzK6HnBtpzUA3DgqqQ0LWdti64wx2dRJv5ppL4AFzy5Ly3UkofZQ9SSpcA/DiAfzWgc9s02D1RBKWBTBYH0a7S/d1LShpW4hgLHlFHT69gDHB2oog9kyWfQbFAtXuy5FvqL6020TSTWV6/p+oyaaNp2PjhP3rYa3gZvnxzKmnbJNgeg4p24QDaWx2NFcI70W46SYjyTwzjRc3flTdNG4bl+AwKaC/E5Dc033ZwGwhBzzIfM0lklfgCk4SbD2GBIi0PJZI7GRt4+UoNL1+t4Y4jUzgyU05mUEziS5CNRWUZhVy7xJeVQZV8iS/oJp6EfjCoR04vomU5+K0fuAWlnIpf/MLTAMLyYiEXbXXkdoRgmyognfmzzyQpB7XZGNRvAzgOYIIQ8m8J/2kADoCJgZzZJgIbh5FWC5Uk8bG/87JOEvJqf00Sl1d1VAsaqsUc9mwr+YGJvaddE6U2BpWkY4/ltZBVnYfjUPyPpy91lFRmI94/dfcxHJgqg1KEOkkAbsPYaIBiDCrUzVzY6sgOOSe7qTOr6abQwcfAu71Y0etkUiGmGeTFxos53LhrvOcA1XEOquWaGpYaBraHOl4kbz7qLcsfk8HAmMiXn5sDANxxdBpHd1Rw8motdjQL3ywWgFDibJpO29iXKNtgzwWkv/eSVwfFclBJmw6gPwHq6yfmUdAUfN/rduO3f/AWLHg1XGGJT23LUQqZY8J1ywJc9N7hIeoislFIvUoppZ+FK+W9HsApAM8QQv6IEPIxAA8CuHewpzj6YAMFUwOUt/srR1sdcUnJpEm1PPKaAtuhmXsA8lhpGLiw1Ag9Nreq+wFo72QJV9Z0mLbj2813TxYxVc4jryq4vKqndrtwm3uKz+2rL17F//6XT+GbJxcynzNLkB+YGsMXfuZNeP9bDuGdN+0MHePmoMILnZ1gkjAjJomolt8Ng0oLUGzR8zuZhxhUeAFiOTy2gXnDoSkcP7+cKiUnwbDdXJsqqF+Lolp02zM1DBsrDSOx40UU9chYGfZ8APDVF65gcswNuEdnKljXrfjxGFbQiw8Q5zV1buw5Q1IOKrVQ17OZrzZNjOXVVMbVjwD1jZfm8cbDUyjmVNx58y780LG9AIDtleD6iNY+ukal9vedtLFiG72kHFRBU6ApZPPUQVFK1yilFqX0bwD8CwDn4DKnTwP46X6fFCHkTkLICULISULIhwR/LxBCPu/9/VFCyEHub//Oe/wEIeS7+n1uIvgBKsUoURc4m9zfAwaVVeLL2hhShF/64jP40U89Gnrs8loQoPZsc/sLzq3quLyqo5RTMVHKQVEIdk4UcHm1mdqy35X4xBf4k14eZS5FEuXB8gkFTUWloOHXvudG3DA7Hjomrypt4zZ8k4QgQEWDQajgUVM6bnW0HjOskGGilMOp+Tqev7QWMKixeBtxI7JZuXnPOHTTwfnI5qITpPWi48Hyaeu6haW6ie3lvNAOL4JoLhZjA3XDxpuPTEFRCI7sqACId/L5DCqhO4nYJKG2yZBZ5U2mXlxdb6UaJIDex76fX2zg9EIdb71+xn/sI3fdjD/8V7fh+p1V/zEWfBjbFKkYaTmoYJpu/GdACGkrfVltmrFDIAeJblodnaeU/hal9GcopR+nlHZ/twhACFEB/D7cxrQ3AngfIeTGyGHvB7BMKT0K4BMAPub92xsBvBfATQDuBPAH3vMNFNViDtWClsqgRIljIAhYrsQX3yWcR7fdDtZ0E18/MY/zS41Q7dbcatMPtHsmxwC4tVCXVprYPVn0d8K7xks+g0oKoizRLMJxr+B0IWbXLIKeQTsXufj85qh8qyOl/bOrRxhhNyaJWssS9uFj+Ik7DoJSiu/5r/+Ij9//EgCEclDRxD7fHxAAjsy4i3mSqSANpk1T64AY/H58LdNjUHnObZjOoKLXOS+VvfmIO17+6I749+Q41G9NxHb8whyUgM0Xcu0SeGYXn/dcc6t6qsUccDcQOZV0HaAefOkqAOA7XhUEKMakeAZa0BQ4NCidEOXe8lq7zM1DZE0XwS19CT7rX/3bZ/GTf/p4xnfUP4ziwMLbAZyklJ72CoE/B+CuyDF3AfiM9/MXAbzdy43dBeBzlNIWpfQMgJPe8w0cs1wRaxxYTiba6ijQfO2QcysJ3faLe+CFK/6N+5THZEzbwdX1FmY9E8Sebe7/X1lp4tKqW6TLsGuyiEsrzdR+gXHzeyzbwTMX3YGIC+vZR0iw3XC09omHKAfFJD5e0lI851O0Dmqsw4LHKNwBffEL2puPTOOrH3wrfuQN+3033jaeQeXEEh/LjbGBlafm6x2dFw+3rijbnq3KzYRaqhsug8pllfjsNqWgyPUcfMtRN0DNjhdRKWjC98TnjJJzUHZbTkWUg8rq4mOvNbempxokAJdx9NLu6Osn5rF/+xgOTSeP3IvKq3E5qCRVhW0skjZ6AAtQwfv55wvLODG33vdi5DSMYoDaA+AC9/tF7zHhMZRSC8AqgKmM/xYAQAj5ACHkCULIE/Pz8z2f9C6uDVAc2I4kGnyYxNcwsjOorK31o7j3W3PYUS0gryl46oJrZZ1fb4HSQKpk/39luYnLKwGzAtxAzPJSSUaOOBffi3PrfuCKyzuIoFtut4Ck3Inr4ovYzP3WPuF/Fw1ALiMMO6Y6+WwppajpyS4+wJ399NHvvwV/93N34Hd+8Bbftg2IbcRAcC1UiznsHC/gVILrLQ1sfEUWMDZ4db2FluV4OagOJD6BPFYt5rBnsoQDUy5LJ4S4Tj4Bg2ILbcHrZg7ES3xCk0SE5WXtJMFe68qankniA7pvd9SybDx0ahFvvX4GYf9ZO3yDivcZ6AInbWaJL2WTwo8AqrUsXFhqwrCdjmT5fmAUA9RQQCn9JKX0GKX02MzMTPo/SMGu8SIupTSMbbTcXXq0wSrvmvFZVsYcVCcMqtay8OBL8/juW3bh5t3jPoNizI8FomJOxUy1gHOLdczXWtg1wTGo8aLfASCamOYRJ/Gx15wdL8aO5RZBF+ySo3DroOIYVPhco1JIVCaK1iSloWU5sBwaWwcVxWv2TuK9t+8PPdbeyqY9H3l4uoLTvQSoDOMmGFhhLTPUbB+LH00fhahrPwC8bt8k7nrd7tBifGSmIg5QVjuDEm16RAP4eukkwT7vluWk1kAxTJTCfQaz4vEzy2iadkjei4O/KeUK+qMb2bRCXRagkuqgAKBSzPmd7F+6su4/fm6xe/beDUYxQL0CYB/3+17vMeEx3oTfCQCLGf/tQLBrsoiFWivx4qgblrANTkFToCoE9ZYVVIen2MwZ3e9kl//AC1dgWA7+xWt24db92/DMxVWYthOykjPsmSzhyfPLoNT9OXifwc9JDW1LeU242z1+fgUz1QJeu29CmIM6u1DH2YX2m6BlJRcXAm4Oqo1B+TbzCIPS2hlUOZSD6sxmnjSsMCuiif26Ee4xBwBHdpRxar4ea8tOg+uK6ywHxUwZbg4qWx1UdJouw6fuPoZfuvOG0GNHdlQwt6a3tQri84dxDIpSKq6DEtnMO6iDYhjP+H12O3Lj6yeuIq8qeNORqdRjAwaVIvElBqisEp/qmyROzPEBqq+Wg1SMYoB6HMB1hJBDXhul9wK4J3LMPQDu9n5+N4CvUveOvQfAez2X3yEA1wF4bBgnzYpYk4p1ay1baEMmhKCcdyl1I2MOKqvUwuPvPXnv2/Zvw237t6FlOXjh8ppvlpjlpLy920r+xciGMgIIyX1RWYVHnMR3/Pwybt03iZlqQSjx/dIXn8Gv/d2zbY+7vffSZxjF5aC0CGvNcVKI43iLHLegdjrOJGlYYVa0D6Sz2hbeIzMVrDZNLNa7GwHfCYNi1yoLUG4OKtt1V2uJN2MiMKPE6UgeireFx+WgTNttBtxmkvDYKB/IO81BAeldJBi6CVCUUnz1xFW84fD21OkFQHsOSuReTJsHxb63tDKWCufiOzG3jnJeRV5VcHarMygvp/TzAO4D8AKAL1BKnyOEfIQQ8n3eYZ8GMEUIOQngFwF8yPu3zwH4AoDnAXwZwM9RSocydYst7pcSrOZM4hOBNYzNnIPq0MVXb1n42omreNfNs1AU4nctf+r8Ci6v6hjLq6HdIjNKAGFmxf+cFERdic8KLRALtRbOLTZw24FtmK4UsNIw2wLKuaW6UCpppXRgBtwgFK2DYs+vCSQ+/0a32qW0LD3NeNQSRm1khchmHl3ke3XydWYzd1+bSXzbxnL+jj1J4qOUop7SVYMHe09RmY+X+OJcfHGDMws5FZSGB1iKHJ0i8MEii0kC6G7s+4kr6zg9X8d33TSb6fjoplRkM2fzoOIYtp7RxVcuBI2CX5xbw6tmq9i3vYRzC8NlUN3fTQMEpfReRAqAKaUf5n7W4c6jEv3b3wTwmwM9QQF2eywjKYmYtKtkdQdNwwYh6Tp5p3VQXztxFS3Lwbtu2QXAZUI7xwt46vwyTJtidqIYygvs5aS83RyDmirnfTNCWqGuQ93zYzs/1l/stv3b8PJVVzZYrBl+cGduQtGi0LLau5dHIWJQVoxJ4tBUGc9fXgPQPmoDaG/MmYZ1NqywV4kv0t0i+hmzuqFT83W84XC6LBQFax2UBariMvsLXturbeXAEp8UoJqmDYeKu7qLcGBqDJpC2loetbgAFdffMZDE2xmU+xx2W742Sy8+hqwmCZaDopSmmh0YvvTMZSgEuPPmbAGK35RSSoXXR1CETpHX2s8jkPgyMChvg3libh133jyLq2styaA2K5hFO8koUTfiOw2MeQyqaYablsZBVAf1wuU1vOcPHxK2GPr7b81hulLA6w+6Q48JIbh13zY8dWEFl1ebvkTJwBjU5FgutKNUFIKdnvMsMUAJdrxPnV+GphC8Zu+EP5abz0NdWdNBqbjGJm2GDSBuFiuymQNum50zC3VcXG6g0RJ3hTZtmtq1m6FvDCpSBxVl3LvGiyjl1K6dfIY3XykrqsWcf41NljgXX0Ij4KSxIyLkVAUHp8ttrDDamkg09l3U0RuA0MzhF+qmTdQNSXzZc1AOzd4eiFKKL33rMt54eMq/F9LAS3yufNkeaNI2rlkKdQH3Onaom3Nabpi4fmcVB6bKOLfY6Dr/2Q1kgOoTKgUN1aKWWAvVaMUXt7KkpNtCKP2mELn4njy3jMfPLuPCUvs5PHluGd9+3XRoob51/yTOLTbw8pVaKP8EBMW6uyKBCwjybcm9+Npb0xw/v4wbd4+jmFP9m5KvTuen9UbRspJHBADMJJFeqAu4AQoAHjq5iIbJnJPhXnxAdoa6njCsMCvcHBQv8VkYy4WfT1EIDk2Xuw9QHdjMgYARjhc1aKqSqdVRTTBNNw2iprH+eHrvuxONfRdNlQXE87U6LdQFOmNQQPZuEkze+25P0cgCnhXGBea08hM9Qy8+INhcPOEV1V8/O46D02NomvZQO0rIANVH7J4oJTKopF5t5byGestuKxiNg+hCZIukaBe3rrutanjcut8dTrjeskLmByBgULsjjwNBvi2xm3kkQFm2g6cvrOI27zV3VL0AxTEolr/TY6zEaQurpiixvfiiDOpVOyuYrhTwzVMLQonP3wBkmB4LxHcJ6QRM4gu1shF8xkd2VHpiUFklPiAIuEzey6nu5OKkACWappuGozsqOLfYCF3PfkDxzldU/M0YelsdVK6d6Zm2k6kPYTcmifEOA1Sn8h4QdvE1Y6TNXEpuWrds5FWlrdQlCva9P3nObU58w6zLoADgLOfk000bf/jgKX9OW78hA1QfsW97CeeX4jXaaDEoD94kkSlACS5EZtONw62fKQAAIABJREFUBijHoagLEu637Jnwb9Yog6oUNOyeKPoOKx4smKX14gMCSYEV6DJzhkjiYwxKFzjE0oasAUBeEzAoR5yDIoTgjqNT+ObJxUDii+SgAKBlZ/PY+AGqpxxUWJ6JuxaOzJRxcbnpf7aOQ/ELn3sKXz9xNfU1OjFJAEHAZS2ZCCFCCzcP0TTdNByYKsN2aKj9lu+64xhUnElC1EkCiDAoO9t7VxTiS2BZTRKdMKhu5D0gzArjmkqnMaimYfuBLgmsRODJc8vYUS1gWzmPg15xNZ+HevLcMn7n71/Ey1e6r81LggxQfcThmQrOLjaE7UAopV4OSnzTlr2kpKi2QQT/YrXbGVR00FjdEEsupbyKV+9ym1FGGRQA/PefvQO/8I7r2h5/nWcTT9pdskDM2Akr0GUMqpRXUc6rIbmAMSjTpm2BpmU56RKf0p6DsmJcfIAr8y3UWnj64op3zt0zqHXdQp6TwLpBdFGN6xh/ZKYCSoOF4sGX5vF3/3wJ937rcuprmB3noFiAih89HkXc3LMkjPt9/4JrNzoapJhrZ1DxJgmxxJe1Bowt/Nk7SXjDGDMEqG7kPSC8KY3rqRfkoMQbiCxSORCw35eu1HDDLrcp8+7JElSF4DzHoP7p5AI0heD2Q9s7ei9ZIQNUH3F4ugzDcvxhfzwahg2a4GwqFzQ0vF58nTAoXsJgN3e0TX6S5HLrPjdgzI6355pmJ4pCxveuW3bh8V95R+KFHkh87rk8cmYJs+NF7OXs6zPVAhZqQT0PL49Gcw1Z6qA0lcCM7BytGIkPCPJQX3n+CoBoDqo92XxppRm7Q661zJ7YExBU97P33oiRewOruRugPvPwWQDA2QwW4E5cfABQ9Tpj8A6+tEa6L3tmh9nx9k1PHIINDRegIiaJsXx7DorlVGJNEtzxncib7HyyfqedTNXtRt4DYnJQse7F+BxUmkECCOdSb5h1N7E5VcHebaUQg3ro5AJu3T/ZkZzbCWSA6iMOs4VjoZ3u1mMaxTJUCioM2/Fn0KRBtIDGSXy1BAv0nTfP4shMGQenx1JfsxPwEh+lFI+cWsSbjkyF3InTlUKo3RFvMIk6+bJIfDlV8TtHMAQ28/ZLfc9kCYemy3j6QjuDErkkf/xPHsPvfvlF4WvX9ORZUFlQiLC2OEn40HQZhACn5ms4u1DH10/MI68qOC3owBFFpwGqUgxLfEB7QXEU//DCFdy8ZzzUZzANfkd/TsKLjqcvCRhUnNQVFBRzLr4O5M1SXkW1oGWamwVkl/i6lfeAcPBpGu77auvFl5aDytAyDAhvZvmRH8zJBwCrDRPPvLLqb/QGARmg+gjWbTpaEQ8ELCZJ4gPcnEwmF59Aa16LMUmwJrUiV9UdR6fxwAffmqmSvRPwLr6XrtSwWDfwpkjdznQl3E3i8qrOjXMIL0StDO6znHBgofv5xC00dxwNzkko8XGf7yvLzdhOIWnDCrOAX4Bsh6JlOcLNSimvYs9kCafma/jsI+egKQQ/+sb9WKi12toFRdHq0iSxvRw/WJHH/HoLx88v452v7owd+AxKIPGxz6WYT8hBtQ3uE0h8tuObCNLPR81skABcOVNV0kdurLcsnJ6v49uv67z/ZygHFeviS26BlmWjB4Tzh9fPBgHq4NQYzi66rbYePr0ASiED1GbBVDmP8aKGMyIGxRLHMYGABajlhpna5gjgRkYIXHzRHBST/AZFw0XgA9TDp9zJudF+Y67E5wYo3bSxVDf8kQP8TplSmq0Xn5eDCre3EZskGO44EtxcYZNE+EbXTRt1w46VcNKGFWZBsADZqYMrD89U8Owrq/jCExdw582zeIOXA0iS+Sh1c3vdmCTaBivG5KC+9uJVUAq848YdmV+Dfx2eQUVzUK7NvJ1ZA+25GFErsE7eezHXWYAihGC8qKUGKJajmoo4arOAXcOtTDmoHiU+T9pVFRIySh2YKmNdt7DcMPFPJxdQzqt43b7Jjt9LVsgA1UcQQnB4phLDoJKDBL/7TmtzxBDtuJ0q8Q0xQDHbr27aePj0IvZuK2Hf9rCMyNodGVbQsJaxUJ5BsfeYXqjr3sB8exsrhUG5sqP7d37xykcWODYBN27mT9qwwizgZalg1Ib4OY/MuE1j13QLd7/5IA5Ne/3sBJsjBjd4pxeq8vAZ1Fg0ByWW+L7y/BXsmSzhxl3jwr/HYSySswTaa9hKObWtCD1W4hM0tTWs7AaRO2+axfe8pjMTQ5Z+fL3Uy/EOSlaKEddJIslmnoVBFXMKFOIyJv543sn30MlFvOHwVEemm04hA1SfcXimLA5QRnKA4nfKmQNUpF+cb5KIkfiGGaAYC6y1LDxyeqlN3gOA6aq76C3WW7jsOfgOewstL+VkGVYIAJp3o/AjNywnPgcFuB26b9kz0da9I5qDWvKas67rYgaVNqwwC4rcopo2uJIZJW6YreLYgW04MDUGQtoZVMuy8Yr32UYZSRawkRuTY+kSX9Ow8U8n5/HOG3dmbvfDwO6LeiuBQYnqoEwbmkLavl9RDsqwaeb3/pNvOYSfe9vRjt5DlgDFGFQ1ozswCsZe4yU+1uooiUGlry+EEFQKGm6YDW80WC3Uw6cWcXqhPlB5D5ABqu84MuOODmizeqfkoPjgkUXiA8IjISilCRJf733iOoWmKsirCo6fX8Fq0xSOE5hhtVDrBi5FGBS/ELG6qCytjoAIg7LF3cx5vO/2/XjbDWFJKpqDWm64ASqWQWUYVpgGvlO4qHiYB8sL3P3mgyCEoJhTsXui1CYv/9GDp/H2//R1XFnTuwpQxw5sw3fdtBM37wkWqmjHC4Z/OrkA3XTwjlfvzPz8/nNq7o496uIjJPjuip7Ex7efEs2CYs8HhCU+wytSHRSmKgUs1pO7LPTacYRNXY4NUCkuvlbGHBQA/Nr33Iif/o7Docf2bS+BEOAvHzsPIJzDHQRkgOozDns5lDMRRxULGnFmhHI3Eh/X0LRp2n79VXSXz3T9Tgon+4FiTsEjpxYBtOefAGC6GhTrshooloPicw2tjO1ZAomvnUElubHed/t+/Jf33Rp6LGpCYQyq1rKE/fnWW1bmYYVx4E0SaV3tjx3Yhj//qTfgh48F488OTZdxJjKv5+FTi9BNB5956Gzmbt48dowX8Uc/diy044/2DGS4//k5VIsa3nC485oYd+SM1sag8qriszG2GIeGOgqm6QLihdrsgEF1g53jBVxZSwlQntzeSX6LR8FTTdj10dbFPZOLL9tn8J5j+/CaveH8UkFzN0IXl5uYruRDDr9BQAaoPsO3mkda0aQ10AwxqIyOOn5mER+U6hGdvh9FpN1gLK/BsB0cmi4Le/rNcP34Lq82MVXO+3ZdXcCg0irgWTGuFWJQDjSFdCw5MUOGEWFQlLZ/vi3LhmE5vTMoQaeAuGvB7YQxHWpZc3B6DGfma75JxLIdvwj5s4+cw7KXR+t1kRYNc7QdigdeuIq3Xb+j65zEWCGcY2pFLPFsgnOIXZu2kFmzDUZ7Dqqz66AT7Ki6Q0uthP6NzGTT7bXC7nn2vqMti1JNEhkK3tPASlLuODrd8X3VKWSA6jNYLiCah2I7nnLMjjjMoLJ9LXlu6B4zSKgKERTqWkNnT0AgT70xZiyE3zC21sKlFR27JovCyam+Uys1ByVmUFlrWXj4C5wXHJfrgbTXxlD7lOPji0v9urmMbBoADk1XsOY5rAC3vVTDsPHjbz6INd3CZx85F3qdXs4zKiH984VlLNYNvOPGzuU9hnJeC7v47HBpgejaEM1EAlyJWVNIu4tvoAyqCEoRKj6Pgt2nXUt8Xv4v7n2nN4sVB/ROwPJQvAN2UJABqs8o5lTs3VZqK5qstywUNMVP5EfBB5BSLtvFy+cCWA3UjmrBN0Uw9COB3w3YTi1unHUpr6JS0LBQcxnUromScHIqe49pDCrvmyTCOahudvTRJDtjUEB7Hqofozair9lMkfhEOOTtbFkeirWXev9bDuHYgW34/BMXAHTm4os7z6iL72svzkNTCN56fef1PQxjBTVUB2VGXHfM0cgbaETj3v3zjJiIOnHxdQPWADlpqva67q4D3aoZbHMQ976TXHyU0syt1JJw3Y4KFALccZ0MUJsSh6crOC2Q+JIWsIKm+vJD1l0zz6DYIjk7UWw3SbSs2PqrQYK9D5GDj2G6ksdCzcDlFR27J4rB5FQRg8owbgOIMijHf7wTBAwqnIMC2hlUP4YVAmGJL2Dc2Z+TWc3PeE6+4+dXMFMtYO+2Ev63/+Vw5nETWc4zWgc1t6ZjR7WQuXedCGN5LSSfRlsTiTYvSYWnzFAQ93z9BpuTlhSg1nSrawcfEOT/0nJvogBl2OIZUp3ifbfvx9/93FuwZ7Jdtu83ZIAaAA7PlHFmoR4qGG0YNsZSZDYm82UOUJzUwhbN3RMlNE07pIP3w2HWDbaX87hhtoqZanxLl5lqAWcWau7Ij8lS0ElCYDNPlfiUdout5dBEB18cosnm5YbhP0+0W0M384+SXtN18bE6qOyLyd5tJWgK8RnU8fPLuG3/JAgheMerd/oGlN5zUO0S37pu9rTwAq783TDaTRIMpZjNS9xnFK3X6rSTe6fYOe4xqIR5SWu66TfG7QbMGKXHSZuKOw5FlIPS/XKN3j6DYk7FLXsnenqOrJABagA4PF1Gw7BD49+zsBj296w7HF7CYIsmG5vBa/l1I37U/CDxkbtuxqfuPpZ4zHSlgBNz7vj33ZMlEELcjgHcApjVJMEYaLtJovPLnBASKoReqht+oXGUQfVj1AYQ7sXXTLGZi5BTFezbPoYzC3Us1Fo4t9jAtx1wmwGrCsH733IIQO8dRQqa2zeSdzOu92ETNFbQQuw/2jeQ5WajEl/cxiUaSAedg5qqFKAQ4GqKxFft0sEHBOy1aYqbSrPrVsSgWhmViFHC8FetLQDm5Ds9X/fda42Ece8MLA+VdVFiCwUQLJpsbEatZfmOuJpuYf/2/jaDzYLojCkRpisFv26JDUcs5pTQIpR1CmhOVKhrd2eSAMKF0Mt1A9ftrOLMQr1tpMJ6n3JQhBCfFRvEDbidynGHpss4s9DA8XPh8SaAK83smiji1h5b07CNgmE7KCpBQXZ0IGanKOfVsM08ElDYwtqI5qBiGZTadSeJbqAqBDPVQkoOqjcGxfJ/TVOJvd7yAoYLcPfRJgpQkkENAEHT2CAPVWvZsZ3MGQKJL7vNPMqgWAdpfie63toYiS8LePlv12QwSr4ZMklkK9RluSbD4lsd0a6txQVN8efqLDdMHPDavKxFGNRqj90Boq/ZsuxYl1YaDk6VcXahjifPLyOnEty8J5BiVIXg7a/emTpNNcs5AmEL93qPuRWgPQfVipH49JCLL942HTVzDLoOCnDzUEm1UGtNs6d70TdJJFwfBa4+kkfWgvdRwuY5002E2fEixvJqyMlXb8UPK2RgO6LOevF5dVCeCUM0+K2+QSaJLGBWc4UAO71gVYzM/dEztjoSMijHiXVOpiHP2soYblJ6dqKIvKq0SXzMQMEP9esWzEbszoLq/Ds7NFNG07Rx37NzuHH3xEB2y3xTWwY3B9XbNVYuqN7cNHeDEZXkRDbzVoqLjzEJSqnbzXyADApwa6HSXHzVHgq6WQ1aUu4tFyPxZS3XGCXIADUAEEJwaDrck6/RQQ6qE5OEwZkkqkXND3IscW87FA3D3hCbeRZMV1xZaEe16AeSohYNUFkLdUU5qO5MEkCwE2UW8+1jeVSLWpvNfLlhYHIs13UgjL5my3RQzzi4MgrWyeTsYgO37R9Ml2nRULw1vfdmuWN5zR8zAiSYJNokPvHnzvcMZDJyrwaBNOwcL+BqgkliXbf86bvdIM1mDrT36GTwhzt2cV1tFGSAGhAOz1RCnaVrrXSjQrmgCRtfxiFqkqgWNT8QMYmPSSbDbBTbCZjEt2syyFdFm4L6dVCp86BiXHxdSnyMQfkMqewGqCiDWqwboW7fvcDPMcSMe0/DQS9AAfANEv0G3zOQ/b8fnTRYETvLMUVNEtESBNN2YDk0xSThPZff5mmwnQ92jhexVDeE3d5N2w0sPdvMvesjjh3HmSSCco3Ns+xvnjPdZHjt3glcWGri2VdWQSlF3bBTuznsHC8kWrKjiNrMq8Wcz8LWI+PfRzVAMYlvN9cKKTr3p+WNe09rqyJsFutQqF24+IDAhOIzqHIe46Vcm818uW70bBDgX1M3XYmvG1l213jRD+S8QaKfYBIf+458m32vOahCeHMVNUmwhrJsofUbpsaZJHLB3CozMp13UGBW83kBi+q1USwQYVAx7zsfl4NiSoSU+CR+6PX7MF7U8HsPvOxPSE3LKfzs247iCz/9psyvkdfcAX2OQ9skPp9BpfQA3Gj4DIpz/EVdfK2M/cMYUwq7+BzkenDxtSybyzGJGdRS3cC2vgWo3hiUohAcnCpjdryI3QMqpIxKfP1yMbKAHGJQXEBhJQjs2mC1crEmCU7i8xnUgCU+ZlISyXxsY9NLMXMhp4JSdxPWscRnbT4X32iuWtcAxos5/NS3H8bH738Jj51ZApB+A1cKWkc3uT/11XZQa1k4OF32AxHb1a73qUZnUCjmVPz2D94S6tfnjlUI56Cy5A5yioBB9WIz96SS5XrAoKqFHK6uhbuELNWNvk0VZTvkhmFj92R3C8n733IIptO+QPUL0VEWzJDTex2U+36ZLB2V+ICw/OvnVBJNEkGwA4bAoKpegBIYJfrBoEQ5OdExwgBlbD6JbzRXrWsEP37HQXzqH0/jd/7+RQD9ZzH8SIF13USloCGvKchrCmpGmEGNqsQHuPU5PEqCAJVl15fTmEki7OLrxg0HuBJRo+42XyXEHUgXZVCUUiw3+sigcipWmyYaXTIowGXvgwTr9B70geyPzZ5do41WkDeK5mOLXAlCqsTHSeAsLzlom/kO1k1CYDXvdVghEDYKiVodAe57jJZCALzNfPMwqM0TSjchxos5/ORbDuH5y2sA4juZdwt+LPmabvkW8ypXkT/qOSgRim11UE6mXV9sq6NuTRJeJ4nlhoGJUg6qQtpyUOstC6ZN+2eS4HqtdePiGwaidVD9YAZA4F7lGVSUOfMSXzMl6V/IBYW6TOIbNIPaPpaHphCh1ZwFjV5dfAxZm+QyZO1pOUqQAWrA+Ik7Dvk3br8ZVMG72Wq6FXJRlQuaH5hqm4BBRRF18bkSXwYG5TeL7ZPNPOfWnCxxLr1q0R0JwVjaUi2Q//qBYCBdd3VQw0CbxNenABXkoCy/bilJ4mum5qBciY9SCtMr3h50HZSiEOyoigcX9iUHxd0HcRuYvKYIR74HHVk2z7I/UmdKCNlOCLmfEPKy93+hDYkQcrd3zMuEkLu9x8YIIV8ihLxICHmOEPI7wz17MSZKOfzkHW4PtH53c2B0nyXxmXRQKWj+yI3NGKDYaG9WsKmbGRlUXKFuly4+nkExCY99xuxzXWr0O0C5xapxo8xHAVGJb71PEp+fg2rZsBwKStsZT5E3ScSMPffPU1PgUJdFs44gg5b4ANcocXU9nkH16uJj6DgHZdrQFNKXer1hYdTO9EMAHqCUXgfgAe/3EAgh2wH8ewBvAHA7gH/PBbL/SCm9AcCtAO4ghLxrOKedjJ/5jiP42L+8pW18cq9gNy8bkMYufDdAuYsG292OqotPhGJkFlPL6o1Bqd22OvLmbS3VTWzzGBSTUZmsxRso+oFCTsFK033OkZf4BubiswJTg0Di081IgEpgEuw8jSExKICNfheZJLyxLD18TqK6MNExcYW6m0neA0YvQN0F4DPez58B8P2CY74LwP2U0iVK6TKA+wHcSSltUEq/BgCUUgPAcQB7h3DOqSjlVfzw6/d37SaLA7tYF+uunMB2sOVC0HSzZli+cWKzINoxICuDysXkoLq2masKDMv26pzcz5Z9xswYsNjvAKUpvhQz8gHKDFh6oQ/XGPve6y07MUBdXG7iLx49jxe9LvjxDEr1z9MYkkkCiO/Ht65bKOfVnhgMv1HruA7K6n2a7rAxatvqnZTSy97PcwBE86P3ALjA/X7Re8wHIWQSwPcC+L1BnOSowA9QHoNiO7NKMYezi+7QulofWtAMG35TUC/HoVu2LyslQVEIFCIYt9HlgsBcYC3LaGNQa80BMahQjmE0vzd+sCLQ+xA+BkUhGMuraBiWv8mIMp63Xj+DR88s4v/+m2/5jyW5+Nh5DqtQF3AD1GrTbHOf9mNmFu/ii5f41FiJbzMV6QIbEKAIIf8AYFbwp1/hf6GUUkIIFRyX9vwagL8E8F8opacTjvsAgA8AwP79++MOG2mwi22xxhgUk/hUP0dSz9BiadRQjDColtnu5opDTlVCNUDdDiwEwjblaA6KyTVLdQN5Tekb2+F3uKPKoHKqOxSPz0H1MkKCh9vR3PafO8p43nv7fvzw6/fh3GIDT19cgW7a2FEVj3UpcFLxMBkUG/1+da2F/VPBmJu1Zu9TBTLloGIkvpbpbKo+fMAGBChK6Tvi/kYIuUII2UUpvUwI2QXgquCwVwC8lft9L4Cvc79/EsDLlNL/nHIen/SOxbFjxzoOhKMAdrOxHNQ4Z5LwbeYpo+ZHEdGeay0rWx0U4AYoK9LqqJdefAzMxccswizvslQ3MFXOp7ZhyoosEs5GgxASKoKttay+FYKXCyoaLcsPKKKNCSEEB6fLob6DIvBd180h9eIDuNHv63ooQK23TIz3MKwQiOSgYprkMomPUhq6Ll1Gt7kkvlE723sA3O39fDeAvxMccx+A7ySEbPPMEd/pPQZCyEcBTAD4P4ZwrhuOwCQRZlDlgoaGYcN26KYMUGxhZrmYlulkHhGgqSSUgzK7nKgLhINFEoPa1qcaKPc1eQY1ut8bPwywH9N0GRiD6kfnB75eK46RDQJ+gIoYJfrxOYU2MAnuRaB97LtuxU8fHlWMWoD6HQDvJIS8DOAd3u8ghBwjhHwKACj9/9s71xhJruqO/05Vdfc89zG76/Xu+rHGa+wFE5PNYvxSZGwDRopYK8qDCAmj4FhCioggCYIQBQXyIRGJUBIljngYnCghKE4I/pI4xgpKPvAyBBuDvdgEG6+zu6z3OTuzM9OPmw9Vt7q6p3u2Z/p2V93u85NaM11d03NvV1edOuf87znmFPBx4FvJ42PGmFMichlxmPA1wHdE5Lsicl8ekxgWTZFEkoPKqPggXvDo8u52WNh1Gqlaq1a/aKsNSxQELSq+eh8hvhYPKhVJJDmopabM3FX+CVpzDEUN8UFr+HN+qdpXj6MscVfd7iq+9Y2xmSurDmmhLjQLxrYLJeJmhX3moHpR8SVzbA/zrVUBvagU6spljDkJ3Nlh+xPAfZnnDwIPtu1zBBi8/14g7Jf11MIKE6UgTShne0ItLNeZ3l6ow3xR0sZ0iRdYrXdvqdBOOZTWUkd9yMyzF0frJZXCgMlS2OJBXb51quPfbwQfQnzQ2q12fsndTdBUJeLshaqTnFG2LYgLg9crmydLlKNgVT2++Uy1l41iz/lS2L0tj51ju4FaqjaYmy6aT7I2fo1WacF+WU8vrrTcmVlRxMJyLb54+Bbiy6j47EWwZw8qjCu8W2qNRio/Xy+VDgYKaKnHd8phq432/1lsD6pZKfy8wxDfdDnOQVnVXT/rliqZC3U3VeAgEJGOa6HmHagdrZp1LU+ovEaIrxc1bJFQA+Ux9otoTOvqdHs3e3651lOr+aKRVfGttzxLFEp6YjYahoahb5FEILQkt21X3ZVag/ml2uAMVKm4Nxa282+jYTi/4kZmDnEOanGlzrILDyoT4humBwW29XszxLeUrMXq15Db8N1aVUZKXUJ868nlFgU1UB6TDQdlLxDWYzpzocqFap0ZR/mBYWEN1FK1vu4Cl6UgSEN8Vm6+cZl5/D+3TJVbFlnPTpSYX6pxZrHZadcV2TvcQof4EhXf+ZVafIPkyEufroQsZCtJuBBJ1OqsJHnJjX4X1svOTRWOZ8odnUvr8PX3OVmJ/1redfcQn6r4lCGSvRvMfvGtgbIx8It18i0aaQ6q2lwP02uIrxRJKjOvJ6G+jS7UtZ/v1qlWA79pssS5pVpah2/bADyoUiiFrv5hQ3yuCsVapsoRi5lKEr2uf+s4RpuDqjbS5oeulgNcjEtmJzh+dimtJzmfVjLv72bRSvzXDPGFraWoLL22rSkSxT0DlIsSBpLe2c92MFDHzi6ves0Hmiq+RtOD6lVmHgRUE8NU7fOu2V4c20N4cQ6qmlYyH4TMvKiFYi22TmGz1YY7Fd9KPa7mDm5VfMM0+Hu3TbGwUk8767pqSQLxvNbyrrvLzHsrGVYk/Bqtsopym3Iv+/uxxIPyLcQXhQGlUFo8qN4X6kqaYE89qD5l5u0GaNNExLkLNeeVzKF5US3yGiho9q1KC6A6VPEBnF6M37c/A9Wq4hvGIl3LtZduAkjrBbpoVmgpR8GaNzCdQnzVeoN6o3c1bFFQA+U5NozRScV33NMQHzTbKlgPqtdQTxQEabsNm4vacIgv7Gag4qaFpxzX4YPm8Syygg9iQ7pSazDvqN27xTb1PJ0YfxcqvuXq8D2oay+dBeCHiYFKQ3wODFRlAwbKx2aFoAbKe+xFNHuBKEcB5TBIDZRvIT6IQ1zLtYyB6tWDipoLdat9elA2HNIugpidiFiuNTh2Nv58t0y581DTEF/hDVRriM9VLT57c3XWgQcVhQFhIKmKbxgSc8vcdJkds5XUg2r2zOr/c9o+U2FHUu+vE50W6qZq2IJ/r9rx78qltGBP4PbQwcxElPGg/DvM1oNaXmeyvBRI6kHV632KJML4ZLZVJCz2s37x1CKbJ0tOL3zNEF+xLyR2oa6rZoUW6+1bD6rfyg9WbdipO++gue7SWQ4fPwe4zUF9+l0H1xQN2XlmS341c7l++SR+jVZZRdNAtX7xpythWkTWt4W6EHtQFzYgM49CSdtqbvJtAAAPNklEQVR79ysz3z5b5vK5SV63p7XRpC0Y++LJBafhPWiG+CYLn4OKa/Gdd9Ss0GJzb2kOyomBaqr4hsmrd87y3PHz1BuGc0tVAmk2ZeyHHbOVNUOFnRbq2gXvvoX4in0WKBfF3nG3h1hiYcSF5Hf/DvNEOW77bguS9qo+ijLtNpoy840ZqKlyxH9/8I5V223duRdPLvLqnbMbeu9uWE9xquAXkmyIL0z6OLnAXsDPLK5QCoWgz3VL1pAOOwcFcR5qudbgxZMLaUWXfufTC51k5hdW1ic2KgrqQXmOPenalXrZ6hFehviiIFHxWZFEr7X4mu02bIhjo9XMu2G91fmlmlOJOTQvLlMFF7ZUolgOfvZClZlK5Gx90VQa4qs68XjKmRDfMHNQEIf4AA4fm+ecg2aFvVLpJJJIPSi/Lvl+jVZZRaWDSAKaXlMlCoZ+YrpgshwmlSTW6UEFzWKxtQFVD8heaFwu0oXmQkwfclAQ1yJ06aFnPSgXHo/19Ko1M/QQ3zWXzCICh4/PO2lW2Cuq4lMKQ/ccVNRxuy9MlmIDtd7YeRQGaVmbWp8hvm7YHBS4LXNkefsNu7lt33bn7+sSe5d+4vyy0++Y9aCqdePkxsouKF6uNygNOcQ3WQ65cm6Kw8fm467DfVaR6JVOOahmTUu/DJSfVy8lpdJNxVdpNi/0kYlUJNEgkN69oFIoq9dBOQ/xNT/rdoWfCz7xyzc4f0/X2JDrK+eX2T7dXfK8XrK5NzceVLxcoZqDSALiPNThY/NMlEJ2b+ncmt41nWXmGuJTcqCbB2UNlI8CCbAy80ZaP6zXHMdMJW6F8bUfnexbJLHW/7DMObw4+4S9MXplftlpQ8woDNL3dhbiqzYSmfnw28Vdu3OWF04uJJ7mcDyoKAwIREN8SgGwi3Lbv3jTnntQk6WQ5aTU0XoKhr77lr1cvWOaez/3TR79/jHAfQ4qDCSt3j0ID8oHbA7qnMNeUBb7nXXh8aQ5qHpeHtQmGgZOzC87W8zcC+UoaA3xrbPoclHwa7TKKiaisOMFwm5z1QZh2EyUgnQd1Hru+i7ZNMEX77+Z/bs28dDXXgQ2vlB3LeznO74eVLbVi9vvmBWI9FPJ3GJDfMOuJGGxJY/A3WLmXiiHQYsHtawelJIH77rlSj526PpV2+1dqMvwyzCZLIXUGoaFldq6T6qt02X+4b43cuu+bUCzvptL7MVmzrHM3BeyxsN1MWKr5HMS4isFuVQzt+zdNtU1DD9IypmOx8C6uwIUBT+vXkrKa3dv5rW7N6/a7rtIwtaiO7NY3dCd9HQl4sF3v4EnXjjNvktmXA+v6UHNqIFy7kElSj4nKj6bg8rJg4rCgH07ZvjB0XNDU/EBlENZVYsvDGSoFd1doB7UiGINlK8hPlsc9vRitedCsaveIwq5dd/2gTSp2zRZohwGA/HOfCB7TFznVpx6UDbEV19fLtMldsHucD2othxUtc5ENLyGja5QAzWijIJIAuDs4kohC1xumSqxfabs3QnvilYPyq1nYHNQbkUSbtZVbYRrUwM1RA8qClhJ1hBC3J3at/wTaIhvZPFdZm4N1JkLVfYV8MR63x3X8Gs3XpH3MHKjNQc1IBWfoxzUhWodY9y830b4uSu3IgKXbZ0c2v+MDVRriE8NlFIYts2UEYl/+ohdULi4Ui+kB7V3+zR7t0/nPYzcyIb4BqXicxXiM/FyuNw8qIN75/j277/ZeeX7tSiH7TLzuncSc1ADNbLs3DTBP7/3Fl63Z7WAwgeyHUM3moNSBscgQ3zTmTqS/ZJ9j7w8KHDbdbkX2j2o5WrdOwUfaA5qpDlwxVYvC8VCa+fPInpQ485AVXxltyo+S9kzBVs/lKOwQ4jPv/PIvxErY0H2bs/H2PmoM8iFuqmKz0mxWLe1/XyhnCmaDKx7wXtRGJ8jpnjFZMaDykserHSnFApWwFhokUTmPXyNJmyESpuKb6nmp4HSHJRSSLI5KB9PrFHH9q0SxHkpqemKW5GEZaw8qFXroPwM8amBUgpJ9mTy8cQaBypROBDvdsrhQt3ymHpQ7bX4FpfXXzKsCIzPEVO8Insy9druXRkulSgYSHWEaccLdS1j50ElBur8co3/O7vEVdv8WxZRqCMmInMi8piIPJf83Nplv3uTfZ4TkXs7vP6IiDw9+BErg6ISBWmOQz2oYlIpBcwMoDrC1IByUHm028iLrIE6fGwegP27NuU5pA1RtCP2IeBxY8w1wOPJ8xZEZA74KPBG4Ebgo1lDJiK/CJwfznCVQSEiqZJPPahiUonCgfQ4cupBjauKL5ODeuboOQCu2zW71p8UkqIdsUPAQ8nvDwH3dNjnrcBjxphTxpjTwGPA3QAiMgN8APijIYxVGTBWyefjCvhx4OZXbeOmV21z/r57tk5yw2Wbud7BIvNxVfGVw4Bq3dBoGJ49do7ZiYg9W4ZXaskVRRNJ7DTGHE1+Pwbs7LDPHuClzPMjyTaAjwN/Bixe7B+JyP3A/QBXXDG+NdWKjFXy+ZjcHQc+fs/qPmQumCpHfPk3b3PyXuMc4gNYqTd45ug8+y/d5GVh46EfMRH5iog83eFxKLufMcYApsvbdHrf1wNXG2O+1Mv+xphPGWMOGmMO7tixY32TUIaC9Zx0HZSyUVpDfP5doDeKPWeWaw2ePXqO/R6G9yAHD8oYc1e310TkuIjsMsYcFZFdwE877PYycHvm+WXAV4GbgYMi8gLxvC4Rka8aY25H8RL1oJR+afWgxud7ZD2oH504z8JKnes8FEhA8XJQjwBWlXcv8OUO+zwKvEVEtibiiLcAjxpjHjDG7DbG7AVuA36oxslv1EAp/dKSgxojD8qGM5966Qzgp4IPimeg/hh4s4g8B9yVPEdEDorIZwCMMaeIc03fSh4fS7YpI4Y1TBriUzZKSyWJMcpBWUHIk0fOIgLX7tQQX98YY04Cd3bY/gRwX+b5g8CDa7zPC8BgMrjK0JhQD0rpE1sz0BgojdGNjg3xPXnkDFdtm26pbekT43PEFO9IZeZjdGFR3GJrBsJ4eVDWQP3viQVvw3ugBkopMLYPlHpQSj/YMN9YrYPK3NRdd6mf4T1QA6UUGOtBaakjpR8qUUAYCGEwPiKJSsYYqwelKANgsqSljpT+qZSCsQrvQZsH5ekaKCiYSEJRstx41Rw/fmVhrO58FfdUopDSGLV7h6aB2uRpiSOLGiilsNy5fyd37u9U7UpReqcSBZTHzAu3Buq6XX6WOLKMl9+rKMrYUYkCyuPmQSUhzf0eCyRADZSiKCNOJQrHqtUGwJapMqVQOHBlx5Z63qAhPkVRRppyFIyVxBxgbrrMV3/3TezePJH3UPpCDZSiKCPNgSu2snuL3xfqjeCzOMKiBkpRlJHmt+66Ju8hKBtkvPxeRVEUxRvUQCmKoiiFRA2UoiiKUkjUQCmKoiiFRA2UoiiKUkjUQCmKoiiFRA2UoiiKUkjUQCmKoiiFRA2UoiiKUkjEGJP3GHJHRE4AL+Y9jj7YDryS9yByROev89f5+82Vxpgd7RvVQI0AIvKEMeZg3uPIC52/zl/nP5rz1xCfoiiKUkjUQCmKoiiFRA3UaPCpvAeQMzr/8UbnP6JoDkpRFEUpJOpBKYqiKIVEDZSiKIpSSNRAeYaIbBGRh0XkWRF5RkRuFpE5EXlMRJ5Lfm7Ne5yDQkTeLyLfF5GnReQLIjIhIleJyDdE5HkR+aKIlPMep0tE5EER+amIPJ3Z1vGYS8xfJJ/FUyJyIL+Ru6HL/D+RnANPiciXRGRL5rUPJ/M/LCJvzWfU7ug0/8xrvy0iRkS2J89H6virgfKPPwf+3RhzHXAD8AzwIeBxY8w1wOPJ85FDRPYA7wMOGmOuB0LgHcCfAJ80xuwDTgPvyW+UA+HzwN1t27od87cB1ySP+4EHhjTGQfJ5Vs//MeB6Y8zPAD8EPgwgIq8h/k68NvmbvxaRcHhDHQifZ/X8EZHLgbcAP8lsHqnjrwbKI0RkM/DzwGcBjDErxpgzwCHgoWS3h4B78hnhUIiASRGJgCngKHAH8HDy+sjN3xjzX8Cpts3djvkh4G9NzNeBLSKyazgjHQyd5m+M+Q9jTC15+nXgsuT3Q8A/GmOWjTE/Bp4HbhzaYAdAl+MP8Engg0BW6TZSx18NlF9cBZwAPici/yMinxGRaWCnMeZoss8xYGduIxwgxpiXgT8lvmM8CpwFvg2cyVysjgB78hnhUOl2zPcAL2X2G4fP49eBf0t+H4v5i8gh4GVjzJNtL43U/NVA+UUEHAAeMMb8LLBAWzjPxOsGRnLtQJJnOURsqHcD03QIfYwbo3zML4aIfASoAX+f91iGhYhMAb8H/EHeYxk0aqD84ghwxBjzjeT5w8QG67h145OfP81pfIPmLuDHxpgTxpgq8C/ArcRhjCjZ5zLg5bwGOES6HfOXgcsz+43s5yEi7wZ+AXinaS7oHIf5X018k/akiLxAPMfviMiljNj81UB5hDHmGPCSiFybbLoT+AHwCHBvsu1e4Ms5DG8Y/AS4SUSmRERozv8/gV9K9hnl+WfpdswfAd6VqLluAs5mQoEjg4jcTZx/ebsxZjHz0iPAO0SkIiJXEYsFvpnHGAeFMeZ7xphLjDF7jTF7iW9cDyTXh9E6/sYYfXj0AF4PPAE8BfwrsBXYRqzkeg74CjCX9zgHOP8/BJ4Fngb+DqgAryK+CD0P/BNQyXucjuf8BeKcW5X4YvSebsccEOCvgB8B3yNWPOY+hwHM/3niXMt3k8ffZPb/SDL/w8Db8h7/IObf9voLwPZRPP5a6khRFEUpJBriUxRFUQqJGihFURSlkKiBUhRFUQqJGihFURSlkKiBUhRFUQqJGihFURSlkKiBUhRFUQqJGihFURSlkKiBUhQPEZGrReSUbUgnIrtF5ISI3J7z0BTFGVpJQlE8RUR+A3g/cBD4EvA9Y8zv5DsqRXGHGihF8RgReYS4srUB3mCMWc55SIriDA3xKYrffBq4HvhLNU7KqKEelKJ4iojMAE8Stxt5G/A6Y0yn1uCK4iVqoBTFU0Tks8CMMeZXReRTwBZjzK/kPS5FcYWG+BTFQ0TkEHG7+/cmmz4AHBCRd+Y3KkVxi3pQiqIoSiFRD0pRFEUpJGqgFEVRlEKiBkpRFEUpJGqgFEVRlEKiBkpRFEUpJGqgFEVRlEKiBkpRFEUpJGqgFEVRlELy//+hEwG9f14nAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "print('Accessing Fit Attributes:')\n", - "print('Objective Funtion Evaluations:\\n', res.optimum_chi)\n", - "print('RMS:\\n', res.rms)\n", - "#print('Parameters:\\n', res.optimum_params)\n", - "#print('Fitted y:\\n', res.y_fit)\n", - "print('Sign Combinations:\\n', res.optimum_signs)\n", - "#print('Derivatives:\\n', res.derivatives)\n", - "\n", - "plt.plot(x, y - res.y_fit)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel(r'$\\delta y$', fontsize=12)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To fit the data with a CSF we can use the 'constraints' keyword argument in smooth(). 'constraints' sets the minimum constrained derivative for the function which for a CSF we want to be one." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 17.524691343307495\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 14\n", - "Signs : [ 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 0.04997366530209926\n", - "Parameters: [[ 4.93743471e+02 -1.22808405e+01 2.13814758e-01 -3.19444274e-03\n", - " 4.37560407e-05 -5.59706086e-07 6.90272432e-09 -9.04292660e-11\n", - " 1.11944074e-12 -8.77169765e-15 5.93712196e-17 -1.83309620e-18\n", - " 3.79739211e-20 -3.40532152e-22 1.13892498e-24]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 1\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - } - ], - "source": [ - "res = smooth(\n", - " x, y, N, constraints=1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note in the printed results the number of constrained derivatives has increased by 1 and the only derivative that is allowed to cross through 0 (Zero Crossings Used?) is the the $0^{th}$ order i.e. the data." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(x, y - res.y_fit)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel(r'$\\delta y$', fontsize=12)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A Partially Smooth Function can have derivatives constrained via $m \\geq a$ where $a$ is any order above 2 or it can have a set of derivatives that are allowed to cross zero. For the first case we can once again use the 'constraints' keyword argument. For example we can constrain derivatives with orders $\\geq 3$ which will allow the $1^{st}$ and $2^{nd}$ order derivatives to cross zero. This is useful when our data features an inflection point we want to model with our fit." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 11.277308464050293\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 12\n", - "Signs : [ 1 -1 1 -1 1 -1 1 -1 1 1 -1 1]\n", - "Objective Function Value: 0.04967661128564428\n", - "Parameters: [[ 4.93744160e+02 -1.22808738e+01 2.13797953e-01 -3.19410879e-03\n", - " 4.38228511e-05 -5.60764306e-07 6.80628376e-09 -8.89234642e-11\n", - " 1.18037177e-12 -9.83236755e-15 4.35392184e-17 -1.47274085e-18\n", - " 3.85498374e-20 -3.87573378e-22 1.39326338e-24]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 3\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1, '2': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "res = smooth(x, y, N, constraints=3)\n", - "\n", - "plt.plot(x, y - res.y_fit)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel(r'$\\delta y$', fontsize=12)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To allow a particular set of derivatives to cross zero we use the 'zero_crossings' keyword. In the example below we are lifting the constraints on the $3^{rd}$, $4^{th}$ and $5^{th}$ order derivatives but our minimum constrained derivative is still set at the default 2. Therefore this PSF has derivatives of order $m = [2, 6, 7, 8, 9]$ constrained via the condition at the begining of this example code." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 3.629124641418457\n", - "Polynomial Order: 15\n", - "Number of Constrained Derivatives: 10\n", - "Signs : [-1 -1 1 -1 1 -1 1 1 1 -1]\n", - "Objective Function Value: 0.049724821013759544\n", - "Parameters: [[ 4.93744054e+02 -1.22808710e+01 2.13800665e-01 -3.19414659e-03\n", - " 4.38115602e-05 -5.60616156e-07 6.82331415e-09 -8.91658318e-11\n", - " 1.16913341e-12 -9.64513657e-15 4.66060212e-17 -1.54047402e-18\n", - " 3.84176646e-20 -3.78334129e-22 1.34361764e-24]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossing Derivatives: [3, 4, 5]\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 1, '3': 1, '4': 1, '5': 1}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "res = smooth(x, y, N, zero_crossings=[3, 4, 5])\n", - "\n", - "plt.plot(x, y - res.y_fit)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel(r'$\\delta y$', fontsize=12)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "While PSFs can seem like an attractive way to improve the quality of fit they are less 'smooth' than a MSF or CSF and consequently they can introduce additional turning points in to your residuals obscuring any signals of intrest." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/example_notebooks/turning_points.py.ipynb b/example_notebooks/turning_points.py.ipynb deleted file mode 100644 index 1d8cbc7..0000000 --- a/example_notebooks/turning_points.py.ipynb +++ /dev/null @@ -1,316 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Turning Points and Inflection Points\n", - "\n", - "This example will walk the user through implementing DCF fits to data sets with turning points and inflection points. It builds on the details in the 'Simple Example Code' and uses the 'constraints' keyword argument introduced there. The 'constraints' keyword argument is used to adjust the type of DCF that is being fitted. Recall that by default ``maxsmooth`` implements a Maximally Smooth Function or MSF with constraints=2 i.e. derivatives of order $m \\geq 2$ constrained so that they do not cross zero. This allows for turning points in the\n", - "DCF as illustrated below.\n", - "\n", - "We start by generating some noisy data that we know will include a turning point and defining the order of the DCF we would like to fit." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "x = np.linspace(-10, 10, 100)\n", - "noise = np.random.normal(0, 0.02, 100)\n", - "y = x**(2) + noise\n", - "\n", - "N = 10" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can go ahead and plot this data just to double check it features a turning point." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.plot(x, y)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel('y', fontsize=12)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As already stated ``maxsmooth`` does not constrain the first derivative of the DCF by default so we can go ahead and fit the data." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 2.1448731422424316\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [-1 -1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 0.04927823450747571\n", - "Parameters: [[ 1.17731858e-02 2.02215641e-01 1.00004968e+00 3.64579570e-06\n", - " -4.01724980e-07 3.79943911e-08 -2.52386154e-09 1.08957701e-10\n", - " -2.74734089e-12 3.07852441e-14]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 1, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - } - ], - "source": [ - "from maxsmooth.DCF import smooth\n", - "\n", - "res = smooth(x, y, N)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we than plot the resultant residuals we will see that despite the data having a turning point present we have recovered the Gaussian noise." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZYAAAEJCAYAAAC3yAEAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOy9ebAs2V3f+Tm5Z1Xd7e39lu7XUndLaIMGITxhFjGEZTSAZXsks9hhj00MdswQXrDHgwePjAnNjCVHCGJsCKMZYAjARowMhDCyZA0IC4wQ6lZr6V3dr5fXb73vbnWrKvc888c5JyurbtXd7333vZffiBu3Kisr61RW5vme3/b9CSklDRo0aNCgwX7But0DaNCgQYMGdxcaYmnQoEGDBvuKhlgaNGjQoMG+oiGWBg0aNGiwr2iIpUGDBg0a7Cuc2z2Ao4ATJ07Iixcv3u5hNGjQoMEdhccff/yWlPLk+PaGWICLFy/y2GOP3e5hNGjQoMEdBSHEK5O2N66wBg0aNGiwr2iIpUGDBg0a7CsaYmnQoEGDBvuKhlgaNGjQoMG+oiGWBg0aNGiwr2iIpUGDBg0a7CsaYmnQoEGDBvuKhlgaNGjQYC9YvgQv/v7tHsWRwpEkFiHEdwshnhNCvCCE+PEJr/tCiI/q1z8vhLhYe+1tQojPCSGeEkJ8VQgRHObYGzRocI/hcz8Hv/kjt3sURwpHjliEEDbws8C7gTcBPyiEeNPYbj8MrEgpHwJ+Gvigfq8D/Crwd6SUbwbeCWSHNPQGDRrci8gGkMW3exRHCkeOWIB3AC9IKS9JKVPg14H3jO3zHuCX9eOPAd8lhBDAu4CvSCm/DCClXJJSFoc07gYNGtyLyGMokts9iiOFo0gs54DLteev6W0T95FS5sAacBx4BJBCiE8JIb4ohPjH0z5ECPEjQojHhBCPLS4u7usXaNBgKpZfgk/9BJTl7R5Jg/1CnkCRQtPmvcJRJJa9wAG+Ffir+v9fEkJ816QdpZQfkVK+XUr59pMnN4hzNmhwMHj2d+Fz/xp612/3SBrsF4pU/2+87gZHkViuABdqz8/rbRP30XGVOWAJZd18Vkp5S0o5AD4BfOOBj7hBg22iiNbUgyy6vQNpsH/IdXzFEEyDI0ksXwAeFkI8KITwgB8APj62z8eBv6Efvxf4fSmlBD4FvFUI0dKE8x3A04c07gYNtsSLl9Uaqdvr3eaRNNg35MZiaYjF4Mj1Y5FS5kKIH0WRhA38opTyKSHETwGPSSk/DvwC8CtCiBeAZRT5IKVcEUJ8GEVOEviElPJ3b8sXadBgAozFEg36zN7msTTYJ5jAfUMsFY4csQBIKT+BcmPVt72/9jgG3jflvb+KSjlu0ODIwUq6AGTJ4DaPpMG+IU9G/zc4kq6wBg3uWriZIpY8aWIsdwuiSC8SmuB9hYZYGjQ4RLi5iq3kSf82j6TBfiGO9SKhqWWp0BBLgwaHiCBfB6BIm0rtuwVO2QTvx9EQS4MGh4iwVJZKkTausLsFttSEkjfEYtAQS4MGh4WyJJTKH1822lJ3DRypYiuycYVVaIilQYPDQtrDRkm5lFmTFXZXQEo8bbFkjXuzQkMsDRocFuK16qFsJqG7A7VMsCJrXGEGDbE0aHBISPqr1WPZuMLuDtTcX3mzWKjQEEuDBoeEQXdp+CRvgvd3BWpFkWXWxFgMGmJp0OCQEPdWhk+aKu27A7XfsWiIpUJDLA0aHBKS9SGxiLxxm9wVKBpimYSGWBo0OCRkfUUs6zJEFA2x3BVoLJaJaIilQYNDglE2XpRzWI0r7O5APcbS/KYVGmJp0OCQUEZrRNKjRwu7bCyWuwI1GRfZWCwVGmJp0OCQIOI11mlR2j5Woyt1d6AWKyub37RCQywNGhwSRNqlS4vSDnAai+WugKy5vxqLZYiGWBrsHvEa/Icfg7SRgN8OnLTLQLQpLA+nbCahuwFZMlwgyEaEskJDLA12jbXn/gge+wV6l/70dg/ljoCbrRNZHXLLH0qtN7ijkWfDQlfZuMIqNMTSYNd49eaS/n/rNo/kzoCf90icDqXt48rGYrkbMNJXp8kKq9AQS4Ndo9Q3VRE3rrDtICh7pM4MhR3gymZ1ezcgTxWZJNJpGn3V4NzuATS4c1FoN0CRNBLw20Gr7FN4M5SiqKTWG9zZKLSYaI+wIZYaGoulwa4hU0MsjcWyJbIYj4zCm0M6Pj7NJHQ3oNT3QF8GiIZYKjTE0mDXqLogNllhW8P0YvFnkXaAQwFFfnvH1GDPMIrGPVoNsdTQEEuDXcPk8Jdp4wrbCvlA9WIR4Ry4gd7Y1LLc6SjzhFIKBviIMtv6DfcIjiSxCCG+WwjxnBDiBSHEj0943RdCfFS//nkhxMWx1+8XQvSEEP/osMZ8T0LHWETTZndLROvLANiteXBCAGRDLHc8yiwmxSGVDqLpeV/hyBGLEMIGfhZ4N/Am4AeFEG8a2+2HgRUp5UPATwMfHHv9w8B/POix3uuobqSsaVq1FaKuIhanNbRYkiab7o6HzBMSXFJcrMZiqXDkiAV4B/CClPKSlDIFfh14z9g+7wF+WT/+GPBdQggBIIT4i8BLwFOHNN57F3rFbeWNxbIVTJMvr3MMy1UWSxY15+1Oh8wTUlwynIZYajiKxHIOuFx7/preNnEfKWUOrAHHhRAd4H8G/vlWHyKE+BEhxGNCiMcWFxf3ZeD3GqyKWBqLZSuYXixhZwGhLZa0SdOejs/87/D/fO/tHsXWyJTFkjTEMoKjSCx7wU8CPy2l7G21o5TyI1LKt0sp337y5MmDH9ldCEu7wuyGWLZENlBZYeHsMWxPWyxxc96m4vqTcPOZ2z2KrVEkJFJZLHYj01PhKBLLFeBC7fl5vW3iPkIIB5gDloBvAT4khHgZ+PvA/yKE+NGDHvC9CkMsTtMNcUvIaJVcWszMzA2Jpcmmm454dfdp7HkCf/hhyA7huixSUhwK4WLJxmIxOIqV918AHhZCPIgikB8Afmhsn48DfwP4HPBe4PellBL4NrODEOIngZ6U8l8fxqDvRdhaodctm5X3VpC6F8tM6GJ7LQDyxmKZjmgV8gjKAix7Z+995Y/h9/45nHgYvu77DmZ8GiKPSXHB9nAaYqlw5CwWHTP5UeBTwDPAb0gpnxJC/JQQ4i/o3X4BFVN5AfgxYENKcoODhyGWprfI1hBJl65s0fEdHF/FWPK0IZapiFXdD7tJZTfvWXxu/8YzBaJISTSx2A2xVDiKFgtSyk8Anxjb9v7a4xh43xbH+MkDGVyDCsan7DfEsiXstEtPtHFsC8dvA1A0rrCpKAcratWb9sGf2dmbTfr7IRCLVaaqhsXxcJKGWAyOnMXS4M6Bqy0Wr5GA3xKqF4siFFdbLGXSWCwTUWTDFPbdxFmMxXLrEIilSMiEi7R9HNlI9Bg0xNJg1zDS7wGNxQLA8iW4/IWJL3l5j9hWK2/XWCxNYelkRKvDx5sRi5STe6BUFsvzUJb7O7YxWGVKJjyk5WJRTtd/u/YV+PnvgGT9QMdzVNAQS4NdwzSrCmSibvJ7HZ/5P+A3//uJLwX5OpnbAcALVfBeHkbW0p0II9gJyHSTyoHnPwkfej3E3dHtxmLJI1h79QAGOIRVZhTCpbQ9tWGaEOXVL8K1L8HqwY7nqKAhlga7hrFYbCEb3Sug7F5Djk9yGmHZJ3OVxeIHiljKxmKZjHhosWTxJiv81VchXYfBaAfTeFAjowOOszhlQi48ZEUsU9zCSW/0/12Ohlga7Bo+KblUl1ASNbpXt25cIYkmTBxFTouIwpsFwPd8CikOp87iTkTNFZb2NyEWYx2MEfTiyqo6vwCLz+736EZglxm55UFFLFMC+Mal17jCGtwpSPKCsjx8V5QnM9ZQ7p14swngbkMWTySFIF0mINno10+0FeMrYgk8hwRPuWoM4jX4Dz92z6xoN4OMVqrHWbzJ+TDEMpZdV6YD1mjTc48fuMViy5TSqlks0/reG5deem/cJw2x3OEoSsm3fvAzfOzx1w71c2WR4YqCrlDunSS6N24YAD72N+F3/u7otrKgU+rYwJhbUOqYgQzmAPBsixh3dBJ65XPw2C/A5c8f2LDvFCS95epxttl1ZayDbMxaTgdE+Fxx7j9wYnE1sYitYiyNxdLgTkIvzllcT3hl+XBdUaa4r2+rVXh6L7nCVi/DjadHtw2WsVBWoxzLZIrX1QrcDucBsCxBgoeoS+GYuMJg6WDGfAchWR+eg3ySa9FgisVCHhFLj2eLc4pYDiqxREpcmVHYPtLZiliaGEuDOwjruigrSg82rXIcJqYSOWoVnt5DFsvSWpfB8qh8nezfrB6nY31WBnqitFvzw32Eh1WzWK5cvwbA9Wvjsnj3HrL+CrF0KaWg2CR4v7imJul8zF0msogIny9Gp5TrqXv1YAaqLSZpewjb19saiwUaYrnjsR7nfJv1FUR0uCvdVOtcJZ6aLPN7qGlVlsYE2cpIzUJ/5Xr1OBmMTh7xurJGvE6NWPCwahZLb3Vpw3HuVRT9FVbpMMCn3KSO5daqOs/d9dFMPCuPiPB4NtfdNg4qgK9dntLyEMZiybewWJoYS4M7Af1Bn19yP8Q3Lv72oX5uGiv3Q+EvAFsEWe8yuDJVbq+aldJfHhJCOua+SXWTr6CzUG3LLK9ShwalfgxQ9htXmIxWWZNtBgTIZJMFi7YOxi0WK4+IpM8L8qzacFBxFv35pe1jaWKZmnbfWCwN7iTE66s4osTJDndiz3STKhmqybK4h5pWebp+p+wOySRerVksY8SSDTSxzBwbbhN+JeKpDwCAdciW51GEiFdZo01f+sOV/iRoV1QxRj52ERMLnyVmlav2wCwW/fs5Q1dYMdViMcRybyzAGmK5wxH19YR0yD1RMq1zJVpqsiw3W1neZXBRE1pvaZiJl3eH1ks25hYsdJOv9uzQYsktH6dmsVg6JdmJl7nXYadrlcUiNlE3FloEtYg3EkthB5yeDbjqPXBwFouxTmwfy1XEkqeT78N0oH7fPFqb+PrdhoZY7nBkmlgOu4tjri0Up30cULUDB4KyPHC9px1BSjxNLP1bw0B72Ru2tx53zZRJj0h6zLbCalthjVosTqomHD9d4V6Hm3bp0maAjzWeSlyD0BZLObaPW0ZkVsBDpzp8rTynLJaDyAwzgXonQDjaYskm17GU2lIxBHO3oyGWOxy5vlDtQ5auNyszb/YEsDHFdt/wyR+Hf7tph4TDRZFVacXJ6jDbyBrcUq4bII9HSVYmPfoEzATDLhW55VeSOABernzvrfzeWNFuBj/vEtszRISbL5h0j3k55oZ1y5jCDnnoZIcvDk4rN2N/cdIR9gbtChOOh72FxeIUeoxNjKXBnYAiUsTiHrIrzPQSac0skEl7g6zGvn3OzWcpl186kGPvCrXgbLF2rXrsJktclqfU9nE/etZnQIDvDG+30vYrEU9QIpUA7bJ7tCy0w0aRE5QDEneWxApwiukLFmtKgaRXJhS2sliezu9TGw8izlLFWPyKWIpsQoylyHC0205sFjO6i9AQyxFAWUqur+2OGIzo4WF3cSz0yswPWkT4G6uf9wlXb9xkae3orPLqK1KrlhUWpstc5SQAcswtaGcDYhEghKi2KWIZTkJh2SOTNjbliAjjPQetUpB7c2RWC7eYvmAxMZaROEyR4ZBrYpnhqlSuWno3Jxxhj9AxMssNKmIpJ7nCata8dchJNrcLDbEcAXzqqet8+4c+w3J/dLWz1Ev467/4p7y4OP1ilNq09srDbbZlJk83aBELH3FAFouTdUezp24z0lpzLi8aTlbtfJVV7wywMZHBzgckIhzZVtpBlV1GWdCWA17VFg/9UbXeewqaVEt/jtwONyUWS7vCRq49/Vi6IQ+d6rAu9XlPdhnbGCxPLbA0JCIcH8tVzduKSVph2kpZkR3sA1qAHTU0xHIEcG0tJi1KlvujF+Uz19b57POL/OOPfWWqyKTQBVeH3cWx1CKMXtAixsfaTfLAa4/D2uaV5kHRx9uvXuJre9dTS7U/P5EOrVQTQBbRkgOy1mkS6SDHSNYpBiTWKLFIx8dDE4tepb8sFTEl3QOIB9wp0PU8Mpgnc0K8chNi0dfFyLVniMUJOdHxsAMlObTr2Ma//X748NfBz7wNfuvvwCt/XL1kZI0s18fxtrZYbsgFnDKZroB8F6EhliOAKCsA6CfFyPZ+qiq7H39lhV/5k1cmvtfWxOKTIA+x2VadWFIrwN5kZTkVH/1r8Nl/uekubdmr0nv3hBtPw0+/Ga58cU+HybXFckWeYCZfVvEQbWHYM6eI8BHjWUrFgMweIxY7wKGEIquKI687qqBvX6vvV16GoxSj2gqxyooTrXkKp60y8KZMxMZiGbn2tFtMOi2EEJw7dZwSa3fEIiXceBIu/Bm4723wzO/Ap99fvZwn6h6wvBDbVQWS5SYWy02plRfugQB+QyxHAANNIP1ktK2pef7wqQ4f+uSzXFndOHk7ubpoAxKy4hCl87NhjCUVAc5OLRYpKXo3uXlzEx2nLMYjxyfbe7roqibmPWYHZXoyucJJHAqIlsnXlUvMnT2l402j58IvIzK7NXog7Tohi4i1mm+//QAA0do+xgN+9x/C7/y9/TveAcOQrNtaQLr6nE3JOLR1j/kq4wqG595TRP7w6Rn6BLubzNevK6J663vh+38VHn6Xco1p5PoesN0AV1sschOL5Sa6jqkhlgaHgUGqLZZ03GJRzz/03rchgZ/4ra9usErcXF20IUll+RwGZBZTSoHvB2RWsPPkgSzCljkry9MrzWU9iD2tz8U2sXhTWQFXl/YWGM+0+2PJUW4r2b1Kb1llh3lzp7VbcDR475cRudMePZCjiKXMYqI1dQ7KY68DIO3uI7EMlkYmw6MOI5nvdY5RuvqcTSMWbbHUC00NsQhNSg+d6tCVIUl/F7/78ovqv/5dCOdHEisKfS04ro/nOKTSppxUea+zBG8Yi+UeyAxriOUIINIEYiwXg4G2WN5wZoZ/9K438AfPLfLHL45OxL5Ox2yREB8isYg8JsHFdWxyO8DdIbGUZmWaT7/Jkt7wJi732G1xdfkGACvdvQVPjSus3z4PwGDlKgPtugrnz5CKAGtMLyqU0XCS1BCuWlGn8YBoXU2msyfO05c+RW//ZF2yqEce3TlFeYk+F/7McfD0OZtSfW/rGEs9DiO1G9LyFLG8/lSHngzpd3dDLJfU/+OvV/+DeRUP04u7XFsnth/iORYZDnKiK2wYY9FfcudjucPQEMsRQGWxjMVYBnHM99l/TCgyvudtKh//5aXRiTEo1fNAZETJIQYFi5gUF4Dc3jzIOgkDM4EU04ll0B1OsPVsrN2g6KvPm6rltE2YPvXF7P0A9BavkKwq0uocu0/XXtTGWmR4ZEO3jobQrrAkHpDqsZ08dZplOQv7KES51l2j37tzii6z3jKxdJnpdBCe6k46bYVvXGFebVFjFCFsX53vCwshPcLdSaksvQiWC3MX1PNwHsq8Igqz2HFcH9e2SHGRk2TzqxiLIZbGYrktEEJ8txDiOSHEC0KIH5/wui+E+Kh+/fNCiIt6+58TQjwuhPiq/v9fH/bYd4MhsYxaLAvLX+Ffuf8a8fG/y3yoqraXe8MLNytKWnK4mosPsdmWyGMSoYvCnHDHWWkD7f4JiulSMNH6UN5kr8RifPdyj5ZPnqrv6Rx/EIB45QpF7yYD6bOwME9m+aPBZD0JyTGLxdJEk8UDcq1+fPrUaZaZxY73j1i8MsbfIenfThQDJUA5F7pYgTpncspEbCwWh7wK8JuGc5av3tvxXXoyxNqN+2n5RVi4CJatnusOoFVKtK5pcrwAz7FIcSbL5psYSxW8v3MsyN3C2XqXw4UQwgZ+FvhzwGvAF4QQH5dS1lv2/TCwIqV8SAjxA8AHge8HbgHfJ6W8KoR4C/Ap4NzhfoOdQ8QrfL/9GfrJQ6PbE22+f/U38M+8hY7/RpYHwwu3F+d0GE4a43LtBwmrSCqLRdohgdzZhB3p5lctOZ0Mk16t9/ke1ZOFkaXfq8Wi/erHjp+iK1sUa9eQUZclOcuJts+iFeKWw9VxmfSwAOF3Ro5je8piyeI+ZbRKJm1mZ+a4Ys9yPNk/vTBfxvikUBbDCfIIQ0ardGWb2dDF0ucsi3p4E/Z1qC3E0j6E8+S6hsjRbrRO4LBOiJ3d2Plgll8ausFAucJApUTPnafME0op8Fwfz1ausImNvsaD94cdY4lWVcvrZF199vz98PqDXXMfRYvlHcALUspLUsoU+HXgPWP7vAf4Zf34Y8B3CSGElPIJKaVJM3oKCIXQy+ojjG9Y/ywfdP8vnN5oTUdVaHfum+DT/4x3B19lpVZE2UtyOiIiE+q2yw7RYrGKhEyf2tJtEbAziyXV1kjI9Lz+bDD0i5vUzt3CSVfNgfZ0nEJbPKcWZtQKtHcDO7rFErPMhg7FWLwp1lpu48Ri6aylLI3UZEqLuZZH35knzPaJWMpCkQpMDYAfNRjJ/LnQxQ3UOUumCDc6MqcrtYtRuyhNwzknVNtbrk2fsEpy2TakhOVLXLXP8ltPvEYvyZUrDCqLRWYJCS6ea+M5Fol0JxJLEXeJpDcc62HHWD79fvi3fwX+/Q+rDMFf+ysb2znvM44isZwDLteev8ZGq6PaR0qZA2vA8bF9/lvgi1JO9tEIIX5ECPGYEOKxxcXbW5Dm6Iu+HAuyCjMZ/KWfhzNv4Z+lH0Z0h+TTjTNmiRh46qunh+i7tYuEzNLrSDfEpdhR4VedNOSUG62oZfKYbKzdwtXqwRN94DuAKYBbmJ3lFgu40U38ZIl1ex4hBLkT4teJpa9FQoNRYnF0DCBPBohYycTPBA6pt0C72KeYSD3ofYdkItmJOhdzLRcnmAGUxTIJDjlrcjTAbywWT7vRLEuQWO2dE8v6NcgGfPRFh3/w0S/z9g98mn/xB7q+yLhV85gUB9+xcDexWPK4R4+AHkYF4JCJZekFOPso/I9fUHNJmcHVJw70I48isewZQog3o9xjf3vaPlLKj0gp3y6lfPvJkycPb3ATYOs4QznW37sqtGufhL/4b+jIPvevfaF6vd/v44uMNFAKw+OqugcJq0zIhSEWEy/Y/uRVDGrxkympoGU8nGDzPcZYglxN8HKPrjDTIdALQrruccLkFmG2wsDVfWns0XhT2le/6QZi0RZLnkTY6Ro90cGxLbLgmHIr7odETm1VWtwhHT7dbI0ubTqegxtqYpnU974ssCnpMlrrYhrOucEwppXabfxysDNxT50R9kT/ON/ztvt47zed5zOv6IWTsVhy5Q72HKuKsVgTLZYeAxkw2/IZSP/wiWX1VTj5Rjj5iKrFAeUaO0AcRWK5AlyoPT+vt03cRwjhAHPAkn5+Hvgt4K9LKV888NHuA0xx4bj0vGVWnF4bFlTxnJcMaxLivpp4i5bSmNpr3/msKMmL7d18bpmQW8oVJnRqZzzY/g1jgukA/e6UOos6sUyRI98uwkKPbY8Wiwn+u0FI7J9gNltiplgl9TWxuC0C4iolNdFWqGOkRTQcXxFLmUa42TqRpYkn1Ib3YO8B/HpnxXhwZ2SGedk6sT2DZQm8tiKW8f42QPU7rkp93vS9UiYDMmkT+EG1a+Zunl02EUtq6rhUnuE733CKD/zFt/LWh9Q9aCwW8lS5whwL1xbKYik3Xl9lss6AgNMzymoZX0BuCSnhd/4+vPxHO3sfKC9C94qKqwC0jsGJRw6cWI5c8B74AvCwEOJBFIH8APBDY/t8HPgbwOeA9wK/L6WUQoh54HeBH5dS/pdDHPOe4JYRiKHul4FT9MmEi2u7YDnkwiOoNYKKdRqpNXMarkGxCz/6xx57lU/+3u9xf/QUb8yfJ2+f4Yf+yUe2fJ8tUwpbTZZCB0qTQZ+ZbX6uiIduv7g32WIRteyZYi/EUpZ0pJ5U9qrTpGM0vheQtU7h6WSKIlRWI05YSbXgeOSR1nJrjZ4ZL1BkXKQRft4lclSWmeio45S9Ray583saahKtm/U8Sb9Le9O9jwDKgrDskbrqumoFLXJpUW5CLGv6W5XJAAvVcC7CI/SGiQql24EEZSmMEfxULL9Iablclcd58IQ6i15rjhKBZYok85hEuviOhWdbpNKdaLGUSZ8+AadmfXorAfNRd2IywlR0r8Ljv6Sy0i5+607eqfTxZDkkFoAL74Bnf1cRVk1xez9x5IhFSpkLIX4UldFlA78opXxKCPFTwGNSyo8DvwD8ihDiBWAZRT4APwo8BLxfCGFEfd4lpTwAzez9gZRSBXttNrRhdfKI1Gqp3CshiN15ZrIuUVoQejapXoW686rGZTddHOf+8Kf4v6P/V73fEcSxB2xNLK7ueQFg69TOJNr+SsxOhyvoZAqx2Ok6qbTxREGxF9dQslY15xLF3oL3Mk+IpYvv2tA5o/IQATpaMt/Uq2T9MWIZndBcHWMps5ig6JHpeII7o4ilv3KTmT3mM8aD7pBY7oTOhZVkviYW32FAUHVfHIFeIJgYSxqvEwAyGxDjE7pDYpF+B3rszAW1fIlucJ5yYHHxuPqM2ZZPV7aYi1YRAEVKisOMY6n4mnAQ5caFi0h79KVqldwjpIh3+Ftc/4r6n+9icWWkjOYfGG678GfgiV9VsZcTD+/8mNvAUXSFIaX8hJTyESnl66WU/5ve9n5NKkgpYynl+6SUD0kp3yGlvKS3f0BK2ZZSfkPt78iSCkCclSozCrDHejV4xYC0pjGVBcc4JrpVynGmiaV1TIkXlruIQxxLX+OafR/83Sf4woUfpiUS8jzf8n2uTCl08N4QS7qDrDQnW+eWVBNINiXG4mbr3ELVDkxr+bodlP1altUeLRajOODZFvbsmWq7PXNava7dgsbXb2IbYXuUWExwucwGtMsemae+pz+njjNY3UV67BiyQa/2+A6o9taWQOGrc9HybKXzNckS15ZBZCs3V5Vqnw2IpEdQIxY8bS3uhFiWLnHNOcts4HCsra7zudBlTbar+KAoVFaYb6vPyoWLNcEVJlJtscz49GU4tS5nKq5pYtnN4mr1VfV/xGL5FvX/AN1hR5JY7iUM0nxILDWNKSklfjkgd4bEUoTHOS7Wq5Rjk0Xmzfqoh3kAACAASURBVGndqinSF5vBzQd07WNKD0m7tAaDrS98T6aUWu/K0ZPkToL3XrbONd2EKZtSFe3lPZZQKZ5yD66wwfow609MuPF3BF2/Y1mCQBM6gD+n4lzGLWjiTWa1HWxwhakYix2v4ZJT6lV6e14dZz+k89Pa75HvwJq8bahJ5gO0fUcFuzchlsQZi8NkERH+iCtMVNL527QUyhKWL/FSeYYHT7SrBm1zocsabXKtlCD0teC7ahrNhVMpLtchdAdRY7HsuEByTxbLqyBsmK2Zv8cfgnABXv2TnR9vm2iI5QDw8q0+T1/d3sUzSAtamljqKZFJXtIipqgRi2if4BhdljSxSO06EDN65bwLYvHKAZmtUzP90Ulx0/eRIm0VvDfEspPkgaBY55YWciynEEtQ9Ojaqqis3E2/F41oddg4S+wxeG/lCan2kHdODHNM2gvKHWlpF1eiV9Ay7ZFIl3ZrVDbf930yaePF2qDWk+nM/AlyaZGt793Qrtc15Tt1v9wOaIvFCtVv3vJsBhPaEMCwBXDuKuvGZERaeUSMN+IKEzvtydK7DnnEU/EJLp4YRqaMxWIST6wiJZUOnq2m0UJ4lTBmHXY+oC99Ts/69Ah23p74+h4slpVXYO4c2LWoh2XB+XfA5T/d+fG2iYZYDgAf+N2n+bHf+NK29o2ygpbQxFKTAuklOS2RjEiB2J0TLNQslqr+o6M7D+7iwgvLAblriEVPiv2tb0BfpkhHTZa+Tgstku0TS6vskQQnVGOsKSKJYdmvanQmypFvE5HOOsulNdEHvhOIMiUTSnHg+LHjRFKRzMxx5cKytcWSGqsv7dPHp+WNVr0HrqXcKLoLpaWL747PBKzQodwHIcqi5nKZGKc4YjATttM2xKJiLNaEBZMRgDRuM2OxiDwmkqMxFidUxCK3aynojLAvD47x4Dix0K4sK6tMSYWHZSmLprBc7AkWsZsri+XkTLBzeZloZejO2q3FUo+vGNz/LXDruQNTvm6I5QDw2krEje72LoJBWlSusFBGpLlK9x0kBW3iEVVcf/YksyJidV3fRIZYWscpsBC7WNUHckDpGGLRlc5buU3KEk/koC0WL9yhxSIlbdnHCueVayCdVKdQ0mJA6mti2UPFfKon6SVmJ974O4FVJKS6fufUbMBNOc+y7HB8Vp0DY70ZN5Rxg/jO6K3m2RYxHkGsXF52WxHLQstjWc5iRftALDVXmNwB6d8uGMl8t6NSt21LEBGOuIgNqu6NQYdM2pVKhV1EJMKvJnsAt6XIJ+1vk1i0XL5xhRnMhg5d2cZKdDZmkVLoRQZAIdxKv6w2UGyZ0ZOhirEQVgXR28L1r+oHYmtiefYT8PynRretvjIaXzEwcZbXHtv+WHaAhlgOANe7MSuDbFs1IYM0r1xhLREPm36lOS3ioXQ4w8BuvKpWuXbWo8ACt0Wyi/bAUkpaMqb0zKSoC9K2iLGYm9r0FPF1/GC8Dmcq0h42JSKco0cLa9JKMl3HQiLDYxRyGzfVZuPVasGLcn6iD3wnsIqkmkwWWh6LLLAk5ziuA7yOb0hWnUMrGxCJsPLTGwghSPBo6/bGnl6lB67NmpjFife+kjS/x5psHV5R3tKLkxMk/tM/hec+uelbjWR+MHOs2pba4WgjLw3jCvP8gAiv+q52EZNZoypOvk6cyLZby7N8idJyucbxKiMMhhaLk3VBSuwyJa8RS2lNIBbtxsvskJnAYV2GanGz3UJdQyyn31w115uK/++fwX/6X4fP80QpCEyyWM5+o4q9XD6YOEtDLPuMKC1YHaiLa7m/9cUTpQWhdoW1iavmXv0kpyXiyooAsNoqFTXrqVWuk/WIrDYIQSp87B1OvkmmrCL0Z7hjq+2p7zMV/lr63Q/V+7eb7lwFP4N5BqKNnW2c9IrB0C2S4sIeXGGyv0JPBqRWuGdXmF2mlTabZQn+g/fdfFT8+SoLydXnwvj8naxHLMKJx8qES6fUCRidoSJR357DT/euF1amqlhwVXYmxin2HdEq/NyfgS//u9HtUsLnfx6e+JVN3571lkmkQ6c9THRIrXDERWxgXGFBEBDhV9eeU8RkVjCybzsMGEh/+8Sy9CKrwXlKrIkxFrvMdKO6tCoSBigtD0eOZVQaRQC3Rdt3VJYbbL9Y89pXVFr73AXYbOGYp4rU6+6ttdfU/0kWi9dS7ZYPKM7SEMs+43rNBXartzWx1F1hbRFXzb36aUGbZFS8sKUmn7KnVrlu0SfRgfd0XK59G+j11rCERPi6hkJPisUWlcFGwt70FGm12pRSVKuzrTBYUxe+3ZontlsTdZwGXTWxuq15ElzYS/1JvMIqHXB8nD26wuwypahNJk/Mv4tPd/5C9dykEZv4hlNEpNZkYklr+qitueEqPfYWaOX7UCmf9onwVZziMEQou1dUttbyS6Pb41W1ffHZTd9e9Ffo0mauNSwfzO1QFRCPwbQvCANFGiZxxS1j8jFimQmcndWPLF/iun2O422PuXBokcwEblWQSbyqr4XhWEvLxZFj15fp3eK0cW2L2DJClNscy/WvwJm3qkXcZhbL8osgdT+nK4+r/ysvq/+TiAVUPcuVx/deNDwBDbHsM66tDW+CW72tJ8MoyQm1Am3dYhnECS2RjGpMaYuFgSKWIO+RamLZTXtgU7kvfOUqMCmxEyU0aqh6XuguiL5rE+HBJKHIsoRP/E/DXHyGTb6c9gKJ3cGb0EXSyOrbrTlSvD0VNtrJGl06YLtY4yvKPIGf/3Z46Q+3dSynTMlrk8k7HjzG2x8YkoLf0uRsNKvGapHqMJYPQGt2aLGk/gLtsrszbauJHzBggE9shdg7FWHcDda1SGNvLKNNW9gsX9p0cpTRCmtaMt8gt1uqn8xYS+4yNxZLSERQFRe7ZUxuj1ksnnJBye0QS1nA0ou8UJ4esVZAxXyMKgDRKo7MKKyaK8z2sCnVMQz0AqPQsdLSyMtsJ5kii2DxOWVZOOHm7uCbzwwfm/oUE/RfmOAKA3jDu+Htf2tX2aRb4chV3t/puL42/PGX+ltPhkncwxLqpmkTc0VbLLHOzDJxDwBailicSHdfLAfk+kLNrQBnh66wwboiFltndfm6OrzcItCbaYvF0j1FhBDq5p4QZGX9KvzpR8jdGZz73qa+myYWr71A6nQI4ksb3hbpXixee4FMOIg9BO+ddI2u1UGKCSvKwTJc+zJc+xI8+G1bH0umlDVi+YnvedPI6562+ozP3ysjcm+aK0xZLD0ZMNce7lMGx7DXSrXSbx2b+N7twMoGRPikVgt7D+na20ZPF3X2x4jFPJclLH1NrcAnIVljjTanasRSuC2cqFAWjzO08EyMpRWGRHjMZgOQEk8mFM7o+e5oi2VmO5P5ystQJHwpPsODFzeK4BTeHKRAvKqvheGYpCGZIgVjpWqXl+mGWXgdiNlezOvmM8oKOfM2lR22Wdbn4nMgrFEdsNVXwHL5o+su//6TX+K+uYCz8yGP3j/Pm8/Oweu+Q/0dABqLZZ9xrUYst9a3drtktUyqtoirLpLZJCmQcIESCz9dJskL2gwoDLHYwY67OCZGEkanY/ptE4TffAWTmQwcd3gDJ2Jy8kB/UUlKvHTpuWpbqkkjmDlG7nYIyo2fl2mZl6AzT4qHVe6eWIJsjciZpbS8qp2tgUlEWFzaXrDckaPuj3G0whaFFEh9XL+MRopcRz5bT0prtJkNa2s8vYCgf2vCu7YPK4+IRUBqh3jFwVss2do1ANaXro6+ULdg6ivrMdQl8w1MxuJ4kaRp2BbqGIuVDyBPsJCUYxbLjO/SlwHWpOzDcWh33eOD0yMZYdXn+qbZ1wquzEYWGdj6cb1WyozbG7NYthNjMfUr972Nr95IqrbYU8e9cBEe/Ha48kUocmWxzJ3nV/70Mr/z5at85LOX+Ke//STv+zefq7JPDwoNsewzbnRjZgMHz7G4tQ2LxaSEFv48beKqTbGRCjfS4QBYFrEzRytfpRup7pFSy1UUdjjS+3s7yLR+lCGvls6e2Sq7yzTdsvxRYpm0Kl65pvzt9vpQoDrXEiutueMU7gwtBhtcHaZfSzh7nEx4E8X9touw6JI4cxN94Gvr6jxfW9xeeq8rMwp7eu+40HeI8CsVhEBGFM7GCQqoCGqd1kjdhaWFKJOaYsBuYOcDUhGQ2S28Q2hP3F/Sv/GYK6xY3x6xuGmXdVp0vBrJekYWf3QiNhI/jueTiAC7iIcKx1Mslm3Vj2hieUGeG8kIMxCm2Vdf/TalXYuxmOsi30gsVRKOb+RltuGWu/YV8GZYdO7jP7/U2zzGsvicksY//w51rm4+rWtY7ufV5Yhvf+Qkz33g3XzovW9jkBY8c+1gC2YbYtlnXFuLuW8u5ETbY2kbwXtTVCjbp2iJhEGibhgjwWEC6gaJv8A861xdjZgRUXWhlk6Iv8P2wMYqMumYtquqwbfqNjhs/zq8gVMRTCSW+JayWDrxUPuq0AVmnbkF8GeVGvD4ilQTS2t2gdzag8UiJe1yndyfm5i1Y9x62/UzuzJFbmKxBI5NhK98/mVBQLqh372BIai+mBlJRzbyMP3l69sa0zTYRURqBeSO7kdywMi1xRJmyyMLhXj1Orm0eIlzmwbwvXydSEvmG0jPrPBHx28sFsfxSa0Ap4iGriJ31EJs+zbrhDjZdojlOQbhWfqEEy0Wu6XbC6+r61nWFxl2zRVmoMnMxEot49rejlvu+lfhzFt56to6iXSxpjXTKzIlKHnyjUq5GJQ7bOUV5Pz9vLY84MJCiG0Jvu1htWh54tX9a389CQ2x7DOur8WcmQs43vG3Fbw3QV5TPZ8Yjal41Ddb7R8c47jo8sryQPW713IVilgS5NjKf9PP1hXvfnuu2haJYIPK8ob3ad0up2axZPbk5IF8RaU8zueL1WQjo1XWZchsK6zGP56xU8ZrRNJjtt0mF5MrmreFbIBHTunPI213A7GYPi9bfWcDjxTpTLdYLEsQ46tiVU2Wcuw3NDDZZbE9+nqgiSVa25usi1tEZFZI6bRU87CDho6xODJXMQGNZO06y8zyVHGe4sbTk99blgRFj8SdHdlseZu7wmzPJ7MClZJcEcuoK8x3bCLRwi22MZnffIbF4CIAF09sdGF65l7pGWIZLjIqkqknmmhiMYRib1depizgxpNw39t46mqX2AjtT4qjLl9SXSFPvlFlgHXOwEufhf5N4s551pOcC8fUd7lvLuTMbMATlycLv+4XGmLZZyiLJeBEZ3sWi1E6tWbUZGKKE6Ux28cmJdk6zjHWuXJrjVCk2IG60KUTEoqUZAe+U9NwKOwMiSUmUP7qTVDo+IHj1ZSXzc09Bku7wDyyKmYgkjXWadHybCx9o5n0YgORaLdI4JBb/u6JxTRlCueRlovL6IrPtDze6jsDICUu2egqdQJioc7h8DecbLGYWEDijE6mMwuqEDbdoxClypAKKdw2Lvmoi+aJX4Pf/JE9HX/D5w1ukkrt0usPx16u32RJzvK18jzW6iuT+62n61iU5N7cyGbjQirHJmKjxOC4vk5JHrrCxi0WUF0kvby/weU6grKAW8/zkjjP6Vmflrcxt2m2Haje9SZRoXYtiCrGMrzGzP1t1Ckc49reyi13/avq+5z7Jp68sjYklknuMGMFnnyD6q9y4ZurCvxbttLjO78wPCeP3j/PE682xHLHIM1LbvWSHVksxhdvddRkYlbulQTH2KRktU9yTHS5saiLJE1w3w0JSInSgu3C3Kytzny1LbEmu7TqML1R3GBoseR2iDthVRxGyg0CEC8pt5iTdOkLpRprxm+ywKrvqYnFtS0Ky9t1/Umi05at9jGk7eEwarEUyQ6Ipcyxkcix4PA4TLFqoiVErGCyxWLUoVNvlFjm52boyYCit0Xw/vpXR7psjsMvY3InHC5O6pPZi78HT/7m3lOaawiTW7wgdXOyWpxF9BdZlHM8L88jkKqIbxx6AVCOnQtLu4KzMZmhusVS2CGeHBKL8DYSS+Z0sCk2T9ldfRXymKfzsxPdYACzgcOabFOa1Oqa9SocY1UM7/si6TGQPqGv9uuEPv3ttCe+9Bn1/8Hv4MmrNWKZdG8uPgcIlREGSq5FW02vSdUn6MKx4b366P3zvLo82Nb8tFs0xLKPuLmuLtozswEnOj5LvXRr15RZvelGUZVwYDaZWNzZE8zTY3lpUb+sV3heSEhClG7dS6WC/iy7VoSZimCihEYdpXYfeX5N0n9K8sBsep2npcqjX72uAvlO1mWgW/E62mcdjxGLk64rVQFMRfPuiKW/pgm4fQxsD59sZNVq3HqTrK0N0JOScDfv/2d8/pEmlvr5rcNYPuOT6fG2x4qc2bo98S/9N/Br75ta4ObLmMJuVcoKI8TSX1Tuk/7erKIKyTpeGfFUqWsmainHbnyLW8zxsqUL9W5OiLNogjSS+dV7K/260RW+1HEM1/UpnBY2ZRWXsyZYiLlj6kc2mdAXFeF9fv3EVGIxsi5Sx1hGicW4woa/Rx716OPT8ZX10/ac7bUnvvQHcOpNrNnHuLwckchNLJabz6haFUOoRgcMeCFT9VHGFQbw6P3qnvvSAVotDbHsI0wNy8P5c3x98jhpUdKNN5/oK6kNbbFIfcFZ6WRiCeZOYQuJ1MVPvhbYE24LR5REyfZ96Va2riQmrOFlUAVCN4GsLJbhxWpiPCPIYubKNZ5xvg6A/k1lsfj5OrHuo+Fp8cV0rNmXmw/7wBd7IJZoTWtxdU4M/eHl8Dcp9I068TuvvDIyEeUm0O9sbrHkuljVdG20p1gs5jilP+r+mQ1cVpjBijZJgc5TlVl0+fPw+x/Y+LqU+CRIN6zcSVmti2TVSKx7ZeN7dwM90ZpFRLp6vRpHmC6xKuYIzzxMhguLEzLDtGS+aI0Si3EdbWhUlqeUUuA6LqV2fWXaSrL9jRZL6W2HWNS4nohO8/CpyU22Z7WsizDE6dQWGeZxLcZSxOsMZFCpW7d9m54Myae0ilBfJIJXPgeveydPXVX7JUaTbJrFcvKNw+f3fb1KfbY9nu+HzIUus8EwhfstZ+dwLMETlw8ugN8Qyz7C1LC88Zl/xbe+8C8BWNrC3KwUidsqxmJSfa3KYhmdlLxZRUAzg8sA+NqNZenVSrKNJl0GVtYnGtOxyuwWXrE5OUk9GXtjxBIw+r5yTU1a6cm3EkuXbEWNOSh6pJpYAh3fyfujN5raR/vXbR93XNxvmzCusHDueOUDrysll9kmFssvvRv+8MPVUxOPEVsQS2Yrfat0MCFlvA4dZK5SWDUsS9C15vCSTW58fX3kwTH4Lz8DX/v02OuRqulwW1g6czCuEUuu4zdyv4ilp4jka/I8ubSIVlSGGGkPt0yIvOO8/vQ8L3PfRItF6mC/rXuxGHihUYMYi7EUGRkOrmtX7RvMdzIdTUf2306a7+JzJK0zdGnzyOnJv5mxWCxNHvVrwdIWqCneBOVu7hPS1haLkZfZ1GK5/HlFTq97J09qYjmxoK+RcYulyFXR6ck3DLc5Ptz3DSrVeCXh/mOjRBt6Nl933+yBxlm2TSxCiMm2YYMKxmIJopuEiVopb6UXVsUztCvMNAFyioFSTnXG3C5aL+x+oVZMnl7hmZsp20GzLSfrE4vRi65wQjy5hVtIu4SCsPZet0VANuKzX9Our2NnX8c1eQxLT2Ktsj/saz6jJpLxFVxQ9MhcXbBp+7i7tFgyLZnfmjtZFbDV2xwbYvHG40NSItevIXvDlN80NsSyefC+0MWqWbQFsegJ0RpbpQNE7jxBNp1YTFD4Z7K/RH7iTfBbfxu6tcJEE29wW9Xnm5gPZUmrUOc7vnV50++ybeiYgz13H7eYI+tqi0hbEVl4kkdOz/BMcY5iQi2LsVjdziixmKLdcWIhV/3mXVtUwfpCq1ib1gV1iIpYNrNYnuWWzgh75PRkK9MIUVbHrbvCXHN9Da8lmah+PIZY2r5DXwabE8uLnwHLgQf+LE9e6XJuPmSmo8c/brGsvKzSm0++kX/w0S/xjz/2ZbX93R+E7/0ZLq8MRuIrBo/eP8+XL69SlNvPIt0JdmKxfE0I8T8IIRoZmCm4tharTKf+dZxsHZ90S4ulEo5sa2LJjErrYLJ4odYLu1/oG1dnVRnzfytl4jqcYkAypmNV2CH+VoWWeUwiHTxneCmYWo2ylhba1a6v+TMPcss+iT+4BmVBh/6wFe+MmlTLsSB0Sw7Ideppafsqq2wXKAcrpNJmbna28oentTbHhlg2fOcsQsiSl68MiSUbE9+chkLXFJnJ0GtNJhbLVeNx2wsbXku8edrFdHdJqknrlSjkJ/1/pCzdP/rp4fdKhhlpjg6Ap8ZiiVdx0Jp0S69u+l22C1N1f+LM/dySc5QmeK9jOLJ1kodOd3i+PI+99uqGOg4jme/XVJ4BWkFAIp2NMkNlSoaNZ1tVO2ipsw7dCRaLtVWab1nC4vO8bF1gLnQ5OTN58VA1+zLHrVss+vrKs9F044EMaGtXWMd3pvcgMrj0B6rQ0e/w5JU13nx2dnjNjVssxq148o189vlFPv30DRXXPfeNlA98K6+tRFxY2OgafPT+efppwdduHkw7hZ0Qy7uAdwPPCiF+4EBGc4fjRjfmwoyF0Gb9SbG2ZeaFW0RKM0qvqGzt4vCKAdkk8cIxi8W8z/QBqbej3Qpe0a9ELA1Kt7XBpTUOkcekuCNFfUIXS8a1IKspjjx230W63mk6yY3KFWGCtDMtn3UZQr2OJU/wSYfuC0MsO6jRqRCtsEaH+bZfucLyZPibmM6UG76zthzrcYlME5K9BbGUtoo3Ffpc1OuE6ih8RSj27H0bXsv844QymlptbbTkFhYW+NUXA1aCC7A2dGuZYLfltyvJHkNG9aSATNcZ7RXRyjUS6XLx3DluyTnsgU4KMHGP2VM8cnqGr5msscXRzLCst0whBe2Z0XMVejYDgo3EUqTKFWZbCL2oEoZYJlgsdmiIZcrCq/saZH2eTM/wyOnOhv45BrOhS7dmsRi9PFAFxjCsjQK1UOwTVKnLhljEtHEY7brXvZP1OOPSrT5vOTc3JLBxi0WnGt8KH2Cpn7IyyHhlSS1OF3sJaV5y/tgEYrmgrr2Dcodtm1iklE9KKb8P+FvA3xNCfFEI8a4DGdUdimtrEW/sDDOqFLFMd+FIKXHLiMwepoQ6eZ+ylEq80J5gsVTEoi0WQyym73yy/QprvxyQO+PE0ibcophO5AmJGHXRmVVjXGtrXK6+xi05y30nFohb9zFfLJFoP7gI1QQSura+0WorJ5Ny7ZsaHR8LuSt5byteZY0Obc9GuBNWlNqt55GPHL/SUatpbJngvfA2JxbpthQx6k6DYWt24n63zr6Tv5z8JO7JhzYeI9Tik1Myw4xb6zvf+iDf9cZTPNf16JuAPMNCW8tvV8oKRs1BaiuikALRHdP12iWy1assMsfXnZ3jFnN4kZrkjeXizZ3m7FzAq47OGhsL4OeVZP6opdD2dA+T8boPE2OxrSq+KHTXTT/cOJGa9sRTg+Y67vO57kkenhJfgQkWizscr60tlrJ2fVlZj74MhllhvkNPhtjTVABe+iwg4XXv5Jlr6vd6y7nZYQr1uBjr4nMwdz/PrwwXXSYo/+qymgsuLGycRx443mKh5R5YBf6Og/dSys9KKf8r4APAvxFC/J4Q4pv3f2h3Hq6vxbw+HE6QF4PephZLkpcEJIpALJtM+DjFgChTvViKSVIgjk9stZkVEaXuHgn1PiDbt1jCMtpALLgtApGRZ9MncVEkpIwSi6VXjUnNYnJ6V7nBceZCl3LmLDYl0dWnALB1wFoIwUC0sGuuAdOi1pCP0EFRuQuFXiddo6clUywdr8rqPvD6jVpz45mWAl5Nbt4IVjpbWCxSi3Pa0RKJdGi3Jqsbe57LF+UjIz0/DKy2bsk8mFzLkkZDN9u/fN/X07Vm6K8MU3xTncRhe+3KYjJ9duI1RUAvyrPKPbkPkOs3uCnnOTsf0rUXKlkXE8RvzZ9GCEFw6vXq2hmLs5TRqpbMH/W0t3xb9VsZq7wXRUYqVYzFpHNb0TKR9Aj9jefTpOWn05p96ZX/E/EZHjk1JYsPcG2LyB4Sj1NbZBiSqcdYnFxbLP7QFdYnwJ7QKgJQbjBvBs59I09eUWN9y9k5LCOfNC5EufgcnHwDX7uhY7OWqNKILxtimWCxCCF49P6F22+xCCFOCyHeLYT4CSHEvwc+DJwFZoGPCSF+TQixe43vOxxFKbmxnvCAN3SdPOD1Nq2+j9KCFkOZ79Rp4ReDqntkMUUVN/aUGRtZLVVpy1CuvdiBxRLKCDmWzmwsj8F4emcNVhFXku8GVfJArZCtFV9n1T2FEAJ74YI67mXVatWpxRUGoj2i49RfHzYCA6rsqSzZeUGXl60x0BOBKWAr6inZI8Vsw8kr0VlqdY0t4+Kwp8jgGwi9IHCiWwwIqsDtON7x4HH+wtef5eEJgWJnVuuFrUyuM8m0HI8bznCs7ZH7xwjz4SRhYm1OMEO71aGUAqmtwsGKIpYn5YN00sWdF0km6/A7fx/6Q2vK7t/gplzg5IxP4p/AkRnEq6RrN1iWHY7Pqu/40Ok5LnN62ITKIF6lS2sDybY8h4HRXqtBlCm5cFSRrU7ndpNlIrwRQU+DMGyTSZt8KrE8RxqcZI3O1Iwwg6KWHm7VFhm2p4mlpnDg5AN1DdRcYao9cbbR+gBVGPngt4Ht8uTVNU7O+JyaDYbEMl7gOViGzimev7HOXOjy9osLlVzL5WVFQufmJ1+vj16Y52s3e6xFt7fR1xXgQ8BDwO8B7wNmpZTfrLe9DPzmfgxKCPHdQojnhBAvCCF+fMLrvhDio/r1zwshLtZe+yd6+3NCiD+/H+PZDm71EopSctYa3tzn3PVNe7IMsoKQlFITSG6r+MbKIKMz1u++jkz75hNr+LofmuD58Ab8F//xWX77icnppHlR0iYaV0w01QAAIABJREFUynhrVC6t3mbEkpCJ0QnABEzTWlbaXHaTfqAkJcITygWSX1XE4s8MiSW227i1FVy8brpHmhodHXTfAWkaBPk6sauOMym4Wm8gFtXiKcbVFNaIpap58Te3WNCTgKuJxXcm32bn5kP+zx98tGprXIc/q5I0BqvXN7wGw2ZsRplaNQdbr5pMDTPS2rQCtUo2ag6J1iB7unxAEcBWhZjjeOmz8PgvwdO/XW0KkkUWmed42yMPtex/b5Fi/Sa35BwnOorUHzk9w9VirhKsNKgk88eIJXRtBjLAHlNGEEVGrttJuZpYvLxHhD+RWGZClx4h+WBKuvHis9wKHwTY1BUGQxctgD0SY9F1Sbq7JXmKLTMGBASuugZUe2I90Y/HWbpXFeE++O0APHWly1vPzenPCfWxxyyWtAdem+dvrPPI6Q6P3r/A01e7xFnB5ZUBp2f9idcXwF/+pvN8/Ef/bJVYsJ/YCbHMSSnfKqX8m1LKn5NSfkFKlQMqpcyklD8BfMNeBySEsIGfRSUKvAn4QSHEm8Z2+2FgRUr5EPDTwAf1e98E/ADwZuC7gZ/TxztwmFTjk3JFpbWGC5y2No+xRGlOKJKqwCt32nSIWFxPaG1CLGWo3CSmzgOGWkR1Yvm1z7/CJ5+cPDH1o4RAZMOqbA1TzBdPuwEBp0zIrFGLxawaqxVh3KUj++RtFZieP3MRgNaKcjkEM8Psn8zp4NcEAhNNLH5HGcCGEExL5J2gXXar1GZbp4OOEEttBZjWaoCMqylk2L3QaKRtZbFYmmSDdEk1QJsSCN4MrXllscRrky0W027B1xlnRXBcxaG0NIp53Q1nanEKXfuyvkhXtui1lBW54yJJo0316p+o/3lCmHcZeMexLIHUgqr0biD6mlh0ltVDpzvcZJ6iO3pd2mmXNdrMBKPEYltiYgdMUWYqHZ9RBfBYeoQTJsqO79KTIeW0LpJLX+MV6xzzLbciwWmQwXBR5NaIxdULICM3U9Ua2a3qGvCcTdoTG5mY+QcYpDkvLPZ481l17bra1VzUddakhLSH9Do8f6PHI6dn+IYL8+Sl5Kmra1xeHkzMCDM4Nx/ytvPzOPb+lzNueUQhxI8JIR6SUvaFEG8WQrxfCPEPhRBvnrD7d+7DmN4BvCClvKSJ69eB94zt8x7gl/XjjwHfJdQv9x7g16WUiZTyJeAFfbwDhymOnC9uwcwZ6JzhJJtnhQ3SQhGI9smXbpsWMTfXY9oi3qBsbCB0ynFei8EY94upX+glOd+YPs5Md4IuE9DXjbRM8ZyBmRTTaHrasl0m5GOuMHnyDUTSo/3aZ9UwdDEk82ryOn3yFF0ZcixRWUjt2aHXNHM6hOVw4sh0v5ZAF3+KyhW2Q2IpMloyovB0EalZUdaDqzWJ86Tm/su1q8lCDvuWG/mXLYL3tl4QtPNV4in97rfC7PwpCinIpvRkMTpvVWJAazTYX2jL0W/NVKt+od2Nsr/Ikpxh5pSWWNkpsdwcIxYtyJgGilDsGVXES/8mTqTkXE501PXyyOkZFuU8zuDmSJafn3WJrQ62tZGEUyvEGYuvWeXQYjGLKoCIySv0tq+SROSkdGMpIe5yJfZ55NTMlguBet2ROyErzLRNNhbJeKO3KnY6PhajCB0u8PlLyxSl5Jsvqt81cG1i6Q7VH0DVr5Q5PemzFmU8cnqGRy+osT3x6qpKNZ4QXzkMbIeq/glwRQjxMPCfgDeiCORPhBC/LMQwPUhK+cQ+jOkcUK/aek1vm7iPlDIH1oDj23wvAEKIHxFCPCaEeGxxce96Sdd1r/tWegtm7oPOSeblCutxTpxNFoYc6BiLIQXpdeiIuLJYRDDZJHdmtK6YW3vdHQ3uXV+L+ZD783zP6r+beAwTnLbHivdMdlm2ST2MUyYU9uiq7uLZ0/yBfJSFl/8jlAWrusGXf0wRy6kZn2vyBBYlpRSqF4tG7s0QylosQ2futOaUVWPp75bvQK4GqFbvUldzO44JrtYsllqfl7QWHypqj6VJkdbWjTtBMqQOY/V1yvXJtUjbwMJMwCod5BQhSpn0SaRLq6UmNtssNnR/+UKTYdDqYFmCgQgrNQdrsMQys5w69zoA4qWdFUkWN3Tgfe1VWHutknMptXpEsKDcn1n3BkGyxDLzzAaKBM7OBaxZx7B1DEZ9GUlQrBM7k7PnUivEG9Ovs2RGoS2W0PdJpDr+tBjLTOCwTji5jqVIAcnVPhPjXeMwdUeFFLg13TjPc8mlNUwI0b/BeKM3U5+1wWIx5yNc4D8/v4jvWLzjwRqx4I1aLJq4bibqPDx8usOp2YBz8yFfeHmZa2tHm1gcKWUE/FXgL0spf0hK+b3AA8BJ4J8e5AAPClLKj0gp3y6lfPvJkyf3fLxr3RjPtnAHN7TFcpqZXAWhl/uT3WFRWhCKpMrDx9MWSzeiLZLKvTSOULtJTAolUBGLMMSy0uUka4T55GBl3DfEMnozu1PUZOtQ7XlHLZa50OXSqT9HJ19GvvxHrOviyNnTym/t2BZLjjrP64TMhrVe4d4MAWmV7ltGq5S1moZhfcDOYiy5Di4LLXRpeSYddPh7WEVaTUpZzUqry7SbwkJj6bj+5mTh1IgnsXZ3Yx/TQpRiml5YqsQNTVDY1TEZkxkm0x6FFAR6NZ9YIY52J3nJMqtijjNnL5BKm8GtHRRJamn5PymV/huv/kllsVizilBm5k+RS4v45kv45YDIO1ZZAUIIpLFojJBjHuPIjMybTCy5E27ogGmVGYWlvnvLU107AWJ8VY0/BuMKm9ieWC8Y1jJ7y8A9wGwroCtDUlz8Gom5tiDF3UAs4wkyqbagGYz9tjWL5bNfW+RbXne8sr4C1ybBHY2x6BTsKwM1jb9Bj/0b7p/nD55bpJSTU40PA9shlqva7fVOKeXnzUYp5TLw3wF/bZ/HdAW4UHt+Xm+buI9WApgDlrb53gPB9bWY03M+Yv26tlhOE6TqwpnmDjMWi1FjFV6HtohZ7apJ35kiBRLOqRvz7OlTw42WrYoWtctg9eZrWEJ1T5wEk3bpjRGLFxgJjekWi1emwzasNdz3ze9hIH2W/vQ3SJdfoZCCY/fdX73e89W4u7RH3BXS1y2RjcJt1KVXIx8TIN2pxTLQApS2dhMZgipqcRW7TFhFx4dqiQd1l8lgXVs+Rv5li+B93ec/sRZpG2h5Dqti5v9n783DJSnrs//PU0tX9XJO99lmH2BYhp0ZYGQVEBFBBQQNDJrkBxJcopHkRRP0p1FUNBjUuMSXqFHBNzoGSXCJvIosioogMzhsAg7LAAPDbGfO0qeXqq563j/qqe7q7ewr1H1dc83p6urup7ur667vdt/opdbEIpyg2yi8Om8yB1P3h4N50XRS0ulnxMixNJdih+ye2JDkwHPofpmfeCdSEkl47nd4Q0EhPtEVJAd6O212k4UdQWt5JVk/TS/TilhCT5OwLpRoM0gayuJHUmeaH4lYEhoFRSyOsFqmskJ74pbzI+p7LZMYV8SSTZoMkcYhsCAPkTA0HAxkWGMJZ28axgb8sEbTeNGgiGVbKcHTu0Y4Vbk9AliGRkkmqgKw0ed/blijJ52gR6Ubj16Zq/oyzeeI5Z+AB4CsEOKDov5b8wlO6tOJ+4GDhBCrVJrtYuDHDfv8GLhE/f1nwJ0y0Kf/MXCx6hpbBRwE/H6a19eE+57ew2+f3M0BnSIIbzuWQLoPo1IgRalty3HBqZCkHLEtzZCmRH5I6Sa1SYWFsi6ioT4S+M4HV/XhVWhWDrdMxbkqYjEbhvdq2kzt52FM6eBpzSfXM45cxZ3+MSSf/Cli4Dl20sWyrtoay6llAIyI+h9v6K7nqDVp5UGGlBEYgK6Gw7zRPL9bYCRUNu4IiMUwmyMW3XcYkKoAHu3SifwdRndhKswaIxVmRiLNpjmhCSCv57Cc1sSiVUYoYldtfDMqiq2ag7mFoENKfYaOngrSSVKS8gYpJ7pZkrXZTne9xthYUBPzj/n7sJmD4bl7Kfa/iCcFme4gYunNWOyWWaz+IGXmpxbVPUUY2VSJRaWApN06YvGNVFDril4QSBe/SiwGRRl8t26L4xIgpQZxzVbzI4pwy5jjiliyycCTpUyinlh0DbeOWNRvqKFBpmoN0BSxDICZ5u6ng4ua01bXMilhKkxGfwPq+Z8eom7dR+9TqwHNW2KRUv4HQcrrVcBTwENCiK8JIT4L/Aq4dToXpGomfwP8HHgMuElK+agQ4pNCiPPUbt8EeoQQTwJXAh9Sj30UuAn4I/Az4H1SyvE7X00QrufzuZ8/wdu+cS8Zy+Ajp6krERWxAPSJAXa1iVhK5TKWqFRnQHS7gzQliuFwXhuNKVLqSsaq/yGWhY2mlIlDS+CsyDPUok89THXZqfrrgrDLqElCIwKzjT1vNmXy9OIzSVf2su/uX7FT9NZ16MhscEVbbLDi1dQgZOgiqbnDVSMwqLX3es7EiCX0eLFDYkmEg5a170P3HfKaes+RITwtcmUbEh4Vh7I0scZoz7RStffnt3AzHPf6zVzbVKbmFoKIQSGbzVKQFp6qsQi3QAGr2upc0VMk/EJVJ6xid7Oow2a77JnYkKSamH9SLue3zoHIHY9S2fkE/XTS1xm815BYEuXgxKll6onFygWdglWzrFAnzm4W44RAyQCoc57UZQVPC4glZerVVFilDbFomqCspesGXqtww/mkVLXJYDSE0vkOBpZROxYShkYZs9bC3mBLXN0v1UGZROuIJZnj7j/tYmnW5sDIoKZlaopYIhGLiqq3DNSLZh6+LIupC0xdsKRzjNb4GcK4+syklENSyoqU8hbgTcCzBJHKN4F3T/eipJS3SilXSykPkFJ+Wm37mJTyx+rvkpTyQinlgVLK46SUT0ce+2n1uIOllP93utcWwvV81n/td/zrXU/y1mNW8NMrTuGgpDoZdSypetj3Mtg2YglnPsLUiZHswBQeflWltV3EolILDRGLq9kYoeT9cHCy6BRFBvLNtYmw7TLZoM1kqwhGOu2JxcKp2uo2Yvm688hLG9svMGgurrvPVEOSZaOhYUCRW1FN3GdLL5LXaqQZzgd4jT38Y8BVxJJSHWhhB0+UWAzpUFBFY1mOEstIoGFGjViEV6KMSWKM9kwrErH4U4hYnEQXGW+wpUaa4RUoRxoDutMJ+ulAqq4wzS1SjrQ6V4w0tixWhxr9ZC8JQ2PQ7CNd3tnyNVrB3/k4O2QXaw7al43yYASSzPN3sVPmWKROYj2ZRJAKUzBz9cdBNtdNUSYoh9L6KhWmp5rFOIFa270bJRYXX9VYAj0xRSyjuHu6RhpTlgOp+ShUxNKTG1/yJZs06aeTEWnXRSymruFKvWpCFhJLo4x/xtKD9GuhQU6luBdp5/jtU7s59aC+upReGLHUDUiq3+hux6ybvbHNQBZ/WS7ZsstuNjAZSZfnpJSfkVK+R0r5BSnlxKfWXgYwdY0zDl3Mv779aK67cE0wXR1egXUsrRLLCnO4bY0lTDeFApKhvHnaUQNrbeZYyO4DJ/8tHPyGus1RYrEKtTmB/EBzZ1GofpvsqL9KTKbH8OSWEku6dc55UZxx1L7c4R8LQDG5pO6+dJ8ygTIar+BCs69BeGETy8pPc599cvX+sFguW00qj4KKchRMZ8OIJSSWGtGbvoM0kpSkWScbYlRG2CFV949qPaZSxsGopp/aIZmuEUtj4XYi8JI9gQpxC/th0yvgRERKcymTvTJTNQfTvQLlyNW7b6awZQmp0k+66iwsJZcElgSNaZk2cLY/xp/85Zx71DKeNA/GQ8eoFAJiUbMqlqEzqNdIIpWrPw4WdSbZJbM4akjSHQleu1Eyv/Zm1fuMEIshK9VUmGVo1YjFG4VYqmnJxgK+Oq76usZPLJ+vXMhV7rvqLjKCGouJCIll4HlcqUODx0zGMtgrMy0ilgGGtQ6GSxVOXV3fUGQbOmVp1s1dhb/RPDYHL6n/TX3o7EP4/9946Ljez0wgNvqaAt53+oGcc9Sy2gYVJYRdYQD7WsNtpfND7aaweB+q0PYJ1XbYZo4FTYMzPwndq+o2VzQbU8m/Z8o1QcJCiyG70M/DakiF6YZJWZp1P+IofLeMJmTVXKkRuVSCpxefGby/jmV193UtUdP3DUXa0KzMGRlA/v7fKWDzcM/Z1fvD9l5/gjUWvzREWZp0pJXESpgKi8yuGNJBGHZwxRtJtZiVArtQxKKiO62FRlor2JYZEBU1JYPJYDQhyoRXL1JqGTpDIotZCq6CDa+II6LEkkFDUtq9Nbg/bFnPKGXl8cyy+D5G/xa2yBUcsrSDQ/ZZwhb9AAB2yq66NFLZCqLqIZmiK1ufsu3rsNhJF1INSYZKC4lMa0WoUIcuSvxBxBJ8F0IIHBESS/tmiWp7fkPLcZhesluIV7ZCNmnyjFzKZnkgltkQsWCo9mWQz/2Oh+Uqkg3Pm7YM+v0MskXEssNJogl49YG9dXfZKhUmWkQsIzLJ6gbHy5MO7OWsw+sJfTYRE8t0Yvil4OrK6gxUiIXGCjPfdvq+qk2litOhhWyfUFeoEzwpBQZTJQpOhR5/TyBSSc1FMQrhDOOit4w8iqJZmylEOTS7MtvnopevO49/q5zL7pX1ijpLerr4qPsOHl50Tt12W8m7iIFn8R/5L26pnMzZx66u3h92YfmNAnxjQJSCJoDQljV8nrqIRbpIw6KAjYjIhiT8AgWzK0htqBOR8Mo4YmxiSZq11Ixo43c/HuiZcDalOeK0/GLT4N2IkcVyg4sSwyviRE+yKiou7wqyxmEXmZ5TMvaNxOK5cM+/wv+5oNYGO/g8hldgi1zOAX0Zjt23i9+UA2XmYbOnLi1USQbEtUtmm+oWfR0Wu2QWrRB0sDkqBZrsaE0s4aBwtO5nyApSq03puyot6Le54AGQ4XfRQCyhIrg2hqJCiKjsTDRisVRXmPCcYJ7shU383j+EVINWXMYy2EsGv/GCobiXZwoJ1qzMkU3VKxBYKhVWRyzqfaQznU37zzViYplODG8PohUhQNMh1ctifahtKkyG2ldhO2JILKgf8kSJxQicC7cPllgi+hlOqwgh35zm0JwRCrT+IZWw0doQSyhuOJo97+uPWslPFr2bIw6rV+LpSSf4eeocUsuPrNueVsSy4qnvo3tl/sd6I284ouZRMtlUmCgPMUyqWsBOGEaQmohELCYO6Img8SFCLLZfgEQmMGVSP2DNc6oyIqPB1DVKilgaC7cTgamEKPN7m2V5bFlsEiktmzlSSojS9Ip19Ybw5FzZEwyuhh1cdk9Q9yr1R4Ykt/4WvnYq3PYReOpO2PitYLvqCBtM70/aMnjVft3c7weWuGW7YRZMOaLuJktfRz0ZL+qw2ClzJIpBJF0Z6ScvbTrSrY9HvapDV0vPGlSQeu27cNR7DZWlW6KNi6QbEss4Gy06I8RiNdZYMBC+Cy9sQngOv/cPadLiylgGg42pMCmRxb08nU9w6kHNc3W2ajfWvfpUmIfGvot7mvafa8TEMp0IZ1hCZBbTx0BbYgmno8OIJUx9VSMWa2InJd9IkpBlXhoosJi9uL3Bid0baY5YNDdPUbT+ITlas+hf9b4wYhnl6i6XSvDTK07hmH3qc8uaJrjjA6fxjpP3q9ueSWcoS4NkeRcb/dWsO/6UuqtfK5wbaUcsUrYucLvDFCLdZY2pCgALF9+wKAu7ZhMN2H4RqYgltIvW/DLuOCIWgJJKQ7VtwBgHktngBFMc2Fl/h+9hU25qDHCtLlKyABWHhCzVpYXC1nSx91mGZJKuzuB2tm8ZrtQp7FJDkvdeDze8MWi3vvh7cMBr4b6vBZ+90gjTFwe5+7Urc9zLEdzjHca2zqPr1qJ3BMS1R3Y2RSxpy2BA68auDIFbwi/uZZBmAcrqczXKDEmJSX3E4oWDqKNELCLsomyKWJQG3BiDryFsUydhaIF7ZaTArmvBgKTmOfDsPUgEG/3V1VmiEGkVsWilgdpx6xYRXplBmeaQJc3HTDggqUWUInBGKGCzKDs3nV+jISaW6UQYsYTILKJbDrA77+BUWkiTh1GBWU8si6o1lolFLNJIYlNmz0svYAoPY+kRwfbi3qZ9zUqzLXGIspbE8FqnnZzq1d3kDuZO22wSvcvYBsMEa/mu9zredtw+dfdbphHUfdoRy8M/gM+tbrrfdIcpRhSggx++USuu+l5QHNdtHM3GCMnU90kREEtRJNHC7h7PoTJOYglz/kZy8qmwlOqmCs3RqgiPm4bjww9rMsV+LL+EF/l+w3Uk8s/TLzvpTgfvY0k2zQ66giHJrb+Bn38EDn4TvO8+Kge9gd1r3hPMmzx0E/7OxwPPlaVB7SxtGeyzdAlvdz+K01NfKLZUwb6fbEvCKNmqhjCyE4qDDLVQNm5ce1UZQSk0yIisUCUklFEueGr2xPVSKmEqTB9jPimKbNKsu/gJ4QkDzXfh2d9S7jmUITJNtgkZ2whUFfxKbS3qNzpAmlyq+RgLu8KMaMRSzjNCsmoiNp8QE8t0QcoWEcsiOrwg3N053Fx4Fk4jsQQnikViAA89UEmeyBLMJEkc9uzYCkBqxRF4aMGVUQNMbwSnjdyIq9mYXutCeSgEOd589Hhg6hojJNkjO3AOPpdlDf4Rpi4oYzZ7UYTYcltwghqpr0UkvHydAjSgIhY11xMSkWHhaknMUDbEDaU4MhRFCkMN1enSoaKN7zspq4jFbKOeMB7ksjlK0sQbbqixOLX11UG5izKyO4hoImkhXZ1U0+Wd9NNBj1LwXZpNsl12Y+1+BH7wDuhehbzgeu56Os8bvvRrjtvgUuw5DO75Cs72R9niL69rbT123yAqXdRZH5Vkcn28IHt4PnFgy0l4T+mKMbwDUa6vhTUitBquqkGEFwaRVFhYWxGjpLPCYWBZqo9YPHVMjyXVE0U2aba0Q6gIM7goe/73DPatAwIBzChySbOq9lDtxlO/0QGZIdeiXqJrAlck0H2nFuU4eUak1dbvZy4RE8t0oTwUXEk2RCzJ8m5AVmX1o6gWi8NUmEpX9DIYFF4nKrduJElSpqim7hPdKxkRGYxyM7FYfgG3jZGYq0dOsg0Ir+6M0XLZk8B/6W/kU+5f8vaTVjfdJ4QiFq91E8TI04G4gpuvT/klvXxTa3NFGNUBtmqXmWHj6ClMFaWFHXNYGcpaClMN1em+U5URGQvhBLjVbsh1HOjOWGo2pZ5YQrFMrWE+wlDFfmfvNnR8/IiUSGgEpyHZK3LV9MyiTouXZDe54S3g5HnpDf/OX/7H47zjhvtxPR/bNPhv6wLY/QT2zs1skcs5KDK4F6rvhq3GIXo7kpxc/jK/zb2p5XsTmZqsi9HGiyWElQwldxqIJULyVWJJtCeW8DNwGlQlPBUBjiUuGkX7iMWkp/wcuAV29wbE0pgK60olgnZjqDVGVCOW1sQCQdenhl/T1CsPMyztGfFTmSpiYpkuRGdYQqQXofkunRR4aaiZWKo5/YaIxRQebps01WgQicBW2B9Q2k+dyynoHSTcVsTSwpZYoarN1ALuBPPR48WtmQt4qOcsTjqgdSHSEQm0VqmwQj/pkUDwcrjBbTHpj1AxG4ZIMYPiKhHjMMOqe8+hyZdmZXCMNAnlex+oOo89mQ3g6iGxTF7xKJhN6ajOpoSIri8Ks1PVZHY9qzbUjqFERLqnYNRml2xTZ48RnORLb/wSl/xkmAefH+Bj5xzGbf/rNC5at5JPbT0ELxOkv56Uy+smwk88oIcDF2WqkUuIwH9FtJ1kN3OqFT3/EmZlmLzItDxRAySSDWoQYcQZiVjC6fzGYcQoQpJ3GgRWQ2HHxDQQS0UkAvkZYHs2qDs1pqoCYlHHZfjdKmIZlGlyydZRcfXYU+cNr5SnINs7lM4l5t+KFiqiMywhIrIurSIW3WvIlRs2Hjo6HpVJEgtAx8hzVHQDI9VLycySbJTnBpKywGA7YjHS2G0ilnD63ZhmYvnkmw+n0zbbemFUolIZUbxYc2ooDO2m2rDquSQp4zfI3lSEieYHV7xOqYgNaIaNbySx1AxQcWQQm0Bip2KksVSbsyHdJlXndggL53ZmCl1husaglqWz3EAshSE6qMnzh0ipYr/TvxWov3oPPd8Byol6Ergj+xYGzHU88egBbNn5EjdedhynqM6ky05exY2/28rdPX/G6fkv058+sO5E1p1OcPuVpzWtvU9FMO2IJdW1GF8K/MGXsCtDlNtI5gOkLJOiTESIRUUsRiRiUcrIoo3eGEAqmQw8bhoiFt8pUZL1SsVj4bWHLKoj2OpzaUagoNh9AHtFF/B8VfMuRIdtMBjq5RXqI5YRvbPqNtmIimYHz+2WwM4iVY0lJpaXM1pFLGr6frkxxI5WEYtXwkdHC2spQuDqSXQv39bvfjSEYo37sp1hs5cuTcM1c6QL9e2qUkpSstico1fwzcAiueV9IbFMY40F4KQDeke93xEJdL+ZWErPbiRsI6ib11FDjbIlsQRXvGH0JRI2npnCJnj+csSrxjEyVXtiUzrVobyxUNGTuFInZU9NBLBo5LCcp+q2hTL+jerXaSVE6fUHqdDocGYyXfscXLt+XsTqWs6XHzfxn3+Jj7zx0CqpAOzTk+Ksw5bw90+fwBvsFMVFrxrXukMXxr6O1sTS25FmD50k+58n4xdxRumATCnJFhnWvjwHAYhIDXJr9ym886kreW3XQW2fpyNpUsRqEliVbpESibYWvq3wFyfs23K7FzZ37HsSw6VAOqbxxK9pAmnlQNIUsQg71/biShoWVKhGLIF1QrZqnTCfEKfCpgthxJKJ6CIpYjkgVaw6TEZhesUgZRI5kEKZDt+c+MR2mAZYJbZTSgbr8OwsnXIY16t1pRWdChmKTaqrIaSZIinbzN6UQkn/9leGM4GKiEhlRDDyzO95UQYnykqkyB3KuQi7PhXlCaOaCqs2Ipg2mClMKoHQpLIUMJOd+IkMKYrg+5jSxR/Ekd/5AAAgAElEQVRnQ8XO5IE8LFeRtqf2oy+ZOdINQpRh222j+nVXZ4YhmUIbClKh0RpMKpms+s7IVP2cxNJsEl/C+WuXcfkp9WoOAH91yip2F+H/DBzF6iXj+94tQ+fLbzu67Qm4r9Nil8zBrj8BVO2jWyGZUCKTqtkltJYWkYglYSf5hb+upclXiIwVEItXbiaWMuaEiKUd/LAFet+TeeSFQXozCbpa1Ey0quNnjVg89PbCs1DT51O1QeGMMCKTTc0B8wExsUwXhl8KJu6jJ+uqrEu+KWJxKj62LOM2SFCE09RyUsQSPHaF2FWV6pB2F1mRZzCicJwfGUYXskkduQozRUJUcJ1mcgnrNx19+zTdN5OotIpYpMTe+Qfu8w+lLE38iN5VcVhpZiUbicUMOmuAilMjlqqCrjuCqyICK5WtRXVOngQOcpw1lk2L3spb3U+OeqIbD1yrm7TM1+oKQEXVCBINlgdd6QT9sgN7JJiiNyLHYtoyKKjYTkvXR4evP3wx561Zxj+95aiWV8vr9u1izYrgczxoHLLyIc5bs4zludaRbV8mGJK0Bp4EIlLyLZBKGBSkVW2zrhJLhOTDdNNo5JC29CCl1mgYVylRkom2KaiJIIxo5b4ncu/Tezh+VU/Lz7QjbQf2EdWIJdAJy6XaH19VGSUVsWjuCCPYcbvxyxqNMywQyIBrJiuMoabifdHxSIlyk7ZRdehtEhpToZilLiSakqgn1R0oHA/XfkzFvCr+tnGoDLvUCiPNJmHa0AvslDl6spOvHUwGFS2B4TdELEMvknb28JR5MAOkqyq5UJPg19P1J6y6VJgiFj1hV2eIKqU8FdWOmkhnqzl7WR7CxEWOojgQRXcmQU86MaZ/+liozqZESDPUmAu9c0LkkiZ76SDjKoMzu3YMpS2dEUUsYZE/xCkH9fHltx1dZ3EQhRCC95x2AELAUSumx35pUWdALKar6n92++cNU2GhGoTnBMdBNGIJiaXdewDosEwK2M3K3ZXShFNh7bApeTI/zlzINr+PFwdLnLB/a5marlSCQdFRF7EMkRlVmqUqo+SWwPfQvSIj0m7qOpsPiIllOjCyG569B7oa0giaBuk++vQhdgyWkZHp8IJbIUW5SdsoTIE1mniNB0bkRBJKdRjp4MAeGaqliao1hDZT4aH8R7nQTCyJwnZ2631jKvxONzwtgS4biOXFBwDQVh7LIBm0Um0QNPRisRoUc33NxJABsYT+LoZpVYVAS4Xh6ok7memsfg+l4X4M/HHPFr3n1AP43jtPmMhbbAmhLBKiLcehIZndELEYusaw1lntSooajiV0jYIMiMVuUBseD95w5FLu+/AZ4zLCGg960ladtL5ItlE2JlQvrknuuOp7ExGdu5AURosQU5bS22rQnBMhsRhTJ5bnU4dyQ+od/O6ZgDBO2L91l2OoRh1tNx6QaXJtWq6BmqpApVgToIwjlpcppIQfvz+QNj/jH5vvzyyix9+L4/n0j9ROjAXHI0kJv6FIH6ZeGjt+xoNEhFgyfQGxJDqCA7swUGvFDR0RzTZ1kpCgSoXmbrKO8ksMJxY3bZ9p+C0ilvzT9+FKnZ791zEsOjGcWi3CVTUWqyFi8UQCvYFY9ESqenVfLuTxFbGkMp3o6jMqDagGiDZ2AY3IpsbnRjgWwtmUUmg5TOAbU5EaqVRzY0C0lThqkSyEoKSEGtNdk/v+Fk2jaZSuCUYStcjJSLcnFiFEILmj5ow8JSKqG7WTcDViGYVYkqZOUVp1mnBQIxZrGlJhpi5wPcm9T++hJ51o2TkGkEsm2OOn64r3e/x02xmW4Mkj0kZKDWIEm1RcY3kZYuO34Ilb4XVXw5Ijm+/PLKbDC65KoumwouORFE4tt6/Q0x1EGD1d7X9o7ZCInEgSylTLVmmP0lCtYyrsKmrM0YcI5yOcQoMni5T0eLsopZa1eNTMwtMsjIaIpfzsRh6XKzliv8UUjU4st0YslZHgM0921qcifM1El0G3To1Y7GrjQ7mYh/IweWmTthPVyflyeGIfZypsuhAKUY7sjRJLPvCzt5pPQmWzRiyNw5klkWQ4ohM213CTNWJJtPNiUXA0uyrA6FWL9zWSP2xZJ/v3pVnZ3b5bMVSd1hoUHHSvHDiDtpmjmQgShoZT8bnv6X6O37+7bSq0K2Wyx08jVSpMFvfS76dayrmEEOFQshuJWGQs6fLyw64nAm2lA14Lx/91630yi0iVg2ghOstScDxSlGtT9wqhaOFk5NatVKQu06n0nLLBFa8bUTiuKOOq6GxDFGEKpSr6p+Dm95CkjOycfWLxdQtTRiyWfZ/Mnod4SB7I4cs6ccxOkpVahBWac2U6moklJKhw8t60khjqPbuFIdXGaZNOGJhqwLE0MDfEEuqFlQZq9sFCFW0b5yMAKnbtBN1ILGU9zR7ZWZVzmWv46VrkZHeMrtAbyAypiEURi27W3schSzq58wOvGfXErGmBb4veoIOneSVcMfV6GASzR8/vLfDCQLFtGgwgl04wIDMRYhlgUGbaqg9AhFgqpaqQZlHY00KI0435t6KFAs+F//qrgBjOvz6op7RC53KM4m5MKnURS8GpkKTcbASVmHzxPiQEH1FtJAiH5vyIjlZILHYbYglF/yoNE8r92wMvD6NrdjvCAHw9ETgdVhfzFJY3wq7Ow7BNHSeRI+1H1lsaCKTYG7psfC2BoSKWkFgSll397CqlPJobWAromqim0tzhgFi0cabCpguZniVUpEZlsDaLJNwRitiYLSyS/WTtZGYn64+hWzIX87HKpfSkZ/c9tIPRqUQ2pUnHGIOkFT1Fwg8jluA4mMx34UTtuxV0v4w7zsHXsZAwNAqOB8Dxq0YhlqRSVXCGwS2iOcNtdcKq67SaI5aKmZ4WQpxuzL8YaqFAM+CE9wW2o43dYFFklyOQLBF72TFYnwpLiTJao7ZR2N7azj1yFIQCfCNGNx1K7kJTueuowrFfLU63JpZQQqNSro9YBrc/w2JqFsOzCalbJKhFLP62jWiAWB7oMflWLhhwdItgJqE0xBAp+hrmSKReK95LRSxGIoWpru7d0gi6O0JRhJPzwWckFTGLxOxGLN1pm13kagO4gF4pVGX5GxEW+wvSakqVvZQ+hD/oS0btnJpNWF1BS/wQqTqPk1ao6EkSKnXpV5ojlvEi6rIawvDLVKaLWBTZd6cTdZpqjehKJdgbClH2Bx45A7SXc4HaADSVUs06fBJjCbOBmFgmCyFg7dvG3k+ljQ5LD9UNSQbF+zKyUdsoTIFNxtJWEUvYERY8XxYPDRFROK763beJWGx1kvUaJpRDccvcsv0nvrYpIiCWWsQytHUzSWmy7MA1wYakqi0UB8BMojnDjJBiWcNVvdQSwSAkkYjFTlbrU145j1UZYVgpP4fkq6murMnaBUwWvR0WT8oc3SM1YjEqBUZE61pCWOwvYDVNZKctoyqXPx+Qy3WRl/aoApQhfCOJiQteBV9FLPooLqbtUNFtzEp9KmxaiUWlpY5f1T1q52QuZTIQClH2B5mAsSIWQxGL7xTR1G+4nXrGXCMmlplGZ2D9utoeYnNDKixFiZI9fakwdBOEjplbXtumac0Kx+qgbOdumEg1W8ECeAPP40idxUtXTHxtU4VhoSPBq4BuMLz7eQZlF0fuE9RQNHWlXh7ejdW5FMMdoqA1f4ZSN4MTFDVHSstOklSNDH55hIQ3QlkPni+dSlOWBkYpyIXPNrFkLIM9opulxVrx3qgUcPTWFwVWKESJRU9Dl9P/d+K+7ByamAvnTGJRh81OmWOQNMvHIpbIAGstYpk4GXhGkoTrgO9X09eGX8abxHO1QpiePH5V6/mVELlUMHMEQH8g2TPI6DUWI2HhS0GlXCChIpamVPo8QVxjmWmoQcX9Envrpu8LhRF0IatF4yoS6mCbzJWIEMGVe1d9qqqgd2BFWnE1Jx9IZGitUyKhrpR06lNh+vA2doheUtYcXPWqiXeprjb9we3sEl0c2Kfas9W8TthWnajkKenNxBlEPpWgTbxSwpE6lmmSTAfPI8sjJLwCrh78YDtsgzxJbEdN8s9yKgxgxOwl5dRqZAm/2Fb9Oqn0wkpYTbn3Uw7q463HzsFFQRv0dVj8zj+cTf7qMSOWavekU8CvTD5iqQ4kR6IWUzrjVq0eC2HEckIble4QXakEA1KRgopYBuXo7cahi6TnFKqpsKlYX88k4ohlpmF1gJVlhdbP9v4asTz0TNDlYzUaQS06BFK90LXf5F7vL/4bsvUnj6KRxY4SSyVPUSTbON5DUqXCZIP0RbL4EnuNPla2etBMQxVqK04J0+rALu9iyNin6kZpqa6i4tBuugDLy1PWlzY/Tyi17lcQXpkyCVKGRlIp6OKMKEuB4ERmGRo7SdKlvOT1WY5YAIp2Hx35Qag4YCRI+EUqbXxHOnK9+FJUjcbmMxZ1WFxU+StMXfDOsWZIqhFLAamIxZwEsVTnxpxCkBXw3MC7Rp+ez+vYfbo4/eA+Vi8a/YSfSujkNdXuvyeIWIZFZtTW4dBFMuHUiveN1gnzBfMqYhFCdAshfiGE2KL+b9ncLoS4RO2zRQhxidqWEkL8VAjxuBDiUSHEtbO7+lGQXU6f3MNwqULBCf49vFW1jzY63i05Ev7hKeiY5BDisrXQoAXlJrKkIh1TmjNCqY3fPYCmB5pKwq0nlpyzg7zd4mQ9CxDqhB4KR3a6uylYtTmIZDivoxSOk16+yYsFavpSslKCShkHE10TgR4VgR6VLYt4ZvCDFUJQEEk6ZPD56dOs6jweVNty8zsAsP1iW/XrrkySAdKUtflPLKHycTbZ3i6hijDl44wglRipkZhE5FydBVHHtprCny5ied1hi/n2O44bU5lCCIEM5XpUxCLtrlE/h5BYPDdoNy5ikbbnT80sinlFLMCHgDuklAcBd6jbdRBCdAMfB44HjgM+HiGgz0kpDwGOBk4WQrxhdpY9BjqX01WpzbLc8+Seak/+pGopE0TFytEhh/F8SbniBVPpY1zplISNcCM1Fq9Ct+ynkp5bYnFKRSgPk5RF3FSNfNO5cF5nD0hJShZaKuaGIpKe6yC8gFggmAQvYKO5+aCpItJtUxK1v405SIWFVgxyeHsgvEmpVnNoQHc6Qb/sxGlRX5pvSFsGqYQ+ZkcYUOuedGupMGMydZHokCFU7a7lLLeRQzBnVMGAoUA0VBvDFM42NUrSRKqIpcD8NPmC+UcsbwZuVH/fCJzfYp+zgF9IKfullHuBXwBnSykLUsq7AKSUDvAAMD8SytnlZEpBV89LQyXufGIn3WbQmcQ0W/y2grS7yJFnuOSy8Zk9HMZTaH0Hj/qYkrDQInno4T3bAq2s7Nx8pKEAn1suVFtvZaZGcp2dWcrSwB/ph0oJkwp+okXEooQLHaeI8Mq4EavhsrCxy0HEE+22KUfqGRPxRZ8uJLqCOl2x/wWolJpsh6PotE0+UbmEmzPj6FicB1jUYY1ZX4GaJYRbzIPnUJEaCXN8NtF1SNSaANQTAoxbXHQ6kUtZDGvBMToiMnSmRl+DpSIW6RbByQfqEDGxjAuLpZThiPFLQKt80HLg+cjtbWpbFUKIHHAuQdTTEkKIdwkhNgohNu7atavdbtODzhWY5X4sHLYPlLjr8Z0cv0IdRG2uPKcTItVVVTh+7MF76RNDdB31+lEfUxZJ9IimUv+LQa+91TP7w5FQq21UnBLF/kC638zViCWXthhEifopzxhauAmGxFIpO2iegyNqqYSysEm7AbFElQ/CQj4ErcmzjXRP0LI+svuFam69XTeQpgn+mDyW7elDZ219U8HalTkOWzq2x0voLeMU81BxcDFaDoiOBREScjViUV1ys3CB14hcymRQdYYNicyoqgEQ1PvKJJCVIn45z4i056XJF8xB8V4IcTvQaqLwI9EbUkophJAt9hvr+Q1gA/BlKeXT7faTUn4d+DrAunXrJvw6E4LqDFsi+vnln3axfbDEcWtseJFZSYWFHVPDg3vwn7wTAGv160Z9TDChXItY8jsCYsksbjaCmg1o6ofvOiWGhp8nCSS7a9FTOqGzXabRSnsp5/diAVqy2eMj1Jdy3RKaV6YSIRZHs8m4gV98tNvGNdIoc0mMOTgBZXuXUZEa5b3b8ErD6DBq12Bfh0WnPYmr+TnAFy8+elz7VSV3SsPghcQy8YlzTXkWSWcEAdXuMDEHEUtXKsFemWY/Qq/70b+zsMYi3BJ+SZLHnpcmXzAHxCKlbHtGE0LsEEIslVJuF0IsBXa22O0F4DWR2yuAX0Zufx3YIqX84jQsd3rQGRDLAdYAP38kSOOsWaxOaLMQsZhqaO7Zbc9zyMhG9nbuT9cYel+ulqzVgYCysrvtWTY3xFKNWMpF/P4gJ51ZVCMWIQR5rYNOZ5CRoT1YNJt8AWhhxOKU0H0HN0Isrpakww29amrEEhbyYW4ilkXZJLvI4Q1up1wYJsUoXjrAP//ZUfPSo2MqCCV3wlSYgzEpjawwpVYpjwTVtdCNcY4ilt1eBjTo99OjerEA2IbOkExApYRfdinEqbBx48fAJervS4Aftdjn58DrhRBdqmj/erUNIcQ1QBb4u1lY6/ihiOVgewjH81mzIktWV/IkbdpGpxO2EqJ84JHHOE57HPY/fczHuHqyXvpicBvDMklf76KZWuaoCOdHPKdIZXA7I9Kir6e++61gdGI5g5TyQWuw2UKKPUyFeU4Z3S/jabUfcyViuha1FPAj0YE1B8SyuNNmh8yhjeyoWh6M1mZ61IpcW7n2hYrQAqBSGgHPnXQqrFarCdK80g1dROeCWJR0PrDHT40q5wJB8b6MiaiUoJxXEUtMLOPBtcCZQogtwOvUbYQQ64QQ/w4gpewHPgXcr/59UkrZL4RYQZBOOwx4QAixWQhx+Vy8iSao6GB/KzjhnX7IolrxcBa0flKdwQl4xc5fYguX3BGj11cgsEi2/MgQWX47O7U+9Fk2+AphqDZfzynB8HZ2yC4WN/iDlI0stjeEo4jFyjSnwkLhwopbRvcdKqLWDVQxWhNLWMgvSwNrMgXjKSJjGfSLbhLFnVXztajXyisBVqomuYPn4Ep9UsRSS6kFA4YVNaulzUG3X1fKZEDVWMaSc4FaKkzzSuCOKMn8OBU2JqSUe4AzWmzfCFweuf0t4FsN+2wD5p/MJwRRSbKbFVowvX3GIYtha7523wwjkwtmPN6o30dFGBj7vXrMx/hGEkvWIpZ0aTsD5uwbfIXQE2GbcIlkYSc7RDf7N1ytuYkc6dIw/SOt3SMBNLNGLIZ08CKOkNHZkKhXTWhP7GCSmiOJ8mGzl5SzhX5FLEYbk7aXK5K2jSN1/PIIwndxhTmpixzTDr7jiiIWt1zEhGYx2FlALmXyjNILGyDDPuMhFhkQi+aXKGDN25TnfItYXr7ILmdVYoCTDujh8GWd8MImyK6cnHTLBGFkgqn0JWIvQz1HjznDAsGEsk2NWLoquyimJm5pO10wVdHVd4ukyjsZNnub9vGsHElKyHzQ5ddo8gU1GRDPLWP4Ln5EyiPq5hm1FKganzG5k9l0oGT30eEN4o8EXWuJRsWGlzmSpkERC98JiKUyyWvihB1GPkp2vhxELMYcDL7mIgrH4xHitE2NEia6V0arFMgzP02+ICaW2UPnCpbQz/feeULgSb7117Dq1EDfa6ZhdeKprzp96OjdYCGkmSIlS7ilPF55hC6G8DqWj/3AGUI4P+K7ZTrdPXVT9yGk6gIzh56jIjU6WtgCGCoV5rslTOnga5G8dqSRwo48NmwCCIcp5wKemr7XBoLuvETqlUUsqYROARvpFBGeQ0VM7oRq21Yw76RSYCGx6NbsRyyBXpgiFtLjaDdWki6VPJr0KMjWZm/zATGxzBayy2EwmL9gxyPBvMWqU2fntZXCMYB18PiIxTrwVDQkz37xLPY8szl4mtycqIQBtYl3o7ALizKVVHNaTksFEUpi+DnyJOlocQWoJWqT96Z06iKWUOjQk4J0uhbVGSo6iM68zDaEmr5PDAbEYqeb60cvZ6QSOgVpIdwC2hQilmQoi6KIxQsjljkYfM2lTPplcGztlR10jZEK0zWBKyw0fADy2HHE8opH53IoDQQDbs/cHWzb75RZe/lUthffysKy8c0NHPPaP+POIz/LyuLj2P+5PniO3rkZjoRam689omZjW5irGZmAWNKFFxgi1XJ4LJQBkZUyJg5+pMYSDkWOkCQdmQMJLZyjU/qzjYQyxbIGn8GXgmRq/ku2TCeSCZ0iNWLxJvldVCMflQrznNCeem5qLA/I1fyzeAe/9o8csysMqPONiduNY9SkUAZfCIil58Dq4ORswNjvRLQ169tK5bfC6/7s3fx8zVfQ/UCbKbtkvxla3diwVNHVzodT981zOImOoO6SdXcyItIthQDDeRjPdUhQqdOIEqqAm8cmadY+p9CeuDKHEUu6Jzh+OgrPUcAiM0/FB2cKoUioVimgSXfyqTAzEFitik86wf9hUX82YRk6dsLkfxfPxBEJOuyx35MXEctsPE7nE+Yn3b0coWZZGHgWnv0tHHXR7L7++f97Ug877y1v5yYjQ+nBW/iz/Q6Z5kWNH5ZpUJYG6UIwHJnsadYsS6q2ag2fomh9RR8q4spKMXCSjKTCdBWxFEnWkZJdjVjm7mTe2buUitQwcdhLjuw8bTOdKeiaoISNXikgfRdvFHXu0ZBK6OzFwlLqxr5bpCxNbHNuToW5pEnB8cgmzTEVkYEgwvbV30ZqXI+ZC8TEMlsIo5PHfhKY9Kw6bW7XMwFcdN55yHPPHVvafAaR0DVGMMkplejO3mZiSWdrBf2S0bq4bapajVYO2naj4oO6cvMsavUnrZSah4kOU842FmdT7CLHUvopYNM3iRmOhQ5Hs9G9IaQEf5LfRdLUeRGLLiXlIt0iZUzsObryz6USvDhYGlPOJYSv2ygDVHxz/s4yvfKOzrlCh0rdPHpL8P8s1lemA3NJKhCIK4ZdWUMySV9PcytxZzaHK4MThGu0/tGFTQDCCYhFRFJh4fBcWasv5KZTNiVp1g1TzjYWdVjskAHBFYU959/HXMDVbEyviD6FGksyEaTCtCqxlCiRmDNi6UoH7yM7RkdYiKhvjJyntsQQE8vswUhAehGUh2DxkZAe3bo0RjNCYtkpu1jU2XySz6YTDKC0oNpczZmqKwwVsRCJWKp6VA0RS8Y2GCaJp81dKiycvodAefqViFBmSJcuvja5ZItlaBRVrQYUsUgTeywHyxlCWLAfb8RC1MF0nrpHQkwss4swHTZbbcYvM4RdWbu17pYTxx2WEUjnA14Lky8A06qPWLRIxBLOhrgN7oyWofOCXMSg2Tw7M1sQQlSHQh3tlUksFT1Jwi+hywreJOtdQgil3K3EJyvFOY1YQhmXseRcQkRTtyImlhhArYAfE8ukEBbPW03dg1I4FgGhyDbEkjAMXKmjOYGkh4hcAYbT7BWjOcXwPv1j/Kj3XZNf/DSgZAfE5uqvTGLx9BSWDAZbpT75epcbsYQQlSAVNhml5OlAV2piEYuI6Nnp9vwdko2JZTbRtR9oBux70lyvZEEiJJZSi6n7EAVd/dhamHwBmLqGi4HhBsSiRYglmQ4e67VIo1mZ7JzPjlTU9L2rz35r7HyArxSIk7Iw6eI9QEWzMf3AYEd4pTku3k+sxhLK+1fQseZgqHO8iLvCZhMnXQGrz2570osxOioqFVZJt9csK5lZ8EBLtZ5M1zWBg4FZUcQSUbW1Uxn+5C9nT+agpsd99e3HjMtCdyahdSyFHfVima8kSPW+DTzkVIhFtzHVYKTmlSnJuYtYchOMWFDH6wjJeTscCTGxzC46Fgf/YkwKFS0BHi2n7kO4iSyUwGhh8lXdBwPLCyav9UjEkrJMjnOu411L9m96zKHjsM+daYRWzN4sWC3MR8iIUKQ/hUYKz0hhOBXwXDSvjCs65qzLLiSU8dZYhJIdGpnHU/cQp8JiLCB4Ss6i1dR9dR9LCVGmm9uRQ7jCxPIVsUQilqSpk02aLMvOvjfHeBBO38+G6+h8hIi+7ylELNWWXbeA7pVw57Dbb4k61hZ1jO+YC4/XvLTmNbHM35XFiNGAsN031WLqPoRMBoTSyuQrRAWDpB90hemRq2BdE9zxgdPmPOXVDtneZXylcj56z2sZ26rt5YdoF5TUJ08GfkhQTgHDL+PO4XzSEcuz/OA9J7Ju32bvoFbQlezQCEnS81TZGOKIJcYCQkgs2b72xPLSktP5t8o5GL3N6awQFWGSJCjemg0+HL0Za1LOhLOBRdkkn69cRDF34FwvZU6gRQcCp9AVJsPOKjcgFk+fO2IBeNV+3eNOxRkhsczziGV+/oJixGiBip5kr8ywqLt9NGJ2LefaytvpTLdPLVQiU9vGHFjSThZLsjaGJqotqq80aFaUWCb/GYRio7hFDN+pplgXAhKJBJ4Uqng/fyOW+Ut5MWI04Nc9F/HN3Yfzbx3tTwSnre7jwmNXsE93+zqEF1HGNedxy2YjMpbBLe89mQMWvTKL94Y9PRFLtUblFjBluU4xeL7DThiUSDCCTWae2hJDTCwxFhCGOw7goWR61JmDfXvSXHfhmlGfJyp/Pxc+HFPBkSvad7u93GFEaixiChGLFn7n5SEMvDqzt/kOy9DYKXO8JLtZO49TYfN3ZTFiNODdp+7POUctnfLzRCOWhL1wrlZf6TCTkUlzYwrEomo1stCPoF7Ycb7DNnUucj7OMEm+HxNLjBhTx369afbrnXoaKDq1PZ+nl2PUw0pGIpYpEIuuajWV/G5MwDcWErFo7CKoMWbmcY0lLt7HeMUhFDB0pE4iMT9bi2M0w0qm8GXQPaVNIRVmqFRYJd8fbpjy2mYLViQNHHeFxYgxjxBGLGUSJOZpa3GMZqSswJ4YphaxGCry8Ub2hBumvLbZgm3UiKWVwvd8wbz6VQkhuoUQv04woaIAABeVSURBVBBCbFH/t5waEkJcovbZIoS4pMX9PxZCPDLzK46xEBE6QboYGDGxLBikEjrFKrFMPsowVSpMhsRiLqxUWIh4QHL8+BBwh5TyIOAOdbsOQohu4OPA8cBxwMejBCSEeAuQn53lxliIkGrQsswrcx5koSKZMCjIgFA0Y/IpzGqtphikwkLF4IWAsCPSNrV5fVE031b2ZuBG9feNwPkt9jkL+IWUsl9KuRf4BXA2gBAiA1wJXDMLa42xQBHKgbiTtLeNMTdImXo1Faabk49YQoLSinvVcy0cYglVmNPzOA0G849YFkspt6u/XwJaSQEvB56P3N6mtgF8Cvg8UBjrhYQQ7xJCbBRCbNy1a9cUlhxjoSGUXHcm6UIYY26QjKTCtCmkwpIJjSIJtFJALAsxYpnPhXuYg3ZjIcTtQCvd849Eb0gppRBCTuB51wIHSCn/lxBiv7H2l1J+Hfg6wLp168b9OjEWPsKIpRJHLAsKoV89gG5O/qIgaRoUsegsq4hlAbWch8SSmsf1FZgDYpFSvq7dfUKIHUKIpVLK7UKIpcDOFru9ALwmcnsF8EvgRGCdEGIrwftaJIT4pZTyNUwCruuybds2SqXSZB4eY57Ctm2wlAVxHLEsKAghKIuABLQpdIUlEzpFaWF6u4F6hev5jrB4n4kjlgnhx8AlwLXq/x+12OfnwGciBfvXAx+WUvYD1wOoiOV/JksqANu2baOjo4P99ttvzkyAYkwvpJTs2bOH1IGnwPPXB8ZhMRYUHM0GCUZiCqkwU2cHtccvqIjFWBipsPlWY7kWOFMIsQV4nbqNEGKdEOLfARSBfAq4X/37pNo2rSiVSvT09MSk8jKCEIKenh70VHBNUplDH44Yk4OrBSRgTKl4r1Miqhe3cEQ9NU2Q0LV5rWwM8yxikVLuAc5osX0jcHnk9reAb43yPFuBI6a6nphUXn4IvtPge/Wn4EIYY25Q0ZPggTGF2ZOkGaTCQhgLKGKBoNY037vC5vfqYsSYCagLhoXkwxEjgGckwZlau3HC0ChFolXTXjgRC8Cbj17Gifv3zvUyRsV8S4XFiEDXddauXcsRRxzBueeey8DAwFwvacK4+uqr+dznPtdyeyqVYufOWn9GJpNp2q8RJ5100jSsShHLFPSmYswNBhJL6JcZTGtqFwWOCCKesjSwzYV1fX3N+UfypmlQ+Z5JxMQyj5FMJtm8eTOPPPII3d3dfPWrX53rJQFBEdz3/Sk/T29vL5///Ocn9Jh77rlnyq8bRixyAcmlxwhwX+5NnFr+IuYU2o0BXPXdlzFH9feJMTksLKqeI3ziJ4/yxxeHpvU5D1vWycfPPXzc+5944ok89NBDADz11FO8733vY9euXaRSKb7xjW9wyCGHsGPHDt7znvfw9NNPA3D99ddz0kkn8YUvfIFvfSsoSV1++eX83d/9HR/60IdYuXIl73vf+4AggshkMnzwgx/kuuuu46abbqJcLnPBBRfwiU98gq1bt3LWWWdx/PHHs2nTJm699VZuuummpv0APv3pT3PjjTeyaNEiVq5cybHHHtvyPV122WXccMMNXHXVVXR3d9fd12rNEEQ1+Xye7du3s379eoaGhqhUKlx//fWccsop3HbbbXz84x+nXC5zwAEH8O1vf7spEhKExBJHLAsNSStBnhTmFOVMKpoNfiDrExPL9COOWBYAPM/jjjvu4LzzzgPgXe96F1/5ylfYtGkTn/vc53jve98LwBVXXMFpp53Ggw8+yAMPPMDhhx/Opk2b+Pa3v819993Hvffeyze+8Q3+8Ic/sH79em666abqa9x0002sX7+e2267jS1btvD73/+ezZs3s2nTJu6++24AtmzZwnvf+14effRRnnjiiZb7bdq0ie9///ts3ryZW2+9lfvvv7/t+8pkMlx22WV86Utfqtvebs1RfO973+Oss85i8+bNPPjgg6xdu5bdu3dzzTXXcPvtt/PAAw+wbt06vvCFLzS/sOrJWEjOgTEChIOBUyUWTw8K9iWZqBN2jDE9iCOWcWAikcV0olgssnbtWl544QUOPfRQzjzzTPL5PPfccw8XXnhhdb9yuQzAnXfeyXe+8x0gqM9ks1l+85vfcMEFF5BOBwXKt7zlLfz617/miiuuYOfOnbz44ovs2rWLrq4uVq5cyZe+9CVuu+02jj76aADy+Txbtmxhn332Yd999+WEE04A4Lbbbmu53/DwMBdccAGpVOB5EZJhO1xxxRWsXbuWD37wg9Vt7dYcvhbAq171Ki677DJc1+X8889n7dq1/OpXv+KPf/wjJ598MgCO43DiiSe2eFXFLDGxLDgkFbFM1e6gYiShDCUSdBhxxDLdiIllHiOssRQKBc466yy++tWvcumll5LL5di8efOUn//CCy/k5ptv5qWXXmL9+vVAUD/58Ic/zLvf/e66fbdu3Vo90Y+23xe/+MUJrSGXy/H2t799wvWjU089lbvvvpuf/vSnXHrppVx55ZV0dXVx5plnsmHDhtEfHNZYFpBceowA1YjFmNoogFQeLCVM+uKIZdoRf6ILAKlUii9/+ct8/vOfJ5VKsWrVKn7wgx8AwQn+wQcfBOCMM87g+uuvB4L02eDgIKeccgo//OEPKRQKjIyMcMstt3DKKacAsH79er7//e9z8803VyOgs846i29961vk84HzwAsvvFDXuRWi3X6nnnoqP/zhDykWiwwPD/OTn/xkzPd35ZVX8rWvfY1KpQIw6ppDPPvssyxevJh3vvOdXH755TzwwAOccMIJ/Pa3v+XJJ58EYGRkhD/96U9NrxfWWERcY1lwWNGVoitlTjliCYklrrHMDOKIZYHg6KOP5qijjmLDhg1897vf5a//+q+55pprcF2Xiy++mDVr1vClL32Jd73rXXzzm99E13Wuv/56TjzxRC699FKOO+44ICiEhymlww8/nOHhYZYvX87SpUH74utf/3oee+yxagopk8nwH//xH+h6/Y+v3X7HHHMM69evZ82aNSxatIhXvepVY7633t5eLrjgAv7lX/4FgGOOOabtmkP88pe/5LrrrsM0TTKZDN/5znfo6+vjhhtu4G1ve1s1PXjNNdewevXq+hcMB1/jiGXB4aJ1KznnqKVT9iLxzSBVW5JmVYo+xvRBSBkL+65bt05u3Lixbttjjz3GoYceOkcrijGTePDBzay55TR+ddgnOe2iv53r5cSYA9zwzX/l0uc/wp3+Mbz2k3fN9XIWLIQQm6SU6xq3x1Qd4xUHITRcqeOmWtn9xHhFIBHUC91YfWFGEBNLjFceNJ2Tyl9h7+LpmOKPsRChJYJUWCxEOjOIiSXGKw5CwC5yZFNx8f6VCt0KiCWW9ZkZxMX7GK84GJrGf/31SaxZkZ3rpcSYI2hWoMZQiVNhM4KYWGK8InHsvl1j7xTjZQvTDiIWP9aLmxHEqbAYMWK84mDYQcTixcQyI4iJZZ7jhz/8IUIIHn/88TH3/eIXv0ihUJj0a91www38zd/8TcvtmqZVRTABjjjiCLZu3Trq873xjW9ckFL/MV7+MFKd3OcfwnP2wXO9lJclYmKZ59iwYQOvfvWrx5YpYerEMhpWrFjBpz/96Qk95tZbbyWXy83IemLEmApSVoL1zsd4oqOVllyMqSKusYwH//dD8NLD0/ucS46EN1w76i75fJ7f/OY33HXXXZx77rlVWXrP87jqqqv42c9+hqZpvPOd70RKyYsvvsjpp59Ob28vd911V1ViHuDmm2/mf/7nf7jhhhv4yU9+wjXXXIPjOPT09PDd736XxYtHn+k455xzuPvuu3niiSc4+OD6q7wNGzbwmc98Biklb3rTm/jsZz8LwH777cfGjRtJJpNcdNFFbNu2Dc/z+Md//EfWr1/Ppk2buPLKK8nn8/T29nLDDTdUFQBixJhJhDIusbLxzCD+VOcxfvSjH3H22WezevVqenp62LRpEwBf//rX2bp1K5s3b+ahhx7iz//8z7niiitYtmwZd911F3fdNfok8atf/Wruvfde/vCHP3DxxRfzz//8z2OuRdM0/uEf/oHPfOYzddtffPFFrrrqKu688042b97M/fffzw9/+MO6fX72s5+xbNkyHnzwQR555BHOPvtsXNfl/e9/PzfffDObNm3isssu4yMf+cgEP6EYMSaHZJVYYp2wmUAcsYwHY0QWM4UNGzbwt38bSI5cfPHFbNiwgWOPPZbbb7+d97znPRhG8PU1mmSNhW3btrF+/Xq2b9+O4zisWrVqXI97+9vfzqc//WmeeeaZ6rb777+f17zmNfT19QHw53/+59x9992cf/751X2OPPJIPvCBD3DVVVdxzjnncMopp/DII4/wyCOPcOaZZwJBFBZHKzFmC6lE8NuJI5aZQUws8xT9/f3ceeedPPzwwwgh8DwPIQTXXXfduJ9DiJq0eKlUqv79/ve/nyuvvJLzzjuPX/7yl1x99dXjej7DMPjABz5QTXWNF6tXr+aBBx7g1ltv5aMf/ShnnHEGF1xwAYcffji/+93vJvRcMWJMB6oRS+zFMiOI6Xqe4uabb+Yv//IvefbZZ9m6dSvPP/88q1at4te//jVnnnlmncx8f38/AB0dHQwPD1efY/HixTz22GP4vs8tt9xS3T44OMjy5csBuPHGGye0rksvvZTbb7+dXbt2AXDcccfxq1/9it27d+N5Hhs2bOC0006re8yLL75IKpXiL/7iL/j7v/97HnjgAQ4++GB27dpVJRbXdXn00Ucn+CnFiDE52Ing1GfFEcuMIP5U5yk2bNjABRdcULftrW99Kxs2bODyyy9nn3324aijjmLNmjV873vfAwLL4rPPPpvTTz8dgGuvvZZzzjmHk046qS7NdPXVV3PhhRdy7LHH0tvbO6F1JRKJqvskwNKlS7n22ms5/fTTWbNmDcceeyxvfvOb6x7z8MMPc9xxx7F27Vo+8YlP8NGPfpREIsHNN9/MVVddxZo1a1i7di333HPPhD+nGDEmg2oqLI5YZgTzSjZfCNEN/CewH7AVuEhKubfFfpcAH1U3r5FS3qi2J4B/BV4D+MBHpJT/NdbrxrL5ryzE320MgK/e9SRvOGIJ+/dl5nopCxYLRTb/Q8AdUsqDgDvU7Too8vk4cDxwHPBxIUSoz/ERYKeUcjVwGPCrWVl1jBgxFhzed/qBManMEOYbsbwZCJP+NwLnt9jnLOAXUsp+Fc38Ajhb3XcZ8E8AUkpfSrl7htcbI0aMGDEaMN+IZbGUcrv6+yWg1dTecuD5yO1twHIhRDji/SkhxANCiB8IIdpO/Qkh3iWE2CiE2BgWohsxn9KEMaYH8XcaI8bMY9aJRQhxuxDikRb/6iq+MjgDTOQsYAArgHuklMcAvwM+125nKeXXpZTrpJTrwhmMKGzbZs+ePfGJ6GUEKSV79uzBtmPhwRgxZhKzPscipXxdu/uEEDuEEEullNuFEEuBnS12e4GgOB9iBfBLYA9QAP5bbf8B8FeTXeeKFSvYtm0b7aKZGAsTtm2zYsWKuV5GjBgva8y3AckfA5cA16r/f9Rin58Dn4kU7F8PfFhKKYUQPyEgnTuBM4A/TnYhpmmOeyI9RowYMWLUMN9qLNcCZwohtgCvU7cRQqwTQvw7gJSyH/gUcL/690m1DeAq4GohxEPAXwIfmOX1x4gRI8YrHvNqjmWu0GqOJUaMGDFijI6FMscSI0aMGDEWOOKIBRBC7AKeneTDe4H5OC8Tr2tiiNc1McTrmhheruvaV0rZ1FYbE8sUIYTY2CoUnGvE65oY4nVNDPG6JoZX2rriVFiMGDFixJhWxMQSI0aMGDGmFTGxTB1fn+sFtEG8rokhXtfEEK9rYnhFrSuuscSIESNGjGlFHLHEiBEjRoxpRUwsMWLEiBFjWhETyzgghLhQCPGoEMIXQqxruO/DQognhRBPCCHOavP4VUKI+9R+/6mcLqd7jf8phNis/m0VQmxus99WIcTDar8ZlxsQQlwthHghsrY3ttnvbPUZPimEaDJ4m4F1XSeEeFwI8ZAQ4paI7ULjfrPyeY31/oUQlvqOn1TH0n4ztZbIa64UQtwlhPijOv7/tsU+rxFCDEa+34/N9LrU6476vYgAX1af10NCiGNmYU0HRz6HzUKIISHE3zXsMyuflxDiW0KInUKIRyLbuoUQvxBCbFH/d7V57CVqny0icOudOKSU8b8x/gGHAgcTqCivi2w/DHgQsIBVwFOA3uLxNwEXq7//DfjrGV7v54GPtblvK9A7i5/d1cAHx9hHV5/d/kBCfaaHzfC6Xg8Y6u/PAp+dq89rPO8feC/wb+rvi4H/nIXvbilwjPq7A/hTi3W9Bvif2Tqexvu9AG8E/i8ggBOA+2Z5fTqBp9S+c/F5AacCxwCPRLb9M/Ah9feHWh3zQDfwtPq/S/3dNdHXjyOWcUBK+ZiU8okWd70Z+L6UsiylfAZ4ksAuuQohhABeC9ysNrVzxpwWqNe7CNgwU68xAzgOeFJK+bSU0gG+T/DZzhiklLdJKSvq5r0E9gtzhfG8/6i76s3AGeq7njFIKbdLKR9Qfw8DjxEY7S0EvBn4jgxwL5BTVhyzhTOAp6SUk1X0mBKklHcD/Q2bp+rQO278v/buJrSOKgzj+P+B+gGt1I8iNejCSlfqwk9UKghKaYpUFNGKIFpRKnShIoJk51434gfWiiBdiVaCRNSqaxWLaRQrjbsEiaJYkUJReF2cc2W8zI0T75mZW3l+cEkyc3Lve985mXfmzOQeF5bx1M5mOdTmAuDXyk6srk1JNwMrEXF8xPoAPpT0paRHW4yjal8ejnh9xOl3kzy2aQ/p6LZOF/lq8v7/bpP70glS3+pEHnq7CvisZvWNkuYlvS/p8o5C+rft0nef2s3og7s+8gVjzNC71heatPlYeiPpMLC5ZtVMRNTNC9O5hjHex+pnK9siYlnShcBHko7lo5tW4gJeJk1zEPnrc6Qdeeua5EvSDPAncHDE0xTP1+lG0gbgbeDxiPhtaPUR0nDP7/n62bvA1g7Cmtjtkq+h7gKeqVndV77+ISJCUmv/a+LCksUqM1uuYhm4pPLzxXlZ1c+k0/B1+Uizrk2RGCWtA+4CrlnlOZbz1x8lHSINw4z1B9k0d5L2A+/VrGqSx+JxSXoQuB24NfIAc81zFM9XjSbvf9BmKW/njaS+1SpJZ5CKysGIeGd4fbXQRMScpJckbYqIVj9wscF2aaVPNTQNHImIleEVfeUrG2eG3jXxUNh4ZoHd+Y6dS0lHHp9XG+Qd1qfA3XnRqJkxS7gNOBYRS3UrJa2XdM7ge9IF7K/r2pYyNK5954jX+wLYqnT33JmkYYTZluPaATwN7IqIkyPadJWvJu9/MLsqpL70yahiWEq+hnMA+DYinh/RZvPgWo+k60n7lFYLXsPtMgs8kO8OuwE4URkGatvIUYM+8lVR7UOrzdC7XdJ5edh6e162Nm3fnfB/eJB2iEvAKWAF+KCyboZ0R893wHRl+Rwwlb/fQio4i8BbwFktxfkGsHdo2RQwV4ljPj++IQ0JtZ27N4EF4Gju2BcNx5V/3km66+j7juJaJI0lf5UfrwzH1WW+6t4/8Cyp8AGcnfvOYu5LWzrI0TbSEObRSp52AnsH/QzYl3MzT7oJ4qYO4qrdLkNxCXgx53OByt2cLce2nlQoNlaWdZ4vUmH7Afgj77seJl2T+xg4DhwGzs9trwVeq/zuntzPFoGH/svr+yNdzMysKA+FmZlZUS4sZmZWlAuLmZkV5cJiZmZFubCYmVlRLixmZlaUC4uZmRXlwmJmZkW5sJhNGEmXSfplMDmVpClJP0m6pefQzBrxf96bTSBJjwBPkD5u4xCwEBFP9RuVWTMuLGYTStIsaWbSAK6LiFM9h2TWiIfCzCbXfuAK4AUXFTud+IzFbALlybXmSVMuTANXRsTwVLNmE8mFxWwCSToAbIiIeyW9CpwbEff0HZdZEx4KM5swku4AdgCP5UVPAldLur+/qMya8xmLmZkV5TMWMzMryoXFzMyKcmExM7OiXFjMzKwoFxYzMyvKhcXMzIpyYTEzs6JcWMzMrKi/AN/a4T7Rcf0VAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(x, y- res.y_fit, label='Recovered Noise')\n", - "plt.plot(x, noise, label='Actual Noise')\n", - "plt.ylabel(r'$\\delta y$', fontsize=12)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To illustrate what happens when there is an inflection point in the data we can define some sinusoidal data as so." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "x = np.linspace(1, 5, 100)\n", - "noise = np.random.normal(0, 0.02, 100)\n", - "y = np.sin(x) + noise\n", - "\n", - "N = 10\n", - "\n", - "plt.plot(x, y)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel('y', fontsize=12)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we proceed to fit this with smooth() in its default settings we will get a poor fit as by default the second derivative is constrained. We need to lift this constraint to allow for the prominent inflection point to be modelled. We do this by setting the keyword argument constraints=3 creating a Partially Smooth Function\n", - "or PSF.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.616485357284546\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 8\n", - "Signs : [ 1 -1 1 -1 1 -1 1 -1]\n", - "Objective Function Value: 1.2278492797034808\n", - "Parameters: [[ 1.34984313e-01 -7.37897372e-01 -1.35984051e-02 1.60265401e-02\n", - " -1.21425226e-02 6.13320573e-03 -2.06526186e-03 4.47071803e-04\n", - " -5.64542010e-05 3.16834713e-06]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 2\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 0, '1': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n", - "#############################################################\n", - "#############################################################\n", - "----------------------OPTIMUM RESULT-------------------------\n", - "Time: 1.062380313873291\n", - "Polynomial Order: 10\n", - "Number of Constrained Derivatives: 7\n", - "Signs : [-1 -1 1 -1 1 -1 1]\n", - "Objective Function Value: 0.02853207306425321\n", - "Parameters: [[ 1.16343093e-01 -9.58117300e-01 -4.87723262e-02 1.31696692e-01\n", - " 1.23367766e-03 -6.19895817e-04 2.08513389e-04 -4.51222389e-05\n", - " 5.69694366e-06 -3.19689176e-07]]\n", - "Method: qp-sign_flipping\n", - "Model: difference_polynomial\n", - "Constraints: m >= 3\n", - "Zero Crossings Used? (0 signifies Yes\n", - " in derivative order \"i\"): {'0': 0, '1': 0, '2': 0}\n", - "-------------------------------------------------------------\n", - "#############################################################\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "res_msf = smooth(x, y, N)\n", - "res_psf = smooth(x, y, N, constraints=3)\n", - "\n", - "plt.plot(x, y, label='Data')\n", - "plt.plot(x, res_msf.y_fit, label=r'MSF fit, $m \\geq 2$')\n", - "plt.plot(x, res_psf.y_fit, label=r'PSF fit, $m \\geq 3$')\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.ylabel('y', fontsize=12)\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we can plot the residuals to further see that by lifting the constraint on the second derivative we have allowed an inflection point in the data." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZYAAAEJCAYAAAC3yAEAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOy9eZgc1X3u/zld1d3Vy+warSMhYRAGBBKrwWExISzesLHDBdtZ+NkOTrwlIXawr30dk+DENjEGfIlXvMW2wFYCXiA2ZgcDYl8khJCQhDRaZp/eu6u66vz+OFXd1TM9Mz2bZqRb7/PMM91V1dWnu6vOe97vKqSUBAgQIECAADOF0FwPIECAAAECHF4IiCVAgAABAswoAmIJECBAgAAzioBYAgQIECDAjCIglgABAgQIMKPQ53oA8wELFiyQK1eunOthBAgQIMAhhWeeeaZfStk5cntALMDKlSt5+umn53oYAQIECHBIQQjxer3tgSksQIAAAQLMKAJiCRAgQIAAM4qAWAIECBAgwIwi8LEECBBgRmBZFt3d3RSLxbkeSoAZhmEYdHV1EQ6HGzo+IJYAAQLMCLq7u2lqamLlypUIIeZ6OAFmCFJKBgYG6O7uZtWqVQ29JjCFBQgQYEZQLBbp6OgISOUwgxCCjo6OSSnRgFgCBAgwYwhI5fDEZH/XgFgCVDG0C7bfO9ejCBAgwCGOgFgCVPHEt+C/PjzXowgQYMrQNI1169axZs0a3vnOdzI8PDzXQ5o0vvjFL/Lv//7vdbfH43F6e3sr25LJ5ITne/Ob3zyj42sEAbEEqKJcAKsw16MIEGDKiMViPP/882zatIn29nZuueWWuR4SoBzgjuNM+zwLFizga1/72qRe89hjj037fSeLeUksQoiLhRBbhRDbhRCfqbM/KoS43d2/UQix0rfvRCHE40KIzUKIl4QQxsEc+yGNsgm2OdejCBBgRnDmmWeyd+9eAF577TUuvvhiTjnlFM4++2xeeeUVAHp6erj00ktZu3Yta9eurUzCN9xwA2vWrGHNmjXceOONAHzmM5+pISq/srj++us57bTTOPHEE/mnf/onAHbt2sUxxxzDX/zFX7BmzRr27NlT9ziAL33pS6xevZqzzjqLrVu3jvmZPvjBD3L77bczODg4al+9MUNV1ezfv59zzjmnougeeeQRAO655x7OPPNMTj75ZC677DKy2ewkv+nRmHfhxkIIDbgFuADoBp4SQvxKSvmy77APAUNSyqOEEFcAXwEuF0LowE+AP5dSviCE6ACsg/wRDl3YJkgH7DJo8+7SCHAI4dpfb+blfekZPedxS5v5p3ce39Cxtm1z33338aEPfQiAq666im9961scffTRbNy4kY9+9KPcf//9fPKTn+Tcc8/ljjvuwLZtstkszzzzDD/4wQ/YuHEjUkre9KY3ce6553L55Zfzd3/3d3zsYx8D4Oc//zm/+93vuOeee9i2bRtPPvkkUkouueQSHn74YVasWMG2bdv40Y9+xBlnnDHmcYlEgttuu43nn3+ecrnMySefzCmnnFL3cyWTST74wQ9y0003ce2111a2jzXmk046qXLMz372My666CI+97nPYds2+Xye/v5+rrvuOu69914SiQRf+cpXuOGGG/jCF74w1Z8JmIfEApwObJdS7gAQQtwGvAvwE8u7gC+6jzcA/1eosIULgRellC8ASCkHDtagDwt4asU2A2IJcEiiUCiwbt069u7dy7HHHssFF1xANpvlscce47LLLqscVyqVALj//vv58Y9/DCj/TEtLC48++iiXXnopiUQCgPe85z088sgjfPKTn6S3t5d9+/bR19dHW1sby5cv56abbuKee+6pTOLZbJZt27axYsUKjjjiCM444wxAKYN6x2UyGS699FLi8TgAl1xyybif8ZOf/CTr1q3jU5/6VGXbWGP2E8tpp53GBz/4QSzL4t3vfjfr1q3joYce4uWXX+aP/uiPADBNkzPPPHOK334V83H2WAbs8T3vBt401jFSyrIQIgV0AKsBKYT4HdAJ3Cal/Gq9NxFCXAVcBbBixYoZ/QCHLGxX3NklID6nQwlwaKNRZTHT8Hws+Xyeiy66iFtuuYUrr7yS1tZWnn/++Wmf/7LLLmPDhg0cOHCAyy+/HFD+k89+9rN85CMfqTl2165dlYl+vOP8ZqtG0Nrayvvf//5J+4/OOeccHn74Ye666y6uvPJKrr76atra2rjgggtYv379pM41Eealj2Ua0IGzgA+4/y8VQpxf70Ap5XeklKdKKU/t7BzVTuD/TVQUS2A9DHBoIx6Pc/PNN/O1r32NeDzOqlWr+MUvfgGoCf6FF14A4Pzzz+eb3/wmoMxnqVSKs88+mzvvvJN8Pk8ul+OOO+7g7LPPBuDyyy/ntttuY8OGDRUFdNFFF/H973+/4pvYu3dvTeSWh7GOO+ecc7jzzjspFApkMhl+/etfT/j5rr76ar797W9TLpcBxh2zh9dff51FixbxV3/1V3z4wx/m2Wef5YwzzuAPf/gD27dvByCXy/Hqq69O7suug/moWPYCy33Pu9xt9Y7pdv0qLcAASt08LKXsBxBC3A2cDNw324M+LOARS7k0t+MIEGAGcNJJJ3HiiSeyfv16fvrTn/I3f/M3XHfddViWxRVXXMHatWu56aabuOqqq7j11lvRNI1vfvObnHnmmVx55ZWcfvrpAHz4wx+umJSOP/54MpkMy5YtY8mSJQBceOGFbNmypWJCSiaT/OQnP0HTtJrxjHXcySefzOWXX87atWtZuHAhp5122oSfbcGCBVx66aV8/etfB+Dkk08ec8weHnzwQa6//nrC4TDJZJIf//jHdHZ28sMf/pD3ve99FfPgddddx+rVq6f0nXsQUsppnWCm4RLFq8D5KAJ5Cni/lHKz75iPASdIKf/add6/R0r5v4QQbSgSOQswgd8CX5dS3jXee5566qkyaPQF3Hoh7NkIn3gWOt4w16MJcIhhy5YtHHvssXM9jACzhHq/rxDiGSnlqSOPnXeKxfWZfBz4HaAB35dSbhZC/DPwtJTyV8CtwH8KIbYDg8AV7muHhBA3oMhIAndPRCoBfPA77wMECBBgiph3xAIgpbwbuHvEti/4HheBy0a+zt33E1TIcYDJouK8D4glQIAAU8fh5rwPMB1UfCwBsQQIEGDqCIglQBWBKSxAgAAzgIBYAlRRk8cSIECAAFNDQCwBqvDCjIM8lgABAkwDAbEEqMIjlCCPJcAhjDvvvBMhRKXQ5Hi48cYbyefzU36vH/7wh3z84x+vuz0UCvHiiy9Wtq1Zs4Zdu3aNe763ve1th2Sp/5EIiCVABTLwsQQ4DLB+/XrOOuushsqUTJdYxkNXVxdf+tKXJvWau+++m9bW1lkZz8FEQCwBKpDlgFgCHNrIZrM8+uij3Hrrrdx2222V7bZt86lPfYo1a9Zw4okn8o1vfIObb76Zffv2cd5553HeeecBtY2zNmzYwJVXXgnAr3/9a970pjdx0kkn8Sd/8if09PRMOJZ3vOMdbN68uW4Z/PXr13PCCSewZs0arrnmmsr2lStX0t/fTy6X4+1vfztr165lzZo13H777YCqYnzuuedyyimncNFFF7F///4pfU+zjXmZxxJgDuDYhLDV44BYAkwX//MZOPDSzJ5z8Qnw1i+Pe8gvf/lLLr74YlavXk1HRwfPPPMMp5xyCt/5znfYtWsXzz//PLquMzg4SHt7OzfccAMPPPAACxYsGPe8Z511Fk888QRCCL73ve/x1a9+dcKGW6FQiH/8x3/kX//1X/nRj35U2b5v3z6uueYannnmGdra2rjwwgu58847efe731055re//S1Lly7lrrtUfncqlcKyLD7xiU/wy1/+ks7OTm6//XY+97nP8f3vf3+ib+6gI1AsART8DvsgjyXAIYr169dzxRVXAHDFFVdUzGH33nsvH/nIR9B1tZZub2+f1Hm7u7u56KKLOOGEE7j++uvZvHnzxC8C3v/+9/PEE0+wc+fOyrannnqKt7zlLXR2dqLrOh/4wAd4+OGHa153wgkn8Pvf/55rrrmGRx55hJaWFrZu3cqmTZu44IILWLduHddddx3d3d2T+hwHC4FiCaDgCzG2yyW0cQ4NEGBCTKAsZgODg4Pcf//9vPTSSwghsG0bIQTXX399w+dQbZ0UisVi5fEnPvEJrr76ai655BIefPBBvvjFLzZ0Pl3X+Yd/+Ae+8pWvNDwGgNWrV/Pss89y99138/nPf57zzz+fSy+9lOOPP57HH398UueaCwSKJYCCT7HYVnGcAwMEmJ/YsGEDf/7nf87rr7/Orl272LNnD6tWreKRRx7hggsuqCkz77X2bWpqIpPJVM6xaNEitmzZguM43HHHHZXtqVSKZcuWAdSYtRrBlVdeyb333ktfXx8Ap59+Og899BD9/f3Yts369es599xza16zb98+4vE4f/Znf8anP/1pnn32WY455hj6+voqxGJZVsPK6WAjIJYACj6/imMFprAAhx7Wr1/PpZdeWrPtve99L+vXr+fDH/4wK1as4MQTT2Tt2rX87Gc/A1TL4osvvrjivP/yl7/MO97xDt785jdXyuKD6m9/2WWXccopp0zojxmJSCRS6T4JsGTJEr785S9z3nnnsXbtWk455RTe9a531bzmpZde4vTTT2fdunVce+21fP7znycSibBhwwauueYa1q5dy7p163jssccm/T0dDMy7svlzgaBsPjC0C25aC0Du9E+SeNu/zO14AhxyCMrmH96YTNn8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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(x, y - res_psf.y_fit, label='Recovered Noise')\n", - "plt.plot(x, noise, label='Actual Noise')\n", - "plt.ylabel(r'$\\delta y$', fontsize=12)\n", - "plt.xlabel('x', fontsize=12)\n", - "plt.legend()\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/maxsmooth/DCF.py b/maxsmooth/DCF.py deleted file mode 100755 index c38a1d4..0000000 --- a/maxsmooth/DCF.py +++ /dev/null @@ -1,843 +0,0 @@ - -""" -*smooth*, as demonstrated in the examples section, -is used to call the fitting routine. There are a number -of :math:`{^{**}}` kwargs that can be assigned to the function which change how -the fit is performed, the model that is fit and various other attributes. -These are detailed below. - -""" - -from maxsmooth.qp import qp_class -from maxsmooth.Models import Models_class -from maxsmooth.derivatives import derivative_class -from maxsmooth.Data_save import save, save_optimum -from itertools import product -import warnings -import numpy as np -import time -import os -import shutil - - -class smooth(object): - - r""" - - **Parameters:** - - x: **numpy.array** - | The x data points for the set being fitted. - - y: **numpy.array** - | The y data points for fitting. - - N: **int** - | The number of terms in the DCF. - - **Kwargs:** - - fit_type: **Default = 'qp-sign_flipping'** - | This kwarg allows the user to - switch between sampling the available discrete sign spaces - (default) or testing all sign combinations on the derivatives - which can be accessed by setting to 'qp'. - - model_type: **Default = 'difference_polynomial'** - | Allows the user to - access default Derivative Constrained Functions built into the - software. Available options include the default, 'polynomial', - 'normalised_polynomial', 'legendre', 'log_polynomial', - 'loglog_polynomial' and 'exponential'. For more details on the - functional form of the built in basis see the ``maxsmooth`` - paper. - - **pivot_point: Default = len(x)//2 otherwise an integer between** - **-len(x) and len(x)** - | Some of the built in - models rely on pivot points in the data sets which by defualt - is set as the middle index. This can be altered via - this kwarg which can occasionally lead to a better quality fit. - - base_dir: **Default = 'Fitted_Output/'** - | The location of the outputted - data from ``maxsmooth``. This must be a string and end in '/'. - If the file does not exist then ``maxsmooth`` will create it. - By default the only outputted data is a summary of the best - fit but additional data can be recorded by setting the keyword - argument 'data_save = True'. - - data_save: **Default = False** - | By setting this to True the algorithm - will save every tested set of parameters, signs and objective - function evaluations into files in base_dir. Theses files will - be over written on repeated runs but they are needed to run the - 'chidist_plotter'. - - print_output: **Default = 1** - | The parameter can take a value of 0, 1, 2. - If set to 2 this outputs to the - terminal every fit performed by the algorithm. By default the - parameter has a value of 1 and the - only output is the optimal solution once the code is finished. - Setting this to 0 will prevent any output to the terminal. This - is useful when running the code inside a nested sampling loop - for example. - - cvxopt_maxiter: **Default = 10000 else integer** - | This shouldn't need - changing for most problems however if ``CVXOPT`` fails with a - 'maxiters reached' error message this can be increased. - Doing so arbitrarily will however increase the run time of - ``maxsmooth``. - - initial_params: **Default = None else list of length N** - | Allows the user - to overwrite the default initial parameters used by ``CVXOPT``. - - **constraints: Default = 2 else an integer less than or equal** - **to N - 1** - | The minimum constrained derivative order which is set by default - to 2 for a Maximally Smooth Function. - - zero_crossings: **Default = None else list of integers** - | Allows you to - specify if the conditions should be relaxed on any - of the derivatives between constraints and the highest order - derivative. e.g. a 6th order fit with just a constrained 2nd - and 3rd order derivative would have zero_crossings = [4, 5]. - - cap: **Default = (len(available_signs)//N) + N else an integer** - | Determines the maximum number of signs explored either side of - the minimum :math:`{\chi^2}` value found after the decent - algorithm has terminated. - - chi_squared_limit: **Default = 2 else float or int** - | The prefactor on the maximum allowed increase in :math:`{\chi^2}` - during the directional exploration which is defaulted at 2. - If this value multiplied by the minimum :math:`{\chi^2}` - value found after the descent algorithm is exceeded then the - exploration in one direction is stopped and started in the - other. For more details on this and 'cap' see the ``maxsmooth`` - paper. - - The following Kwargs can be used by the user to define their own basis - function and will overwrite the 'model_type' kwarg. - - **basis_function: Default = None else function with parameters** - **(x, y, pivot_point, N)** - | This is a function of basis functions - for the quadratic programming. The variable pivot_point is the - index at the middle of the datasets x and y by default but can - be adjusted. - - **model: Default = None else function with parameters** - **(x, y, pivot_point, N, params)** - | This is - a user defined function describing the model to be fitted to - the data. - - **der_pres: Default = None else function with parameters** - **(m, x, y, N, pivot_point)** - | This function describes the prefactors on the - mth order derivative used in defining the constraint. - - **derivatives: Default = None else function with parameters** - **(m, x, y, N, pivot_point, params)** - | User defined function describing the mth - order derivative used to check that conditions are being met. - - **args: Default = None else list** - | Extra arguments for `smooth` - to pass to the functions detailed above. - - **Output** - - .y_fit: **numpy.array** - | The fitted array of y data from smooth(). - - .optimum_chi: **float** - | The optimum :math:`{\chi^2}` value for the fit calculated by, - - .. math:: - - {X^2=\sum(y-y_{fit})^2}. - - .optimum_params: **numpy.array** - | The set of parameters corresponding to the optimum fit. - - .rms: **float** - | The rms value of the residuals :math:`{y_{res}=y-y_{fit}}` - calculated by, - - .. math:: - - {rms=\sqrt{\frac{\sum(y-y_{fit})^2}{n}}} - - where :math:`n` is the number of data points. - - .derivatives: **numpy.array** - | The :math:`m^{th}` order derivatives. - - .optimum_signs: **numpy.array** - | The sign combinations corresponding to the - optimal result. The nature of the constraint means that a - negative ``maxsmooth`` sign implies a positive :math:`{m^{th}}` - order derivative and visa versa. - - """ - - def __init__(self, x, y, N, **kwargs): - self.x = x - self.y = y - - self.N = N - if self.N % 1 != 0: - raise ValueError('N must be an integer or whole number float.') - - for keys, values in kwargs.items(): - if keys not in set( - ['fit_type', 'model_type', 'base_dir', - 'print_output', 'cvxopt_maxiter', 'zero_crossings', - 'data_save', - 'constraints', 'chi_squared_limit', 'cap', - 'initial_params', 'basis_functions', - 'der_pres', 'model', - 'derivatives', 'args', 'pivot_point']): - raise KeyError("Unexpected keyword argument in smooth().") - - self.fit_type = kwargs.pop('fit_type', 'qp-sign_flipping') - if self.fit_type not in set(['qp', 'qp-sign_flipping']): - raise KeyError( - "Invalid 'fit_type'. Valid entries include 'qp'\n" + - "'qp-sign_flipping'") - - self.pivot_point = kwargs.pop('pivot_point', len(self.x)//2) - if type(self.pivot_point) is not int: - raise TypeError('Pivot point is not an integer index.') - elif self.pivot_point >= len(self.x) or \ - self.pivot_point < -len(self.x): - raise ValueError( - 'Pivot point must be in the range -len(x) - len(x).') - - self.base_dir = kwargs.pop('base_dir', 'Fitted_Output/') - if type(self.base_dir) is not str: - raise KeyError("'base_dir' must be a string ending in '/'.") - elif self.base_dir.endswith('/') is False: - raise KeyError("'base_dir' must end in '/'.") - - self.model_type = kwargs.pop('model_type', 'difference_polynomial') - if self.model_type not in set( - ['normalised_polynomial', 'polynomial', - 'log_polynomial', 'loglog_polynomial', - 'difference_polynomial', - 'exponential', 'legendre']): - raise KeyError( - "Invalid 'model_type'. See documentation for built" + - "in models.") - - self.cvxopt_maxiter = kwargs.pop('cvxopt_maxiter', 10000) - if type(self.cvxopt_maxiter) is not int: - raise ValueError("'cvxopt_maxiter' is not integer.") - - self.print_output = kwargs.pop('print_output', 1) - if self.print_output not in [0, 1, 2]: - raise ValueError("'print_output' must have a value in the set" + - " [0, 1, 2]. See documentation for details.") - - self.data_save = kwargs.pop('data_save', False) - if type(self.data_save) is not bool: - raise TypeError( - "Boolean keyword argument with value 'data_save'" + - " is not True or False.") - - self.constraints = kwargs.pop('constraints', 2) - if type(self.constraints) is not int: - raise TypeError("'constraints' is not an integer") - if self.constraints > self.N-1: - raise ValueError( - "'constraints' exceeds the number of derivatives.") - - self.zero_crossings = kwargs.pop('zero_crossings', None) - if self.zero_crossings is not None: - for i in range(len(self.zero_crossings)): - if type(self.zero_crossings[i]) is not int: - raise TypeError( - "Entries in 'zero_crossings'" + - " are not integer.") - if self.zero_crossings[i] < self.constraints: - raise ValueError( - 'One or more specified derivatives for' + - ' zero crossings is less than the minimum' + - ' constrained' + - ' derivative.\n zero_crossings = ' + - str(self.zero_crossings) - + '\n' + ' Minimum Constrained Derivative = ' - + str(self.constraints)) - - self.chi_squared_limit = kwargs.pop('chi_squared_limit', None) - self.cap = kwargs.pop('cap', None) - if self.chi_squared_limit is not None: - if isinstance(self.chi_squared_limit, int) or \ - isinstance(self.chi_squared_limit, float): - pass - else: - raise TypeError( - "Limit on maximum allowed increase in chi squared" + - ", 'chi_squared_limit', is not an integer or float.") - if self.cap is not None: - if type(self.cap) is not int: - raise TypeError( - "The cap on directional exploration" + - ", 'cap', is not an integer.") - - self.initial_params = kwargs.pop('initial_params', None) - if self.initial_params is not None and len(self.initial_params) \ - != self.N: - raise ValueError( - "Initial Parameters is not equal to the number" + - "of terms in the polynomial, N.") - if self.initial_params is not None and len(self.initial_params) \ - == self.N: - for i in range(len(self.initial_params)): - if type(self.initial_params[i]) is not int: - if type(self.initial_params[i]) is not float: - raise ValueError( - 'One or more initial' + - 'parameters is not numeric.') - - self.basis_functions = kwargs.pop('basis_functions', None) - self.der_pres = kwargs.pop('der_pres', None) - self.model = kwargs.pop('model', None) - self.derivatives_function = kwargs.pop('derivatives', None) - self.args = kwargs.pop('args', None) - - self.new_basis = { - 'basis_function': - self.basis_functions, 'der_pres': self.der_pres, - 'derivatives_function': self.derivatives_function, - 'model': self.model, 'args': self.args} - if np.all([value is None for value in self.new_basis.values()]): - pass - else: - count = 0 - for key, value in self.new_basis.items(): - if value is None and key != 'args': - raise KeyError( - 'Attempt to change basis functions failed.' + - ' One or more functions not defined.' + - ' Please consult documentation.') - if value is None and key == 'args': - warnings.warn( - 'Warning: No additional arguments passed to' + - ' new basis functions') - count += 1 - if count == len(self.new_basis): - self.model_type = 'user_defined' - - self.y_fit, self.optimum_signs, self.optimum_params, \ - self.derivatives,\ - self.optimum_chi, self.rms, self.optimum_zc_dict \ - = self.fitting() - - def fitting(self): - - def signs_array(nums): - return np.array(list(product(*((x, -x) for x in nums)))) - - if not os.path.exists(self.base_dir): - os.mkdir(self.base_dir) - - if os.path.isdir(self.base_dir+'Output_Parameters/'): - shutil.rmtree(self.base_dir+'Output_Parameters/') - if os.path.isdir(self.base_dir+'Output_Signs/'): - shutil.rmtree(self.base_dir+'Output_Signs/') - if os.path.isdir(self.base_dir+'Output_Evaluation/'): - shutil.rmtree(self.base_dir+'Output_Evaluation/') - - def qp(x, y, pivot_point): # Testing all signs - - start = time.time() - - if self.zero_crossings is not None: - signs = signs_array([1]*( - self.N-self.constraints-len(self.zero_crossings))) - else: - signs = signs_array([1]*(self.N-self.constraints)) - - params, chi_squared, zc_dict, passed_signs = [], [], [], [] - append_params, append_chi, append_zc_dict, append_passed_signs = \ - params.append, chi_squared.append, zc_dict.append, \ - passed_signs.append - for j in range(signs.shape[0]): - fit = qp_class( - x, y, self.N, signs[j, :], pivot_point, - self.model_type, self.cvxopt_maxiter, - self.zero_crossings, - self.initial_params, self.constraints, self.new_basis) - - if self.print_output == 2: - print('-'*50) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints-len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', signs[j, :]) - print('Objective Function Value:', fit.chi_squared) - print('Parameters:', (fit.parameters).T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - fit.zc_dict) - if self.zero_crossings is not None: - print( - 'Zero Crossing Derivatives:', self.zero_crossings) - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - fit.zc_dict) - print('-'*50) - - append_params(fit.parameters) - append_chi(fit.chi_squared) - append_zc_dict(fit.zc_dict) - append_passed_signs(signs[j, :]) - if self.data_save is True: - save( - self.base_dir, fit.parameters, fit.chi_squared, - signs[j, :], self.N, self.fit_type) - - params, chi_squared, zc_dict, passed_signs = np.array(params), \ - np.array(chi_squared), np.array(zc_dict), \ - np.array(passed_signs) - - Optimum_chi_squared = chi_squared.min() - for f in range(len(chi_squared)): - if chi_squared[f] == chi_squared.min(): - Optimum_params = params[f, :] - Optimum_sign_combination = passed_signs[f, :] - - y_fit = Models_class( - Optimum_params, x, y, self.N, pivot_point, - self.model_type, self.new_basis).y_sum - der = derivative_class( - x, y, Optimum_params, self.N, - pivot_point, self.model_type, self.zero_crossings, - self.constraints, self.new_basis) - derivatives, Optimum_zc_dict = der.derivatives, der.zc_dict - - end = time.time() - - if self.print_output in [1, 2]: - print('#'*50) - print('-'*20 + 'OPTIMUM RESULT' + '-'*20) - print('Time:', end-start) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints-len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', Optimum_sign_combination) - print('Objective Function Value:', Optimum_chi_squared) - print('Parameters:', Optimum_params.T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - Optimum_zc_dict) - if self.zero_crossings is not None: - print('Zero Crossing Derivatives:', self.zero_crossings) - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - Optimum_zc_dict) - print('-'*50) - print('#'*50) - - save_optimum( - self.base_dir, end-start, self.N, - Optimum_sign_combination, Optimum_chi_squared, - Optimum_params, self.fit_type, self.model_type, - self.zero_crossings, Optimum_zc_dict, self.constraints) - - return y_fit, derivatives, Optimum_chi_squared, Optimum_params, \ - Optimum_sign_combination, Optimum_zc_dict - - def qp_sign_flipping(x, y, pivot_point): # Sign Sampling - - start = time.time() - - if self.zero_crossings is not None: - array_signs = signs_array([1]*( - self.N-self.constraints-len(self.zero_crossings))) - else: - array_signs = signs_array([1]*(self.N-self.constraints)) - - r = np.random.randint(0, len(array_signs), 1) - signs = array_signs[r][0] - - tested_indices = [] - chi_squared = [] - parameters = [] - tested_signs = [] - for i in range(len(array_signs)): - if i == r: - tested_indices.append(i) - fit = qp_class( - x, y, self.N, signs, pivot_point, - self.model_type, self.cvxopt_maxiter, - self.zero_crossings, - self.initial_params, self.constraints, self.new_basis) - chi_squared.append(fit.chi_squared) - tested_signs.append(signs) - parameters.append(fit.parameters) - chi_squared_old = fit.chi_squared - previous_signs = signs - - if self.print_output == 2: - print('-'*50) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints-len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', signs) - print('Objective Function Value:', chi_squared_old) - print('Parameters:', (fit.parameters).T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - fit.zc_dict) - if self.zero_crossings is not None: - print( - 'Zero Crossing Derivatives:', - self.zero_crossings) - print( - 'Zero Crossings Used? (0 signifies' + - 'Yes\n in derivative order "i"):', fit.zc_dict) - print('-'*50) - if self.data_save is True: - save( - self.base_dir, fit.parameters, fit.chi_squared, - signs, self.N, self.fit_type) - - # Transforms or 'steps' of sign combination - sign_transform = [] - for i in range(len(signs)): - base = np.array([1]*len(signs)) - base[i] = -1 - sign_transform.append(base) - sign_transform = np.array(sign_transform) - chi_squared_new = 0 - - while chi_squared_new < chi_squared_old: - if chi_squared_new != 0: - chi_squared_old = chi_squared_new - for h in range(sign_transform.shape[0]): - signs = previous_signs * sign_transform[h] - for i in range(len(array_signs)): - if np.all(signs == array_signs[i]): - ind = i - if ind in set(tested_indices): - pass - else: - tested_indices.append(ind) - fit = qp_class( - x, y, self.N, signs, pivot_point, - self.model_type, self.cvxopt_maxiter, - self.zero_crossings, self.initial_params, - self.constraints, self.new_basis) - if fit.chi_squared < chi_squared_old: - chi_squared_new = fit.chi_squared - previous_signs = signs - chi_squared.append(fit.chi_squared) - tested_signs.append(signs) - parameters.append(fit.parameters) - - if self.print_output == 2: - print('-'*50) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N - self.constraints - - len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', signs) - print( - 'Objective Function Value:', - fit.chi_squared) - print('Parameters:', fit.parameters.T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative' + - ' order "i"):', - fit.zc_dict) - if self.zero_crossings is not None: - print( - 'Zero Crossing Derivatives:', - self.zero_crossings) - print( - 'Zero Crossings Used?' + - ' (0 signifies' + - 'Yes\n in derivative order "i"):', - fit.zc_dict) - print('-'*50) - if self.data_save is True: - save( - self.base_dir, fit.parameters, - fit.chi_squared, - signs, self.N, self.fit_type) - - break - if h == sign_transform.shape[0] - 1: - chi_squared_new = chi_squared_old - break - - if self.data_save is True: - np.save( - self.base_dir + str(self.N) + - '_'+self.fit_type+'_minimum_chi_post_descent.npy', - min(chi_squared)) - - if self.chi_squared_limit is not None: - lim = self.chi_squared_limit*min(chi_squared) - else: - lim = 2*min(chi_squared) - - if self.cap is not None: - cap = self.cap - else: - cap = (len(array_signs)//self.N) + self.N - - for i in range(len(array_signs)): - if np.all(previous_signs == array_signs[i]): - index = i - - down_int = 1 - while down_int < cap: - if index-down_int < 0: - break - elif (index-down_int) in set(tested_indices): - chi_down = 0 - pass - else: - signs = array_signs[index-down_int] - tested_indices.append(index - down_int) - fit = qp_class( - x, y, self.N, signs, pivot_point, - self.model_type, self.cvxopt_maxiter, - self.zero_crossings, self.initial_params, - self.constraints, self.new_basis) - chi_down = fit.chi_squared - chi_squared.append(fit.chi_squared) - tested_signs.append(signs) - parameters.append(fit.parameters) - - if self.print_output == 2: - print('-'*50) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints - - len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', signs) - print('Objective Function Value:', fit.chi_squared) - print('Parameters:', fit.parameters.T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative' + - ' order "i"):', - fit.zc_dict) - if self.zero_crossings is not None: - print( - 'Zero Crossing Derivatives:', - self.zero_crossings) - print( - 'Zero Crossings Used? (0 signifies' + - 'Yes\n in derivative order "i"):', - fit.zc_dict) - print('-'*50) - if self.data_save is True: - save( - self.base_dir, fit.parameters, fit.chi_squared, - signs, self.N, self.fit_type) - - if chi_down > lim: - break - down_int += 1 - - up_int = 1 - while up_int < cap: - if index+up_int >= len(array_signs): - break - elif (index + up_int) in set(tested_indices): - chi_up = 0 - pass - else: - signs = array_signs[index+up_int] - tested_indices.append(index + up_int) - fit = qp_class( - x, y, self.N, signs, pivot_point, - self.model_type, self.cvxopt_maxiter, - self.zero_crossings, self.initial_params, - self.constraints, self.new_basis) - chi_up = fit.chi_squared - chi_squared.append(fit.chi_squared) - tested_signs.append(signs) - parameters.append(fit.parameters) - - if self.print_output == 2: - print('-'*50) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints - - len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', signs) - print('Objective Function Value:', fit.chi_squared) - print('Parameters:', fit.parameters.T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative' + - ' order "i"):', - fit.zc_dict) - if self.zero_crossings is not None: - print( - 'Zero Crossing Derivatives:', - self.zero_crossings) - print( - 'Zero Crossings Used? (0 signifies' + - 'Yes\n in derivative order "i"):', - fit.zc_dict) - print('-'*50) - if self.data_save is True: - save( - self.base_dir, fit.parameters, fit.chi_squared, - signs, self.N, self.fit_type) - - if chi_up > lim: - break - up_int += 1 - - for i in range(len(chi_squared)): - if chi_squared[i] == min(chi_squared): - Optimum_params = parameters[i] - Optimum_sign_combination = tested_signs[i] - Optimum_chi_squared = chi_squared[i] - - y_fit = Models_class( - Optimum_params, x, y, self.N, pivot_point, - self.model_type, self.new_basis).y_sum - der = derivative_class( - x, y, Optimum_params, self.N, - pivot_point, self.model_type, self.zero_crossings, - self.constraints, self.new_basis) - derivatives, Optimum_zc_dict = der.derivatives, der.zc_dict - - end = time.time() - - if self.print_output in [1, 2]: - print('#'*50) - print('-'*20 + 'OPTIMUM RESULT' + '-'*20) - print('Time:', end-start) - print('Polynomial Order:', self.N) - if self.zero_crossings is not None: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints-len(self.zero_crossings)) - else: - print( - 'Number of Constrained Derivatives:', - self.N-self.constraints) - print('Signs :', Optimum_sign_combination) - print('Objective Function Value:', Optimum_chi_squared) - print('Parameters:', Optimum_params.T) - print('Method:', self.fit_type) - print('Model:', self.model_type) - print('Constraints: m >=', self.constraints) - if self.zero_crossings is None: - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - Optimum_zc_dict) - if self.zero_crossings is not None: - print('Zero Crossing Derivatives:', self.zero_crossings) - print( - 'Zero Crossings Used?' + - ' (0 signifies Yes\n in derivative order "i"):', - Optimum_zc_dict) - print('-'*50) - print('#'*50) - - save_optimum( - self.base_dir, end-start, self.N, - Optimum_sign_combination, Optimum_chi_squared, - Optimum_params, self.fit_type, self.model_type, - self.zero_crossings, Optimum_zc_dict, self.constraints) - - return y_fit, derivatives, Optimum_chi_squared, Optimum_params, \ - Optimum_sign_combination, Optimum_zc_dict - - if self.fit_type == 'qp': - y_fit, derivatives, Optimum_chi_squared, Optimum_params, \ - Optimum_sign_combination, Optimum_zc_dict = \ - qp(self.x, self.y, self.pivot_point) - if self.fit_type == 'qp-sign_flipping': - y_fit, derivatives, Optimum_chi_squared, Optimum_params, \ - Optimum_sign_combination, Optimum_zc_dict = \ - qp_sign_flipping(self.x, self.y, self.pivot_point) - - rms = (np.sqrt(np.sum((self.y-y_fit)**2)/len(self.y))) - - return y_fit, Optimum_sign_combination, Optimum_params, derivatives, \ - Optimum_chi_squared, rms, Optimum_zc_dict diff --git a/maxsmooth/Data_save.py b/maxsmooth/Data_save.py deleted file mode 100755 index f3ca144..0000000 --- a/maxsmooth/Data_save.py +++ /dev/null @@ -1,94 +0,0 @@ -import numpy as np -import os - - -class save(object): - def __init__(self, base_dir, params, chi_squared, signs, N, fit_type): - self.base_dir = base_dir - self.params = params - self.chi_squared = chi_squared - self.signs = [signs] - self.N = N - self.fit_type = fit_type - - if not os.path.exists(self.base_dir+'Output_Parameters/'): - os.mkdir(self.base_dir+'Output_Parameters/') - - with open( - self.base_dir+'Output_Parameters/'+str(self.N) + - '_'+self.fit_type+'.txt', 'a') as f: - np.savetxt(f, np.array(self.params).T) - f.close() - - if not os.path.exists(self.base_dir+'Output_Signs/'): - os.mkdir(self.base_dir+'Output_Signs/') - - with open( - self.base_dir+'Output_Signs/'+str(self.N) + - '_'+self.fit_type+'.txt', 'a') as f: - np.savetxt(f, self.signs) - f.close() - - if not os.path.exists(self.base_dir+'Output_Evaluation/'): - os.mkdir(self.base_dir+'Output_Evaluation/') - - with open( - self.base_dir+'Output_Evaluation/'+str(self.N) + - '_'+self.fit_type+'.txt', 'a') as f: - np.savetxt(f, np.array([self.chi_squared])) - f.close() - - -class save_optimum(object): - def __init__( - self, base_dir, time, N, Optimum_signs, Optimum_chi_squared, - Optimum_params, fit_type, model_type, zero_crossings, - Optimum_zc_dict, constraints): - self.base_dir = base_dir - self.time = time - self.N = N - self.Optimum_signs = Optimum_signs - self.Optimum_chi_squared = Optimum_chi_squared - self.Optimum_params = Optimum_params - self.fit_type = fit_type - self.model_type = model_type - self.zero_crossings = zero_crossings - self.Optimum_zc_dict = Optimum_zc_dict - self.constraints = constraints - - f = open( - self.base_dir + 'Optimal_Results_'+self.fit_type + - '_'+str(N)+'.txt', 'w') - f.write('Time:\n') - np.savetxt(f, np.array([self.time])) - f.write('Polynomial Order:\n') - np.savetxt(f, np.array([self.N])) - f.write('Number of Derivatives:\n') - if self.zero_crossings is None: - np.savetxt(f, np.array([self.N-self.constraints])) - else: - np.savetxt(f, np.array( - [self.N-self.constraints-len(self.zero_crossings)])) - f.write('Signs:\n') - np.savetxt(f, self.Optimum_signs) - f.write('Objective Function Value:\n') - np.savetxt(f, np.array([self.Optimum_chi_squared])) - f.write('Parameters:\n') - np.savetxt(f, self.Optimum_params) - f.write('Method:\n') - f.write(self.fit_type+'\n') - f.write('Model:\n') - f.write(self.model_type+'\n') - f.write('Constraints'+'\n') - np.savetxt(f, np.array([self.constraints])) - if self.zero_crossings is None: - f.write('Zero crossings Used? (0 signifies Yes):\n') - f.write(str(self.Optimum_zc_dict)) - if self.zero_crossings is not None: - f.write('Zero Crossing Derivatives:\n') - np.savetxt(f, self.zero_crossings) - f.write( - 'Zero crossings Used? (0 signifies' + - ' Yes\n in derivative order "i"):\n') - f.write(str(self.Optimum_zc_dict)) - f.close() diff --git a/maxsmooth/Models.py b/maxsmooth/Models.py deleted file mode 100755 index 329f3d0..0000000 --- a/maxsmooth/Models.py +++ /dev/null @@ -1,80 +0,0 @@ -import numpy as np -from scipy.special import legendre - - -class Models_class(object): - def __init__(self, params, x, y, N, pivot_point, model_type, new_basis): - self.x = x - self.y = y - self.N = N - self.params = params - self.pivot_point = pivot_point - self.model_type = model_type - self.model = new_basis['model'] - self.args = new_basis['args'] - self.y_sum = self.fit() - - def fit(self): - - if self.model is None: - if self.model_type == 'normalised_polynomial': - - y_sum = self.y[self.pivot_point]*np.sum([ - self.params[i]*(self.x/self.x[self.pivot_point])**i - for i in range(self.N)], axis=0) - - if self.model_type == 'polynomial': - - y_sum = np.sum( - [self.params[i]*(self.x)**i for i in range(self.N)], - axis=0) - - if self.model_type == 'loglog_polynomial': - - y_sum = 10**(np.sum([ - self.params[i]*np.log10(self.x)**i - for i in range(self.N)], - axis=0)) - - if self.model_type == 'exponential': - - y_sum = self.y[self.pivot_point]*np.sum([ - self.params[i] * - np.exp(-i*self.x/self.x[self.pivot_point]) - for i in range(self.N)], - axis=0) - - if self.model_type == 'log_polynomial': - - y_sum = np.sum([ - self.params[i] * - np.log10(self.x/self.x[self.pivot_point])**i - for i in range(self.N)], - axis=0) - - if self.model_type == 'difference_polynomial': - - y_sum = np.sum([ - self.params[i]*(self.x-self.x[self.pivot_point])**i - for i in range(self.N)], axis=0) - - if self.model_type == 'legendre': - - interval = np.linspace(-0.999, 0.999, len(self.x)) - lps = [] - for n in range(self.N): - P = legendre(n) - lps.append(P(interval)) - lps = np.array(lps) - y_sum = np.sum([ - self.params[i] * lps[i] for i in range(self.N)], axis=0) - - if self.model is not None: - if self.args is None: - y_sum = self.model( - self.x, self.y, self.pivot_point, self.N, self.params) - if self.args is not None: - y_sum = self.model( - self.x, self.y, self.pivot_point, self.N, self.params, - *self.args) - return y_sum diff --git a/maxsmooth/_version.py b/maxsmooth/_version.py index cd7e2da..8c0d5d5 100644 --- a/maxsmooth/_version.py +++ b/maxsmooth/_version.py @@ -1 +1 @@ -__version__ = "1.2.3" \ No newline at end of file +__version__ = "2.0.0" diff --git a/maxsmooth/best_basis.py b/maxsmooth/best_basis.py deleted file mode 100644 index ae8531b..0000000 --- a/maxsmooth/best_basis.py +++ /dev/null @@ -1,249 +0,0 @@ -""" -As demonstrated, this function allows you to test the built in basis and their -ability to -fit the data. It produces a plot that shows chi squared as a function of -:math:`{N}` for the 7 built in models and saves the figure to the base -directory. -""" - -import numpy as np -import matplotlib.pyplot as plt -import os -from maxsmooth.DCF import smooth - - -class basis_test(object): - - r""" - **Parameters:** - - x: **numpy.array** - | The x data points for the set being fitted. - - y: **numpy.array** - | The y data points for fitting. - - **Kwargs:** - - fit_type: **Default = 'qp-sign_flipping'** - | This kwarg allows the user to switch between sampling the - available discrete sign spaces (default) - or testing all sign combinations on the derivatives which can - be accessed by setting to 'qp'. - - base_dir: **Default = 'Fitted_Output/'** - | The location of the outputted - graph from function. This must be a string and end in '/'. If - the file does not exist then the function will create it. - - **N: Default = [3, .., 13] in steps of 1 else list or numpy array** - **of integers** - | The DCF orders to test each basis function with. In - some instances the basis function may fail for a given - :math:`{N}` and higher orders due to overflow/underflow - errors or ``CVXOPT`` errors. - - **pivot_point: Default = len(x)//2 otherwise an integer between** - **-len(x) and len(x)** - | Some of the built in - models rely on pivot points in the data sets which by defualt - is set as the middle index. This can be altered via - this kwarg which can occasionally lead to a better quality fit. - - **constraints: Default = 2 else an integer less than or equal** - **to N - 1** - | The minimum constrained derivative order which is set by default - to 2 for a Maximally Smooth Function. - - zero_crossings: **Default = None else list of integers** - | Allows you to - specify if the conditions should be relaxed on any - of the derivatives between constraints and the highest order - derivative. e.g. a 6th order fit with just a constrained 2nd - and 3rd order derivative would have zero_crossings = [4, 5]. - - cap: **Default = (len(available_signs)//N) + N else an integer** - | Determines the maximum number of signs explored either side of - the minimum :math:`{\chi^2}` value found after the decent - algorithm has terminated. - - chi_squared_limit: **Default = 2 else float or int** - | The prefactor on the maximum allowed increase in :math:`{\chi^2}` - during the directional exploration which is defaulted at 2. - If this value multiplied by the minimum :math:`{\chi^2}` - value found after the descent algorithm is exceeded then the - exploration in one direction is stopped and started in the - other. For more details on this and 'cap' see the ``maxsmooth`` - paper. - - cvxopt_maxiter: **Default = 10000 else integer** - | This shouldn't need - changing for most problems however if ``CVXOPT`` fails with a - 'maxiters reached' error message this can be increased. - Doing so arbitrarily will however increase the run time of - ``maxsmooth``. - - """ - - def __init__(self, x, y, **kwargs): - self.x = x - self.y = y - - for keys, values in kwargs.items(): - if keys not in set([ - 'fit_type', 'base_dir', 'N', 'pivot_point', - 'constraints', 'zero_crossings', 'chi_squared_limit', - 'cap', 'cvxopt_maxiter']): - raise KeyError( - "Unexpected keyword argument in basis_test().") - - self.fit_type = kwargs.pop('fit_type', 'qp-sign_flipping') - if self.fit_type not in set(['qp', 'qp-sign_flipping']): - raise KeyError( - "Invalid 'fit_type'. Valid entries include 'qp'\n" - + "'qp-sign_flipping'") - - self.base_dir = kwargs.pop('base_dir', 'Fitted_Output/') - if type(self.base_dir) is not str: - raise KeyError("'base_dir' must be a string ending in '/'.") - elif self.base_dir.endswith('/') is False: - raise KeyError("'base_dir' must end in '/'.") - - self.N = kwargs.pop('N', np.arange(3, 14, 1)) - for i in range(len(self.N)): - if self.N[i] % 1 != 0: - raise ValueError( - 'N must be an integer or whole number float.') - - self.pivot_point = kwargs.pop('pivot_point', len(self.x)//2) - if type(self.pivot_point) is not int: - raise TypeError('Pivot point is not an integer index.') - elif self.pivot_point >= len(self.x) or \ - self.pivot_point < -len(self.x): - raise ValueError( - 'Pivot point must be in the range -len(x) - len(x).') - - self.constraints = kwargs.pop('constraints', 2) - if type(self.constraints) is not int: - raise TypeError("'constraints' is not an integer") - if type(self.N) is list: - self.N = np.array(self.N) - if self.constraints >= self.N.min() and \ - self.constraints < self.N.max(): - self.N = np.arange(self.constraints + 1, self.N.max()+1, 1) - elif self.constraints >= self.N.max(): - raise ValueError( - "'constraints' exceeds the number of derivatives" + - " for highest value N provided to the function." + - " Lower constraints or increase the range of N being" + - " tested.") - - self.zero_crossings = kwargs.pop('zero_crossings', None) - if self.zero_crossings is not None: - for i in range(len(self.zero_crossings)): - if type(self.zero_crossings[i]) is not int: - raise TypeError( - "Entries in 'zero_crossings'" + - " are not integer.") - if self.zero_crossings[i] < self.constraints: - raise ValueError( - 'One or more specified derivatives for' + - ' zero crossings is less than the minimum' + - ' constrained' + - ' derivative.\n zero_crossings = ' + - str(self.zero_crossings) - + '\n' + ' Minimum Constrained Derivative = ' - + str(self.constraints)) - - self.chi_squared_limit = kwargs.pop('chi_squared_limit', None) - self.cap = kwargs.pop('cap', None) - if self.chi_squared_limit is not None: - if isinstance(self.chi_squared_limit, int) or \ - isinstance(self.chi_squared_limit, float): - pass - else: - raise TypeError( - "Limit on maximum allowed increase in chi squared" + - ", 'chi_squared_limit', is not an integer or float.") - if self.cap is not None: - if type(self.cap) is not int: - raise TypeError( - "The cap on directional exploration" + - ", 'cap', is not an integer.") - - self.cvxopt_maxiter = kwargs.pop('cvxopt_maxiter', 10000) - if type(self.cvxopt_maxiter) is not int: - raise ValueError("'cvxopt_maxiter' is not integer.") - - self.test() - - def test(self): - - if not os.path.exists(self.base_dir): - os.mkdir(self.base_dir) - - def fit(model_type): - - chi = [] - N_passed = [] - for i in range(len(self.N)): - try: - - result = smooth( - self.x, self.y, self.N[i], - model_type=model_type, - fit_type=self.fit_type, pivot_point=self.pivot_point, - constraints=self.constraints, - cvxopt_maxiter=self.cvxopt_maxiter, - zero_crossings=self.zero_crossings, - cap=self.cap, chi_squared_limit=self.chi_squared_limit) - - if model_type == 'loglog_polynomial': - chi.append(np.sum((self.y - result.y_fit)**2)) - else: - chi.append(result.optimum_chi) - N_passed.append(self.N[i]) - except Exception: - print( - 'Unable to fit with N = ' + str(self.N[i]) + ' and ' + - str(model_type) + '.') - - if chi != []: - chi = np.array(chi) - min_chi = chi.min() - for i in range(len(chi)): - if chi[i] == chi.min(): - best_N = self.N[i] - else: - chi = np.nan - min_chi = np.nan - N_passed = np.nan - best_N = np.nan - - return chi, min_chi, N_passed, best_N - - chi_poly, min_chi_poly, N_poly, best_N_poly = fit('polynomial') - chi_np, min_chi_np, N_np, best_N_np = fit('normalised_polynomial') - chi_log, min_chi_log, N_log, best_N_log = fit('log_polynomial') - chi_loglog, min_chi_loglog, N_loglog, best_N_loglog = \ - fit('loglog_polynomial') - chi_leg, min_chi_leg, N_leg, best_N_leg = fit('legendre') - chi_exp, min_chi_exp, N_exp, best_N_exp = fit('exponential') - chi_dif, min_chi_dif, N_dif, best_N_dif = fit('difference_polynomial') - - plt.figure() - plt.plot(N_np, chi_np, label='Normalised Polynomial', c='b') - plt.plot(N_poly, chi_poly, label='Polynomial', c='k') - plt.plot(N_dif, chi_dif, label='Difference Polynomial', c='r') - plt.plot(N_log, chi_log, label=r'Log Polynomial', c='g') - plt.plot(N_loglog, chi_loglog, label=r'Log Log Polynomial', c='orange') - plt.plot(N_leg, chi_leg, label='Legendre', c='purple') - plt.plot(N_exp, chi_exp, label='Exponential', c='magenta') - plt.legend() - plt.xlabel(r'N') - plt.xticks(self.N) - plt.ylabel(r'$\chi^2$') - plt.yscale('log') - plt.tight_layout() - plt.savefig(self.base_dir + 'Basis_functions.pdf') - plt.close() diff --git a/maxsmooth/chidist_plotter.py b/maxsmooth/chidist_plotter.py deleted file mode 100644 index a00e50b..0000000 --- a/maxsmooth/chidist_plotter.py +++ /dev/null @@ -1,272 +0,0 @@ -""" - -This function allows the user to produce plots of the chi squared -distribution as a function of the available discrete sign spaces for the -constrained derivatives. This can be used to identify whether or not the -problem is `ill defined`, see the ``maxsmooth`` paper for a definition, -and if it can be solved using the sign sampling approach. - -It can also be used to determine whether or not the 'cap' and maximum allowed -increase on the value of chi squared during the directional exploration -are sufficient to identify the global minimum for the problem. - -The function is reliant on the output of the ``maxsmooth`` smooth() function. -The required outputs can be saved when running smooth() -using the 'data_save = True' kwarg. -""" - -import numpy as np -import os -from itertools import product -import matplotlib.pyplot as plt - - -class chi_plotter(object): - - r""" - - **Parameters:** - - N: **int** - | The number of terms in the DCF. - - **Kwargs:** - - fit_type: **Default = 'qp-sign_flipping'** - | This kwarg is the same as for the smooth() function. - Here it allows the files to be read from the base - directory. - - base_dir: **Default = 'Fitted_Output/'** - | The location of the outputted - data from ``maxsmooth``. This must be a string and end in '/' - and must contain the files 'Output_Evaluations/' and - 'Output_Signs/' which can be obtained by running smooth() with - data_save=True. - - chi: **Default = None else list or numpy array** - | A list of - chi squared evaluations. If provided then this is used - over outputted data in the base directory. It must have the - same length as the ouputted signs in the file 'Output_Signs/' - in the base directory. It must also be ordered correctly - otherwise the returned graph will not be correct. A correct - ordering is one for which each entry in the array corresponds - to the correct sign combination in 'Output_Signs/'. - Typically this will not be needed but if the chi squared - evaluation in 'Output_Evaluations/' in the base directory - is not in the desired parameter space this can be useful. - For example the built in logarithmic model calculates - chi squared in logarithmic space. To plot the distribution - in linear space we can calculate - chi squared in linear space using a function for the model - and the tested parameters which are found in - 'Output_Parameters/' in the base directory. - - **constraints: Default = 2 else an integer less than or equal** - **to N - 1** - | The minimum constrained derivative order which is set by default - to 2 for a Maximally Smooth Function. Used here to determine - the number of possible sign combinations available. - - zero_crossings: **Default = None else list of integers** - | Allows you to - specify if the conditions should be relaxed on any - of the derivatives between constraints and the highest order - derivative. e.g. a 6th order fit with just a constrained 2nd - and 3rd order derivative would have a zero_crossings = [4, 5]. - Again this is used in determining the possible sign - combinations available. - - plot_limits: **Default = False** - | Determines whether the limits on - the directional exploration are plotted on top of the - chi squared distribution. - - cap: **Default = (len(available_signs)//N) + N else an integer** - | Determines the maximum number of signs explored either side of - the minimum chi squared value found after the - decent algorithm has terminated when running smooth(). Here - it is used when plot_limits=True. - - chi_squared_limit: **Default = 2 else float or int** - | The prefactor on the maximum allowed increase in chi squared - during the directional exploration which is defaulted at 2. - If this value multiplied by the minimum chi squared - value found after the descent algorithm is exceeded then the - exploration in one direction is stopped and started in the - other. For more details on this and 'cap' see the ``maxsmooth`` - paper. Again this is used here - when plot_limits=True. - - """ - def __init__(self, N, **kwargs): - - self.N = N - if self.N % 1 != 0: - raise ValueError('N must be an integer or whole number float.') - - for keys, values in kwargs.items(): - if keys not in set([ - 'chi', 'base_dir', - 'zero_crossings', 'constraints', - 'fit_type', 'chi_squared_limit', 'cap', 'plot_limits']): - raise KeyError("Unexpected keyword argument in chi_plotter().") - - self.base_dir = kwargs.pop('base_dir', 'Fitted_Output/') - if type(self.base_dir) is not str: - raise KeyError("'base_dir' must be a string ending in '/'.") - elif self.base_dir.endswith('/') is False: - raise KeyError("'base_dir' must end in '/'.") - - if not os.path.exists(self.base_dir): - raise Exception( - "'base_dir' must exist and contain the outputted" - + " evaluations and sign combinations from a maxsmooth fit." - + " These can be obtained by running maxsmooth with" - + " 'data_save=True'.") - else: - if not os.path.exists(self.base_dir + 'Output_Evaluation/'): - raise Exception( - "No 'Output_Evaluation/' directory found in" - + " 'base_dir'.") - if not os.path.exists(self.base_dir + 'Output_Signs/'): - raise Exception( - "No 'Output_Signs/' directory found in" - + " 'base_dir'.") - - self.chi = kwargs.pop('chi', None) - - self.constraints = kwargs.pop('constraints', 2) - if type(self.constraints) is not int: - raise TypeError("'constraints' is not an integer") - if self.constraints > self.N-1: - raise ValueError( - "'constraints' exceeds the number" + - " of derivatives.") - - self.zero_crossings = kwargs.pop('zero_crossings', None) - if self.zero_crossings is not None: - for i in range(len(self.zero_crossings)): - if type(self.zero_crossings[i]) is not int: - raise TypeError( - "Entries in 'zero_crossings'" + - " are not integer.") - if self.zero_crossings[i] < self.constraints: - raise ValueError( - 'One or more specified derivatives for' + - ' zero crossings is less than the minimum' + - ' constrained' + - ' derivative.\n zero_crossings = ' - + str(self.zero_crossings) - + '\n' + ' Minimum Constrained Derivative = ' - + str(self.constraints)) - - self.fit_type = kwargs.pop('fit_type', 'qp-sign_flipping') - if self.fit_type not in set(['qp', 'qp-sign_flipping']): - raise KeyError( - "Invalid 'fit_type'. Valid entries include 'qp'\n" + - "'qp-sign_flipping'") - - self.chi_squared_limit = kwargs.pop('chi_squared_limit', None) - self.cap = kwargs.pop('cap', None) - if self.chi_squared_limit is not None: - if isinstance(self.chi_squared_limit, int) or \ - isinstance(self.chi_squared_limit, float): - pass - else: - raise TypeError( - "Limit on maximum allowed increase in chi squared" + - ", 'chi_squared_limit', is not an integer or float.") - if self.cap is not None: - if type(self.cap) is not int: - raise TypeError( - "The cap on directional exploration" + - ", 'cap', is not an integer.") - - self.plot_limits = kwargs.pop('plot_limits', False) - if type(self.plot_limits) is not bool: - raise TypeError( - "Boolean keyword argument with value " - + " 'plot_limits' is not True or False.") - - self.plot() - - def plot(self): - - def signs_array(nums): - return np.array(list(product(*((x, -x) for x in nums)))) - - if self.zero_crossings is not None: - possible_signs = signs_array([1]*( - self.N-self.constraints-len(self.zero_crossings))) - else: - possible_signs = signs_array([1]*(self.N-self.constraints)) - - plt.figure() - j = np.arange(0, len(possible_signs), 1) - if self.chi is None: - chi = np.loadtxt( - self.base_dir + 'Output_Evaluation/' - + str(self.N) + '_' + str(self.fit_type) + '.txt') - signs = np.loadtxt( - self.base_dir + 'Output_Signs/' - + str(self.N) + '_' + str(self.fit_type) + '.txt') - if len(signs) != len(possible_signs): - index = [] - for p in range(len(signs)): - for i in range(len(possible_signs)): - if np.all(signs[p] == possible_signs[i]): - index.append(i) - index, chi = zip(*sorted(zip(index, chi))) - plt.plot(index, chi, ls='-') - else: - plt.plot(j, chi, marker='.', ls='-') - else: - chi = self.chi - signs = np.loadtxt( - self.base_dir + 'Output_Signs/' - + str(self.N) + '_' + str(self.fit_type) + '.txt') - if len(signs) != len(possible_signs): - index = [] - for p in range(len(signs)): - for i in range(len(possible_signs)): - if np.all(signs[p] == possible_signs[i]): - index.append(i) - index, chi = zip(*sorted(zip(index, chi))) - plt.plot(index, chi, marker='.', ls='-') - else: - plt.plot(j, chi, marker='.', ls='-') - - if self.cap is None: - self.cap = (len(possible_signs)//self.N) + self.N - if self.chi_squared_limit is None: - self.chi_squared_limit = 2*min(chi) - - for i in range(len(chi)): - if chi[i] == min(chi): - plt.plot(i, chi[i], marker='*') - if self.plot_limits is True: - plt.vlines( - i + self.cap, min(chi), max(chi), ls='--', - label='Cap On Exp.', color='k', alpha=0.5) - plt.vlines( - i - self.cap, min(chi), max(chi), - ls='--', color='k', alpha=0.5) - if self.plot_limits is True: - min_chi = np.load( - self.base_dir + str(self.N) + - '_'+self.fit_type+'_minimum_chi_post_descent.npy') - plt.hlines( - self.chi_squared_limit*min_chi, 0, len(possible_signs), - ls='-.', label=r'Max. Increase\n' + - ' in $\chi^2$', # noqa: W605 - color='k', alpha=0.5) - plt.xlim([j[0], j[-1]]) - plt.grid() - plt.yscale('log') - plt.ylabel(r'$\chi^2$') - plt.xlabel('Sign Combination') - plt.tight_layout() - plt.savefig(self.base_dir + 'chi_distribution.pdf') - plt.close() diff --git a/maxsmooth/derivatives.py b/maxsmooth/derivatives.py index 00767ee..f7806f2 100755 --- a/maxsmooth/derivatives.py +++ b/maxsmooth/derivatives.py @@ -1,166 +1,63 @@ -import numpy as np -from scipy.special import lpmv -import math - - -class derivative_class(object): - def __init__( - self, x, y, params, N, pivot_point, model_type, zero_crossings, - constraints, new_basis, **kwargs): - self.x = x - self.y = y - self.N = N - self.params = params - self.pivot_point = pivot_point - self.model_type = model_type - self.zero_crossings = zero_crossings - self.derivatives_function = new_basis['derivatives_function'] - self.args = new_basis['args'] - self.constraints = constraints - - self.call_type = kwargs.pop('call_type', 'checking') - - self.derivatives, self.pass_fail, self.zc_dict = \ - self.derivatives_func() - - def derivatives_func(self): - - def mth_order_derivatives(m): - if self.derivatives_function is None: - if np.any(self.model_type != ['legendre', 'exponential']): - mth_order_derivative = [] - for i in range(self.N-m): - if self.model_type == 'normalised_polynomial': - mth_order_derivative_term = ( - self.y[self.pivot_point] / - self.x[self.pivot_point]) * \ - math.factorial(m+i) / \ - math.factorial(i) * \ - self.params[int(m)+i]*(self.x)**i / \ - (self.x[self.pivot_point])**(i+1) - mth_order_derivative.append( - mth_order_derivative_term) - if self.model_type == 'polynomial': - mth_order_derivative_term = \ - math.factorial(m+i) / \ - math.factorial(i) * \ - self.params[int(m)+i]*(self.x)**i - mth_order_derivative.append( - mth_order_derivative_term) - if self.model_type == 'log_polynomial': - mth_order_derivative_term = \ - math.factorial(m+i) / \ - math.factorial(i) * \ - self.params[int(m) + i] * \ - np.log10(self.x/self.x[self.pivot_point])**i - mth_order_derivative.append( - mth_order_derivative_term) - if self.model_type == 'loglog_polynomial': - mth_order_derivative_term = \ - math.factorial(m+i) / \ - math.factorial(i) * \ - self.params[int(m)+i]*np.log10(self.x)**i - mth_order_derivative.append( - mth_order_derivative_term) - if self.model_type == 'difference_polynomial': - mth_order_derivative_term = \ - math.factorial(m+i) / \ - math.factorial(i) * \ - self.params[int(m)+i] * \ - (self.x-self.x[self.pivot_point])**i - mth_order_derivative.append( - mth_order_derivative_term) - - if self.derivatives_function is not None: - if self.args is None: - derivatives = \ - self.derivatives_function( - m, self.x, self.y, self.N, self.pivot_point, - self.params) - if self.args is not None: - derivatives = \ - self.derivatives_function( - m, self.x, self.y, self.N, self.pivot_point, - self.params, *self.args) - mth_order_derivative = derivatives - - if self.model_type == 'legendre': - interval = np.linspace(-0.999, 0.999, len(self.x)) - alps = [] - for i in range(self.N): - alps.append(lpmv(m, i, interval)) - alps = np.array(alps) - derivatives = [] - for h in range(len(alps)): - derivatives.append( - ((alps[h, :]*(-1)**(m))/(1-interval**2)**(m/2)) - * self.params[h, 0]) - mth_order_derivative = np.array(derivatives) - if self.model_type == 'exponential': - derivatives = np.empty([self.N, len(self.x)]) - for i in range(self.N): - for h in range(len(self.x)): - derivatives[i, h] = \ - self.y[self.pivot_point] * ( - self.params[i] * - np.exp(-i * self.x[h]/self.x[self.pivot_point])) \ - * (-i/self.x[self.pivot_point])**m - mth_order_derivative = np.array(derivatives) - - if type(mth_order_derivative) == list: - mth_order_derivative = np.array(mth_order_derivative) - if mth_order_derivative.shape == (len(self.x), self.N): - mth_order_derivative = mth_order_derivative.sum(axis=1) - else: - mth_order_derivative = mth_order_derivative.sum(axis=0) - - return mth_order_derivative - - m = np.arange(0, self.N, 1) - derivatives = [] - zc_derivatives = [] - zc_orders = [] - for i in range(len(m)): - if m[i] < self.constraints: - zc_orders.append(m[i]) - zc_derivatives.append(mth_order_derivatives(m[i])) - if m[i] >= self.constraints: - if self.zero_crossings is not None: - if m[i] not in set(self.zero_crossings): - derivatives.append(mth_order_derivatives(m[i])) - if m[i] in set(self.zero_crossings): - zc_orders.append(m[i]) - zc_derivatives.append(mth_order_derivatives(m[i])) - else: - derivatives.append(mth_order_derivatives(m[i])) - derivatives = np.array(derivatives) - - zc_derivatives = np.array(zc_derivatives) - zc_orders = np.array(zc_orders) - - # Check constrained derivatives - pass_fail = [] - for i in range(derivatives.shape[0]): - if np.all(derivatives[i, :] >= -1e-6) or \ - np.all(derivatives[i, :] <= 1e-6): - pass_fail.append(1) - else: - pass_fail.append(0) - pass_fail = np.array(pass_fail) - - zc_dict = {} - for i in range(zc_derivatives.shape[0]): - if np.all(zc_derivatives[i, :] >= -1e-6) or \ - np.all(zc_derivatives[i, :] <= 1e-6): - zc_dict[str(zc_orders[i])] = 1 - else: - zc_dict[str(zc_orders[i])] = 0 - - if self.call_type == 'checking': - if np.any(pass_fail == 0): - print('Pass or fail', pass_fail) - raise Exception( - '"Condition Violated" Derivatives feature' + - ' crossing points.') - - return derivatives, pass_fail, zc_dict +"""Compute derivative prefactors for a given function using JAX.""" + +from collections.abc import Callable + +import jax +import jax.numpy as jnp + + +# function to generate derivatives +def make_derivative_functions(f: Callable, max_order: int) -> list[Callable]: + """Return list of functions computing derivatives of f up to max_order. + + Args: + f (Callable): Function to differentiate. + max_order (int): Maximum order of derivatives to compute. + + Returns: + List[Callable]: List of derivative functions. + """ + derivs = [f] + for _ in range(1, max_order): + derivs.append(jax.grad(derivs[-1], argnums=0)) + return derivs + + +def derivative_prefactors( + f: Callable, + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, + max_order: int, +) -> list[jnp.ndarray]: + """Return list of derivative matrices G[m]. + + G[m] maps params -> m-th derivative at all x. + + Args: + f (Callable): Function to differentiate. + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Parameters of the function. + max_order (int): Maximum order of derivatives to compute. + + Returns: + List[jnp.ndarray]: List of derivative matrices. + """ + Gs = [] + df_dx = f # start from f + + for m in range(max_order): + # Jacobian of current derivative w.r.t parameters + Gm = jax.vmap( + lambda xi: jax.jacobian(df_dx, argnums=3)( + xi, norm_x, norm_y, params + ) + )(x) + Gs.append(Gm) + + # Prepare next derivative wrt x + df_dx = jax.grad(df_dx, argnums=0) + return Gs diff --git a/maxsmooth/models.py b/maxsmooth/models.py new file mode 100755 index 0000000..82673a0 --- /dev/null +++ b/maxsmooth/models.py @@ -0,0 +1,286 @@ +"""Various functional forms for modeling data.""" + +import jax +from jax import numpy as jnp + + +@jax.jit +def normalised_polynomial( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate a normalised polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated polynomial at x. + """ + i = jnp.arange(params.shape[0]) + powers = (x / norm_x) ** i + y_sum = norm_y * jnp.sum(params * powers, axis=0) + return y_sum + + +@jax.jit +def normalised_polynomial_basis( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate the basis functions of a normalised polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated basis functions at x. + """ + i = jnp.arange(params.shape[0]) + powers = norm_y * (x / norm_x) ** i + return powers + + +@jax.jit +def polynomial( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate a polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated polynomial at x. + """ + i = jnp.arange(params.shape[0]) + powers = x**i + y_sum = jnp.sum(params * powers, axis=0) + return y_sum + + +@jax.jit +def polynomial_basis( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate the basis functions of a polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated basis functions at x. + """ + i = jnp.arange(params.shape[0]) + powers = x**i + return powers + + +@jax.jit +def loglog_polynomial( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate a log-log polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated polynomial at x. + """ + i = jnp.arange(params.shape[0]) + powers = jnp.log10(x) ** i + y_sum = 10 ** jnp.sum(params * powers, axis=0) + return y_sum + + +@jax.jit +def loglog_polynomial_basis( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate the basis functions of a log-log polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated basis functions at x. + """ + i = jnp.arange(params.shape[0]) + powers = jnp.log10(x) ** i + return powers + + +@jax.jit +def exponential( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate an exponential basis at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the exponential basis. + + Returns: + jnp.ndarray: Evaluated exponential basis at x. + """ + i = jnp.arange(params.shape[0]) + powers = jnp.exp(-i * x / norm_x) + y_sum = norm_y * jnp.sum(params * powers, axis=0) + return y_sum + + +@jax.jit +def exponential_basis( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate the basis functions of an exponential basis at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the exponential basis. + + Returns: + jnp.ndarray: Evaluated basis functions at x. + """ + i = jnp.arange(params.shape[0]) + powers = norm_y * jnp.exp(-i * x / norm_x) + return powers + + +@jax.jit +def log_polynomial( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate a log polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated polynomial at x. + """ + i = jnp.arange(params.shape[0]) + powers = jnp.log10(x / norm_x) ** i + y_sum = jnp.sum(params * powers, axis=0) + return y_sum + + +@jax.jit +def log_polynomial_basis( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate the basis functions of a log polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated basis functions at x. + """ + i = jnp.arange(params.shape[0]) + powers = jnp.log10(x / norm_x) ** i + return powers + + +@jax.jit +def difference_polynomial( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate a difference polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated polynomial at x. + """ + i = jnp.arange(params.shape[0]) + powers = (x - norm_x + 1e-6) ** i + y_sum = jnp.sum(params * powers, axis=0) + return y_sum + + +@jax.jit +def difference_polynomial_basis( + x: jnp.ndarray, + norm_x: jnp.ndarray, + norm_y: jnp.ndarray, + params: jnp.ndarray, +) -> jnp.ndarray: + """Evaluate the basis functions of a difference polynomial at x. + + Args: + x (jnp.ndarray): Input data points. + norm_x (jnp.ndarray): Normalisation point for x. + norm_y (jnp.ndarray): Normalisation point for y. + params (jnp.ndarray): Coefficients of the polynomial. + + Returns: + jnp.ndarray: Evaluated basis functions at x. + """ + i = jnp.arange(params.shape[0]) + powers = (x - norm_x + 1e-6) ** i + return powers diff --git a/maxsmooth/parameter_plotter.py b/maxsmooth/parameter_plotter.py deleted file mode 100644 index d6b023f..0000000 --- a/maxsmooth/parameter_plotter.py +++ /dev/null @@ -1,558 +0,0 @@ -""" -This function allows you to plot the parameter space around the optimum -solution found when running ``maxsmooth`` and visualise the constraints with -contour lines given by chi squared. -""" - -import warnings -import numpy as np -import itertools -import matplotlib.pyplot as plt -import progressbar -from itertools import product -from maxsmooth import Models -from maxsmooth.derivatives import derivative_class -import os - - -class param_plotter(object): - - r""" - - **Parameters:** - - best_params: **numpy.array** - | The optimum parameters found when running - a DCF fit to the data. - - optimum_signs: **numpy.array** - | The optimum signs for the DCF fit which - are used when the derivatives are equal to 0 across the band. - - x: **numpy.array** - | The x data points. - - y: **numpy.array** - | The y data points. - - N: **int** - | The number of terms in the DCF. - - **Kwargs:** - - model_type: **Default = 'difference_polynomial'** - | The functional form of - the model being plotted. If a the user has defined their own - basis they can supply this with the Kwargs below and this - will be overwritten. - - base_dir: **Default = 'Fitted_Output/'** - | The location in which the - parameter plot is saved. - - **constraints: Default = 2 else an integer less than or equal** - **to N - 1** - | The minimum constrained derivative order which is set by default - to 2 for a Maximally Smooth Function. Used here to - determine the number of possible sign combinations available. - - zero_crossings: **Default = None else list of integers** - | Allows you to - specify if the conditions should be relaxed on any - of the derivatives between constraints and the highest order - derivative. e.g. a 6th order fit with just a constrained - 2nd and 3rd order derivative would have an - zero_crossings = [4, 5]. - Again this is used in determining the possible sign - combinations available. - - samples: **Default = 50** - | The sampling rate across the parameter ranges - defined with the optimum solution and width. - - width: **Default = 0.5** - | The range of each parameter to explore. The - default value of 0.5 means that the :math:`{\chi^2}` - values for parameters ranging 50% either side of the optimum - result are tested. - - warnings: **Default = True** - | Used to highlight when a derivative is - 0 across the band and that in these instances the optimum - signs are assumed for the colourmap if :math:`{N \leq 5}`, - constraints=2 and the zero_crossings is empty. - - girdlines: **Default = False** - | Plots gridlines showing the central value - for each parameter in each panel of the plot. - - center_plot: **Default = False** - | Setting this equal to True will highlight the central region - with a white circle. - - data_plot: **Default = False** - | Setting to True will plot the data, fitted model and the - residuals, :math:`{y - y_{fit}}`, alongside the - parameter graph. - - The following Kwargs are used to plot the parameter space for a user - defined basis function and will overwrite the 'model_type' kwarg. - - **basis_function: Default = None else function with parameters** - **(x, y, pivot_point, N)** - | This is a function of basis functions - for the quadratic programming. The variable pivot_point is the - index at the middle of the datasets x and y by default but can - be adjusted. - - **model: Default = None else function with parameters** - **(x, y, pivot_point, N, params)** - | This is - a user defined function describing the model to be fitted to - the data. - - **der_pres: Default = None else function with parameters** - **(m, x, y, N, pivot_point)** - | This function describes the prefactors on the - mth order derivative used in defining the constraint. - - **derivatives: Default = None else function with parameters** - **(m, x, y, N, pivot_point, params)** - | User defined function describing the mth - order derivative used to check that conditions are being met. - - **args: Default = None else list** - | Extra arguments for `smooth` - to pass to the functions detailed above. - - """ - - def __init__(self, best_params, optimum_signs, x, y, N, **kwargs): - self.best_params = best_params - self.optimum_signs = optimum_signs - self.x = x - self.y = y - - self.N = N - if self.N % 1 != 0: - raise ValueError('N must be an integer or whole number float.') - - for keys, values in kwargs.items(): - if keys not in set([ - 'model_type', 'base_dir', - 'zero_crossings', 'constraints', - 'basis_functions', 'der_pres', 'model', - 'derivatives', 'args', 'pivot_point', 'samples', - 'width', 'warnings', 'gridlines', 'center_plot', - 'data_plot']): - raise KeyError( - "Unexpected keyword argument in param_plotter().") - - self.model_type = kwargs.pop('model_type', 'difference_polynomial') - if self.model_type not in set([ - 'normalised_polynomial', 'polynomial', - 'log_polynomial', 'loglog_polynomial', 'difference_polynomial', - 'exponential', 'legendre']): - raise KeyError( - "Invalid 'model_type'. See documentation for built" + - "in models.") - - self.basis_functions = kwargs.pop('basis_functions', None) - self.der_pres = kwargs.pop('der_pres', None) - self.model = kwargs.pop('model', None) - self.derivatives_function = kwargs.pop('derivatives', None) - self.args = kwargs.pop('args', None) - - self.new_basis = { - 'basis_function': - self.basis_functions, 'der_pres': self.der_pres, - 'derivatives_function': self.derivatives_function, - 'model': self.model, 'args': self.args} - if np.all([value is None for value in self.new_basis.values()]): - pass - else: - count = 0 - for key, value in self.new_basis.items(): - if value is None and key != 'args': - raise KeyError( - 'Attempt to change basis functions failed.' + - ' One or more functions not defined.' + - ' Please consult documentation.') - if value is None and key == 'args': - warnings.warn( - 'Warning: No additional arguments passed' + - ' to new basis functions') - count += 1 - if count == len(self.new_basis): - self.model_type = 'user_defined' - - self.pivot_point = kwargs.pop('pivot_point', len(self.x)//2) - if type(self.pivot_point) is not int: - raise TypeError('Pivot point is not an integer index.') - elif self.pivot_point >= len(self.x) or \ - self.pivot_point < -len(self.x): - raise ValueError( - 'Pivot point must be in the range -len(x) - len(x).') - - self.base_dir = kwargs.pop('base_dir', 'Fitted_Output/') - if type(self.base_dir) is not str: - raise KeyError("'base_dir' must be a string ending in '/'.") - elif self.base_dir.endswith('/') is False: - raise KeyError("'base_dir' must end in '/'.") - - if not os.path.exists(self.base_dir): - os.mkdir(self.base_dir) - - self.constraints = kwargs.pop('constraints', 2) - if type(self.constraints) is not int: - raise TypeError("'constraints' is not an integer") - if self.constraints > self.N-1: - raise ValueError( - "'constraints' exceeds the number of derivatives.") - - self.zero_crossings = kwargs.pop('zero_crossings', None) - if self.zero_crossings is not None: - for i in range(len(self.zero_crossings)): - if type(self.zero_crossings[i]) is not int: - raise TypeError( - "Entries in 'zero_crossings'" + - " are not integer.") - if self.zero_crossings[i] < self.constraints: - raise ValueError( - 'One or more specified derivatives for' + - ' inflection points is less than the minimum' + - ' constrained' + - ' derivative.\n zero_crossings = ' - + str(self.zero_crossings) - + '\n' + ' Minimum Constrained Derivative = ' - + str(self.constraints)) - - self.samples = kwargs.pop('samples', 50) - if self.samples % 1 != 0: - raise ValueError('Error: Samples must be a whole number.') - - self.width = kwargs.pop('width', 0.5) - if type(self.width) is not int: - if type(self.width) is not float: - raise ValueError('Width must be an integer or a float.') - - self.warnings = kwargs.pop('warnings', True) - self.gridlines = kwargs.pop('gridlines', False) - self.data_plot = kwargs.pop('data_plot', False) - self.center_plot = kwargs.pop('center_plot', False) - boolean_kwargs = [ - self.warnings, self.gridlines, - self.center_plot, self.data_plot] - for i in range(len(boolean_kwargs)): - if type(boolean_kwargs[i]) is not bool: - raise TypeError( - "Boolean keyword argument with value " - + str(boolean_kwargs[i]) + - " is not True or False.") - - self.plot() - - def plot(self): - - def chi_squared(parameters): - y_sum = Models.Models_class( - parameters, self.x, self.y, - self.N, self.pivot_point, self.model_type, - self.new_basis).y_sum - if self.model_type == 'loglog_polynomial': - chi = np.sum((np.log10(self.y) - np.log10(y_sum))**2) - else: - chi = np.sum((self.y - y_sum)**2) - return chi - - def plot_formatting(xpos, ypos): - ypos -= 1 - if xpos == 0: - axes[ypos, xpos].set_ylabel(r'$a_{%2d}$' % i1, fontsize=12) - if xpos != 0: - axes[ypos, xpos].set_yticklabels([]) - if ypos == self.N-2: - axes[ypos, xpos].set_xlabel(r'$a_{%2d}$' % i2, fontsize=12) - if ypos != self.N-2: - axes[ypos, xpos].set_xticklabels([]) - for label in axes[ypos, xpos].get_xticklabels(): - label.set_rotation(90) - - def signs_array(nums): - return np.array(list(product(*((x, -x) for x in nums)))) - - if self.zero_crossings is not None: - available_signs = signs_array([1]*( - self.N-self.constraints-len(self.zero_crossings))) - else: - available_signs = signs_array([1]*(self.N-self.constraints)) - - indices = np.array([np.arange(0, self.N, 1), np.arange(0, self.N, 1)]) - combinations = list(itertools.product(*indices)) - - combis = [] - for i in range(len(combinations)): - if combinations[i][0] != combinations[i][1]: - combis.append(combinations[i]) - for j in range(len(combis)): - if combis[-1] == tuple(sorted(combis[j])): - combis.remove(combis[-1]) - - bar = progressbar.ProgressBar( - maxval=len(combis), - widgets=[ - progressbar.Bar('#', '[', ']'), ' ', - progressbar.Percentage()]) - bar.start() - mapped_colours = [] - cp_array = [] - sign_combinations = [] - warnings_count = 0 - - if self.data_plot is True: - fig, axes = plt.subplots( - figsize=(15, 10), nrows=self.N-1, - ncols=self.N-1) - else: - fig, axes = plt.subplots( - figsize=(10, 10), nrows=self.N-1, - ncols=self.N-1) - - for n in range(self.N-1): - for m in range(self.N-1): - if n < m: - axes[n, m].axis('off') - for f in range(len(combis)): - bar.update(f+1) - i1 = combis[f][0] - i2 = combis[f][1] - p = [] - for i in range(self.N): - if i == i1 or i == i2: - p.append( - np.linspace( - self.best_params[i] * (1 - self.width), - self.best_params[i] * (1 + self.width), - self.samples)) - p = np.array(p).T[0] - best = [ - list(self.best_params[i2])[0], list(self.best_params[i1])[0]] - p = list(p) - p.insert(len(p)//2, best) - p = np.array(p) - - comb, id = [], [] - for i in range(self.N): - if i != i1 and i != i2: - id.append(i) - comb.append(self.best_params[i]) - comb, id = np.array(comb).T[0], np.array(id) - - X, Y = np.meshgrid(p[:, 0], p[:, 1]) - - chi = np.empty(X.shape) - pf = np.empty(X.shape) - if self.N <= 5: - s = np.empty(X.shape) - for i in range(s.shape[0]): - for j in range(s.shape[1]): - s[i, j] = len(available_signs)+10 - - for j in range(X.shape[0]): - for a in range(X.shape[1]): - parameters = np.empty(self.N) - parameters[i1] = Y[j, a].copy() - parameters[i2] = X[j, a].copy() - for h in range(len(id)): - parameters[id[h]] = comb[h] - parameters = np.array(parameters) - chi[j, a] = chi_squared(parameters) - - if self.model_type == 'legendre': - parameters = np.array([parameters]).T - - der = derivative_class( - self.x, self.y, - parameters, self.N, - self.pivot_point, self.model_type, - self.zero_crossings, self.constraints, - self.new_basis, call_type='plotter') - - derivatives, pass_fail = der.derivatives, der.pass_fail - - if np.any(pass_fail == 0): - pf[j, a] = 0 - else: - pf[j, a] = 1 - - if self.N <= 5: - signs = [] - for i in range(derivatives.shape[0]): - if (np.all(derivatives[i, :] >= -1e-6)) and \ - (np.all(derivatives[i, :] <= 1e-6)): - signs.append(self.optimum_signs) - if self.warnings is True and \ - warnings_count == 0: - warnings.warn( - 'Warning: One or more derivatives' - + ' equals 0 across the band. Optimum' - + ' derivative signs from maxsmooth' - + ' assumed for these derivatives' - + ' which may cause inconsistencies' - + ' in the parameter plot.') - warnings_count += 1 - elif (np.all(derivatives[i, :] >= -1e-6)): - signs.append(-1) - elif (np.all(derivatives[i, :] <= 1e-6)): - signs.append(+1) - - for i in range(len(available_signs)): - if np.all(signs == available_signs[i]) \ - and pf[j, a] != 0: - s[j, a] = i - - chi_masked = np.ma.masked_where(pf == 0, chi) - chi_fail_masked = np.ma.masked_where(pf == 1, chi) - - if self.N <= 5: - chi_array = [] - for i in range(len(available_signs)): - array = chi_masked.copy() - chi_array.append(np.ma.masked_where(s != i, array)) - - plot_formatting(i2, i1) - - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - if self.N > 5 or self.constraints != 2: - cp = axes[i1 - 1, i2].contourf( - X, Y, chi_masked, - np.linspace(chi.min(), chi.max(), 10), - cmap='autumn') - if f == len(combis) - 1: - if self.data_plot is True: - cbax = fig.add_axes([0.7*11/15, 0.88, 0.2, 0.02]) - else: - cbax = fig.add_axes([0.61, 0.88, 0.25, 0.02]) - clb = plt.colorbar( - cp, cax=cbax, - orientation='horizontal') - clb.ax.set_ylabel( - r'Valid Region', rotation=0, - fontsize=10) - clb.ax.yaxis.set_label_coords(-0.2, 0.1) - clb.ax.tick_params(rotation=90) - else: - cps, mapped_colours_combi = [], [] - mapped_sign_combinations = [] - cmaps = [ - 'autumn', 'winter', 'summer', 'spring', - 'Greens', 'cool', 'pink', 'ocean'] - for i in range(len(available_signs)): - if np.all(chi_array[i].mask): - pass - else: - cp = axes[i1 - 1, i2].contourf( - X, Y, chi_array[i], - np.linspace(chi.min(), chi.max(), 10), - cmap=cmaps[i]) - cps.append(cp) - mapped_colours_combi.append(cmaps[i]) - mapped_sign_combinations.append(available_signs[i]) - cp_array.append(cps) - mapped_colours.append(mapped_colours_combi) - sign_combinations.append(mapped_sign_combinations) - cp_fail = axes[i1 - 1, i2].contourf( - X, Y, chi_fail_masked, - np.linspace(chi.min(), chi.max(), 10), - cmap='gray') - if f == len(combis) - 1: - if self.data_plot is True: - cbax = fig.add_axes([0.7*11/15, 0.9, 0.2, 0.02]) - else: - cbax = fig.add_axes([0.61, 0.9, 0.25, 0.02]) - clb = plt.colorbar( - cp_fail, cax=cbax, - orientation='horizontal') - clb.ax.set_title(r'$\chi^2$') - clb.ax.set_ylabel( - r'Invalid Region', rotation=0, - fontsize=10) - clb.ax.yaxis.set_label_coords(-0.2, 0.1) - clb.ax.tick_params( - labelcolor="none", bottom=False, left=False) - - if self.center_plot is True: - axes[i1 - 1, i2].plot( - self.best_params[i2], self.best_params[i1], - color='w', marker='o', - fillstyle='none', markersize=10) - if self.gridlines is True: - axes[i1 - 1, i2].vlines( - self.best_params[i2], p[:, 1].min(), - p[:, 1].max(), color='w', ls='--') - axes[i1 - 1, i2].hlines( - self.best_params[i1], p[:, 0].min(), - p[:, 0].max(), color='w', ls='--') - bar.finish() - - cbaxes = [] - height = 0.88 - - if self.N <= 5: - mapped_colours = list( - itertools.chain.from_iterable(mapped_colours)) - cp_array = list(itertools.chain.from_iterable(cp_array)) - sign_combinations = list( - itertools.chain.from_iterable(sign_combinations)) - unique_mapped_colours, indices = np.unique( - np.array(mapped_colours), return_index=True) - for i in range(len(unique_mapped_colours)): - if i > 0: - height -= 0.02 - if self.data_plot is True: - cbaxes.append( - fig.add_axes([0.7*11/15, height, 0.2, 0.02])) - else: - cbaxes.append( - fig.add_axes([0.61, height, 0.25, 0.02])) - count = 0 - for i in range(len(cp_array)): - if i in set(indices): - clb = plt.colorbar( - cp_array[i], cax=cbaxes[count], - orientation='horizontal') - clb.ax.set_ylabel( - r'$\mathbf{s}$ = ' + str(sign_combinations[i]), - rotation=0, fontsize=10) - clb.ax.yaxis.set_label_coords(-0.25, 0.1) - if i != indices.max(): - clb.ax.tick_params( - labelcolor="none", bottom=False, left=False) - else: - clb.ax.tick_params(rotation=90) - count += 1 - - plt.subplots_adjust(wspace=0.1, hspace=0.1) - - if self.data_plot is True: - fig.subplots_adjust(right=4.5/6.3) - fig.add_axes([11.5/15, 0.55, 3/15, 0.35]) - plt.plot(self.x, self.y, label='Data') - plt.ylabel(r'$y$') - y_sum = Models.Models_class( - self.best_params, self.x, self.y, - self.N, self.pivot_point, self.model_type, - self.new_basis).y_sum - plt.plot(self.x, y_sum, label='Fit') - plt.legend(loc=0) - fig.add_axes([11.5/15, 0.15, 3/15, 0.35]) - plt.plot( - self.x, self.y - y_sum, label='RMS = %2.1f' - % (np.sqrt(np.sum((self.y - y_sum)**2)/len(self.y)))) - plt.legend(loc=0) - plt.ylabel(r'$\delta y$') - plt.xlabel(r'$x$') - - plt.savefig(self.base_dir + 'Parameter_plot.pdf') - plt.close() diff --git a/maxsmooth/qp.py b/maxsmooth/qp.py index 00618c9..2726481 100755 --- a/maxsmooth/qp.py +++ b/maxsmooth/qp.py @@ -1,232 +1,295 @@ -from maxsmooth.derivatives import derivative_class -from maxsmooth.Models import Models_class -from cvxopt import matrix, solvers -import numpy as np -import warnings -from scipy.special import legendre, lpmv -import math - -warnings.simplefilter('always', UserWarning) - - -class qp_class(object): - def __init__( - self, x, y, N, signs, pivot_point, model_type, cvxopt_maxiter, - zero_crossings, initial_params, - constraints, new_basis): - self.model_type = model_type - self.pivot_point = pivot_point - self.y = y - self.x = x - self.N = N - self.signs = signs - self.cvxopt_maxiter = cvxopt_maxiter - self.zero_crossings = zero_crossings - self.initial_params = initial_params - self.basis_functions = new_basis['basis_function'] - self.derivative_pres = new_basis['der_pres'] - self.model = new_basis['model'] - self.derivatives_function = new_basis['derivatives_function'] - self.args = new_basis['args'] - self.new_basis = new_basis - self.constraints = constraints - self.parameters, self.chi_squared, self.zc_dict = self.fit() - - def fit(self): - - solvers.options['maxiters'] = self.cvxopt_maxiter - solvers.options['show_progress'] = False - - def constraint_prefactors(m): - # Derivative prefactors on parameters - if self.derivative_pres is None: - if np.any(self.model_type != ['legendre', 'exponential']): - derivatives = [] - for i in range(self.N): - if i <= m - 1: - derivatives.append([0]*len(self.x)) - for i in range(self.N-m): - if self.model_type == 'normalised_polynomial': - mth_order_derivative_term = ( - self.y[self.pivot_point] / - self.x[self.pivot_point]) \ - * math.factorial(m + i) \ - / math.factorial(i) * \ - (self.x)**i/(self.x[self.pivot_point])**(i + 1) - derivatives.append(mth_order_derivative_term) - if self.model_type == 'polynomial': - mth_order_derivative_term = math.factorial(m+i)\ - / math.factorial(i) * (self.x)**i - derivatives.append(mth_order_derivative_term) - if self.model_type == 'log_polynomial': - mth_order_derivative_term = math.factorial(m+i)\ - / math.factorial(i) * \ - np.log10(self.x/self.x[self.pivot_point])**i - derivatives.append(mth_order_derivative_term) - if self.model_type == 'loglog_polynomial': - mth_order_derivative_term = math.factorial(m+i)\ - / math.factorial(i) * np.log10(self.x)**i - derivatives.append(mth_order_derivative_term) - if self.model_type == 'difference_polynomial': - mth_order_derivative_term = math.factorial(m+i)\ - / math.factorial(i) * ( - self.x - self.x[self.pivot_point])**i - derivatives.append(mth_order_derivative_term) - - if self.derivative_pres is not None: - if self.args is None: - derivatives = self.derivative_pres( - m, self.x, self.y, self.N, self.pivot_point) - if self.args is not None: - derivatives = self.derivative_pres( - m, self.x, self.y, self.N, self.pivot_point, - *self.args) - - if self.model_type == 'legendre': - interval = np.linspace(-0.999, 0.999, len(self.x)) - alps = [] - for i in range(self.N): - alps.append(lpmv(m, i, interval)) - alps = np.array(alps) - derivatives = [] - for h in range(len(alps)): - derivatives.append( - ((alps[h, :]*(-1)**(m))/(1-interval**2)**(m/2))) - derivatives = np.array(derivatives) - if self.model_type == 'exponential': - derivatives = np.empty([self.N, len(self.x)]) - for i in range(self.N): - for h in range(len(self.x)): - derivatives[i, h] = \ - self.y[self.pivot_point] * \ - (np.exp(-i*self.x[h]/self.x[self.pivot_point])) * \ - (-i/self.x[self.pivot_point])**m - derivatives = np.array(derivatives) - - derivatives = np.array(derivatives).astype(np.double) - derivatives = matrix(derivatives) - if derivatives.size == (len(self.x), self.N): - pass - else: - derivatives = derivatives.T - return derivatives - - m = np.arange(0, self.N, 1) - derivatives = [] - signs = matrix(self.signs) - for i in range(len(m)): - if m[i] >= self.constraints: - if self.zero_crossings is not None: - if m[i] not in set(self.zero_crossings): - derivative_prefactors = constraint_prefactors(m[i]) - if derivative_prefactors != []: - derivatives.append(derivative_prefactors) - else: - derivative_prefactors = constraint_prefactors(m[i]) - if derivative_prefactors != []: - derivatives.append(derivative_prefactors) - - for i in range(len(derivatives)): - derivatives[i] *= signs[i] - - G = matrix(derivatives) - - if self.basis_functions is None: - phi = np.empty([len(self.x), self.N]) - if self.model_type != 'legendre': - for h in range(len(self.x)): - for i in range(self.N): - if self.model_type == 'normalised_polynomial': - phi[h, i] = self.y[self.pivot_point] * ( - self.x[h] / self.x[self.pivot_point])**i - if self.model_type == 'polynomial': - phi[h, i] = (self.x[h])**i - if self.model_type == 'log_polynomial': - phi[h, i] = \ - np.log10(self.x[h]/self.x[self.pivot_point])**i - if self.model_type == 'loglog_polynomial': - phi[h, i] = np.log10(self.x[h])**i - if self.model_type == 'difference_polynomial': - phi[h, i] = (self.x[h]-self.x[self.pivot_point])**i - if self.model_type == 'exponential': - phi[h, i] = self.y[self.pivot_point] * \ - np.exp(-i*self.x[h]/self.x[self.pivot_point]) - if self.model_type == 'legendre': - interval = np.linspace(-0.999, 0.999, len(self.x)) - phi = [] - for i in range(self.N): - P = legendre(i) - phi.append(P(interval)) - phi = np.array(phi).T - phi = matrix(phi) - if self.basis_functions is not None: - if self.args is None: - phi = self.basis_functions( - self.x, self.y, self.pivot_point, self.N) - phi = matrix(phi) - if self.args is not None: - phi = self.basis_functions( - self.x, self.y, self.pivot_point, self.N, *self.args) - phi = matrix(phi) - - if self.model_type == 'loglog_polynomial': - data_matrix = matrix( - np.log10(self.y).astype(np.double), (len(self.y), 1), - 'd') - else: - data_matrix = matrix( - self.y.astype(np.double), (len(self.y), 1), - 'd') - - if self.zero_crossings is None: - h = matrix(0.0, ((self.N-self.constraints)*len(self.x), 1), 'd') - else: - h = matrix( - 0.0, ( - (self.N-self.constraints-len(self.zero_crossings)) - * len(self.x), 1), 'd') - - Q = phi.T*phi - - q = -phi.T*data_matrix - - if self.initial_params is None: - qpfit = solvers.qp(Q, q, G, h) - if self.initial_params is not None: - print(self.initial_params) - initvals = {'x': matrix( - self.initial_params, (1, self.N), 'd')} - qpfit = solvers.qp(Q, q, G, h, initvals=initvals) - - parameters = qpfit['x'] - - if 'unknown' in qpfit['status']: - if qpfit['iterations'] == self.cvxopt_maxiter: - raise ValueError( - 'ERROR: "Maximum number of iterations reached in' + - ' cvxopt routine." Increase value of' + - ' setting.cvxopt_maxiter') - else: - parameters = np.array(matrix(0, (self.N, 1), 'd')) - if self.model_type == 'loglog_polynomial': - chi_squared = np.sum((np.log10(self.y))**2) - else: - chi_squared = np.sum((self.y)**2) - zc_dict = {} - else: - y = Models_class( - parameters, self.x, self.y, self.N, self.pivot_point, - self.model_type, self.new_basis).y_sum - der = derivative_class( - self.x, self.y, parameters, self.N, self.pivot_point, - self.model_type, self.zero_crossings, - self.constraints, self.new_basis) - zc_dict = der.zc_dict - - if self.model_type == 'loglog_polynomial': - chi_squared = np.sum((np.log10(self.y)-np.log10(y))**2) - else: - chi_squared = np.sum((self.y-y)**2) - parameters = np.array(parameters) - - return parameters, chi_squared, zc_dict +"""Quadratic programming call for maxsmooth.""" + +from collections.abc import Callable +from itertools import product + +import jax +import jaxopt +from jax import numpy as jnp +from jaxopt import OSQP + +from maxsmooth.derivatives import derivative_prefactors + +qpsolver = OSQP(maxiter=10000, tol=1e-3, eq_qp_solve="lu") + + +def qp( + x: jnp.ndarray, + y: jnp.ndarray, + N: int, + pivot_point: int, + function: Callable, + basis_function: Callable, + lowest_constrained_derivative: int = 2, +) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Set up and solve the quadratic programming problem for maxsmooth. + + Args: + x (jnp.ndarray): Input data points. + y (jnp.ndarray): Output data points. + N (int): Number of basis functions. + pivot_point (int): Index of the pivot point. + function (Callable): The model funciton from `maxsmooth.models`. + basis_function (Callable): The basis function to use. + lowest_constrained_derivative (int): The lowest derivative to + apply the constraints to. + + + Returns: + jnp.ndarray: state of the solver for each sign combination. + jnp.ndarray: the parameters of the fits. + jnp.ndarray: the reported error from jaxopt. + """ + x_pivot = x[pivot_point] + y_pivot = y[pivot_point] + # needs some dummy parameters to make basis + basis_function = jax.vmap(basis_function, in_axes=(0, None, None, None)) + basis = basis_function(x, x_pivot, y_pivot, jnp.ones(N)) + Q = jnp.dot(basis.T, basis) + + c = -jnp.dot(basis.T, y) + G = derivative_prefactors(function, x, x_pivot, y_pivot, jnp.ones(N), N)[ + lowest_constrained_derivative: + ] + G = jnp.array(G) + g_norm = jnp.linalg.norm(G, axis=2, keepdims=True) + g_norm = jnp.where(g_norm < 1e-10, 1.0, g_norm) # Avoid division by zero + G = G / g_norm + + all_signs = jnp.array(list(product((-1.0, 1.0), repeat=len(G)))) + + @jax.jit + def dcf( + signs: jnp.ndarray, c: jnp.ndarray, Q: jnp.ndarray + ) -> jaxopt._src.base.OptStep: + """Run the quadratic programming using jaxopt OSQP (ADMM). + + Args: + signs (jnp.ndarray): Sign combination + for the inequality constraints. + c (jnp.ndarray): Linear term in the objective function. + Q (jnp.ndarray): Quadratic term in the objective function. + + Returns: + sol: Solution of the quadratic programming problem. + """ + Gmat = signs[:, None, None] * G # if shapes align + Gmat = Gmat.reshape(-1, G.shape[2]) + h = jnp.zeros(Gmat.shape[0]) + sol = qpsolver.run(params_obj=(Q, c), params_ineq=(Gmat, h)) + return sol + + vmapped_dcf = jax.vmap(dcf, in_axes=(0, None, None)) + # Solve all QPs + sol = vmapped_dcf(all_signs, c, Q) + + vmapped_function = jax.vmap(function, in_axes=(0, None, None, None)) + + # map over each primal in sol.params.primal + @jax.jit + def obj_val_fn(primal: jnp.ndarray) -> jnp.ndarray: + return jnp.sum( + (y - vmapped_function(x, x_pivot, y_pivot, primal)) ** 2 + ) + + objective_values = jax.vmap(obj_val_fn)(sol.params.primal) + best_index = jnp.argmin(objective_values) + + return ( + sol.state.status[best_index], + sol.params.primal[best_index], + sol.state.error[best_index], + ) + + +def qpsignsearch( + x: jnp.ndarray, + y: jnp.ndarray, + N: int, + pivot_point: int, + function: Callable, + basis_function: Callable, + lowest_constrained_derivative: int = 2, + key: jnp.ndarray = jax.random.PRNGKey(0), +) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]: + """Set up and solve the quadratic programming problem for maxsmooth. + + slowqpsearch uses some elements of the searching algorithm + detailed in the maxsmooth paper to reduce the number of QP solves needed. + However it involves a lot of conditional logic which makes it slower + in JAX than the brute-fore try everything method for small problems. + + Args: + x (jnp.ndarray): Input data points. + y (jnp.ndarray): Output data points. + N (int): Number of basis functions. + pivot_point (int): Index of the pivot point. + function (Callable): The model funciton from `maxsmooth.models`. + basis_function (Callable): The basis function to use. + key (jnp.ndarray): JAX random key. + lowest_constrained_derivative (int): The lowest derivative to + apply the constraints to. + + Returns: + jnp.ndarray: state of the solver for each sign combination. + jnp.ndarray: the parameters of the fits. + jnp.ndarray: the reported error from jaxopt. + """ + + @jax.jit + def dcf( + signs: jnp.ndarray, + c: jnp.ndarray, + Q: jnp.ndarray, + ) -> jaxopt._src.base.OptStep: + """Run the quadratic programming using jaxopt OSQP (ADMM). + + Args: + signs (jnp.ndarray): Sign combination + for the inequality constraints. + c (jnp.ndarray): Linear term in the objective function. + Q (jnp.ndarray): Quadratic term in the objective function. + + Returns: + sol: Solution of the quadratic programming problem. + """ + Gmat = signs[:, None, None] * G # if shapes align + Gmat = Gmat.reshape(-1, G.shape[2]) + h = jnp.zeros(Gmat.shape[0]) + sol = qpsolver.run(params_obj=(Q, c), params_ineq=(Gmat, h)) + return sol + + x_pivot = x[pivot_point] + y_pivot = y[pivot_point] + # needs some dummy parameters to make basis + basis_function = jax.vmap(basis_function, in_axes=(0, None, None, None)) + basis = basis_function(x, x_pivot, y_pivot, jnp.ones(N)) + Q = jnp.dot(basis.T, basis) + + c = -jnp.dot(basis.T, y) + G = derivative_prefactors(function, x, x_pivot, y_pivot, jnp.ones(N), N)[ + lowest_constrained_derivative: + ] + + # square root of sum of squares of each row + G = jnp.array(G) + g_norm = jnp.linalg.norm(G, axis=2, keepdims=True) + g_norm = jnp.where(g_norm < 1e-10, 1.0, g_norm) # Avoid division by zero + G = G / g_norm + + all_signs = jnp.array(list(product((-1.0, 1.0), repeat=len(G)))) + + signs = jnp.array( + [ + jnp.ones(len(G)), + -jnp.ones(len(G)), + jnp.array([1 if i % 2 == 0 else -1 for i in range(len(G))]), + jnp.array([-1 if i % 2 == 0 else 1 for i in range(len(G))]), + ] + ) + + visited_signs = jnp.zeros(len(all_signs)) + visited_signs = jax.lax.fori_loop( + 0, + len(signs), + lambda i, vs: vs.at[ + jnp.where( + jnp.all(all_signs == signs[i], axis=1), # type: ignore + size=1, + fill_value=0, + )[0] + ].set(1), + visited_signs, + ) + + flip_sign = jax.vmap(lambda i, s: s.at[i].set(-s[i]), in_axes=(0, None)) + vmapped_dcf = jax.vmap(dcf, in_axes=(0, None, None)) + + sol = vmapped_dcf(signs, c, Q) + error = sol.state.error + minimum_index = jnp.argmin(error) + signs = signs[minimum_index] + error = error[minimum_index] + best_error = jnp.inf + + initial_state = ( + sol.state.status[minimum_index], + error, + best_error, + signs, + c, + Q, + sol.params.primal[minimum_index], + visited_signs, + ) + + def condition(state: tuple) -> bool: + _, error, best_error, _, _, _, _, _ = state + return error < best_error + + def body(state: tuple) -> tuple: + status, error, best_error, signs, c, Q, best_params, visited_signs = ( + state + ) + best_error = error + flip_signs = flip_sign(jnp.arange(len(signs)), signs) + + def body_unique_flip(i: int, fs: jnp.ndarray) -> jnp.ndarray: + """Check if flip sign has been visited already. + + Args: + i (int): Index in flip_signs. + fs (jnp.ndarray): Current flip signs. + + Returns: + jnp.ndarray: Updated flip signs with visited ones zeroed. + """ + index = jnp.where( + jnp.all(all_signs == flip_signs[i], axis=1), # type: ignore + size=1, + fill_value=-1, + )[0] + fs = jax.lax.cond( + visited_signs.at[index] == 1, + lambda f: jnp.zeros_like(f), + lambda f: f, + fs, + ) + return fs + + flip_signs = jax.lax.fori_loop( + 0, + len(flip_signs), + body_unique_flip, + flip_signs, + ) # type: ignore + + visited_signs = jax.lax.fori_loop( + 0, + len(flip_signs), + lambda i, vs: vs.at[ + jnp.where( + jnp.all(all_signs == flip_signs[i], axis=1), # type: ignore + size=1, + fill_value=0, + )[0] + ].set(1), + visited_signs, + ) + + sol = vmapped_dcf(flip_signs, c, Q) + minimum_index = jnp.argmin(jnp.array(sol.state.error)) + return ( + sol.state.status[minimum_index], + sol.state.error[minimum_index], + best_error, + flip_signs[minimum_index], + c, + Q, + sol.params.primal[minimum_index], + visited_signs, + ) + + results = jax.lax.while_loop(condition, body, initial_state) + + return results[0], results[6], jnp.array([]) diff --git a/maxsmooth/utils.py b/maxsmooth/utils.py new file mode 100644 index 0000000..cb93544 --- /dev/null +++ b/maxsmooth/utils.py @@ -0,0 +1,20 @@ +from jax.scipy.linalg import cholesky +from jax import numpy as jnp + +def is_positive_definite_cholesky(A: jnp.ndarray) -> bool: + """ + Checks if a symmetric matrix A is positive definite using Cholesky factorization. + + parameters: + ----------- + A : jnp.ndarray + Symmetric matrix to be checked. + """ + try: + _ = cholesky(A, lower=True, check_finite=True) + return True + except ValueError: + return False + except Exception as e: + print(f"An unexpected error occurred: {e}") + return False \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8466a87 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,36 @@ +site_name: maxsmooth +theme: + name: material + features: + - navigation.sections + - navigation.top + - search.highlight + - search.suggestions + palette: + - scheme: default + primary: blue + toggle: + icon: material/weather-night + name: Switch to dark mode + - scheme: slate + primary: blue + toggle: + icon: material/weather-sunny + name: Switch to light mode +plugins: + - include-markdown + - search + - mkdocstrings: + handlers: + python: + paths: [maxsmooth] + options: + show_root_heading: true + heading_level: 2 +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences +nav: + - Home: index.md + - Tutorials: tutorials.md + - API Reference: api-reference.md \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8441bc9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = ["setuptools>=77.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "maxsmooth" +version = "2.0.0" +description = "Derivative-constrained function fitting." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "jax>=0.8.0", + "jaxopt>=0.8.5", + "matplotlib>=3.10.7", + "numpy>=2.3.4", + "progressbar>=2.5", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["maxsmooth*"] + +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", + "pre-commit", +] +docs = [ + "mkdocs", + "mkdocs-material", + "mkdocstrings", + "mkdocstrings-python", + "mkdocs-include-markdown-plugin", + "pymdown-extensions", +] + +[tool.ruff] +line-length = 79 + +[tool.ruff.lint] +select =["E", "F", "W", # basics + "I", # isort + "D", # docstrings + "UP", # pyupgrade (includes type modernization) + "ANN", # type annotations + ] + +[tool.ruff.lint.pydocstyle] +convention = "google" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0552818..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -matplotlib -numpy -CVXOPT -scipy -progressbar diff --git a/setup.py b/setup.py deleted file mode 100644 index 5420251..0000000 --- a/setup.py +++ /dev/null @@ -1,37 +0,0 @@ -from setuptools import setup, find_packages - - -def readme(short=False): - with open('README.rst') as f: - if short: - return f.readlines()[1].strip() - else: - return f.read() - -setup( - name='maxsmooth', - version='1.2.2', - description='maxsmooth:Derivative Constrained Function Fitting', - long_description=readme(), - author='Harry T. J. Bevins', - author_email='htjb2@cam.ac.uk', - url='https://github.com/htjb/maxsmooth', - packages=find_packages(), - install_requires=['numpy', 'scipy', 'cvxopt', 'matplotlib', 'progressbar'], - license='MIT', - extras_require={ - 'docs': ['sphinx', 'sphinx_rtd_theme', 'numpydoc'], - }, - tests_require=['pytest'], - classifiers=[ - 'Intended Audience :: Science/Research', - 'Natural Language :: English', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Astronomy', - 'Topic :: Scientific/Engineering :: Physics', - ], -) diff --git a/tests/test_best_basis.py b/tests/test_best_basis.py deleted file mode 100644 index 40d78af..0000000 --- a/tests/test_best_basis.py +++ /dev/null @@ -1,74 +0,0 @@ -import numpy as np -import os -import shutil -import pytest -from maxsmooth.best_basis import basis_test - -def test_keywords(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(0, 1, Ndat) - y = 1 + x + x**2 + x**3 - - with pytest.raises(Exception): - basis_test(x, y, colour='pink') - with pytest.raises(Exception): - basis_test(x, y, fit_type='pink') - with pytest.raises(Exception): - basis_test(x, y, base_dir='output_files') - with pytest.raises(Exception): - basis_test(x, y, base_dir=5) - with pytest.raises(Exception): - basis_test(x, y, N='banana') - with pytest.raises(Exception): - basis_test(x, y, N=[3.3]) - with pytest.raises(Exception): - basis_test(x, y, pivot_point='banana') - with pytest.raises(Exception): - basis_test(x, y, pivot_point=112) - with pytest.raises(Exception): - basis_test(x, y, constraints='banana') - with pytest.raises(Exception): - basis_test(x, y, constraints=16) - with pytest.raises(Exception): - basis_test(x, y, zero_crossings=[6.6, 7.2]) - with pytest.raises(Exception): - basis_test(x, y, constraints=5, zero_crossings=[3]) - with pytest.raises(Exception): - basis_test(x, y, chi_squared_limit='banana') - with pytest.raises(Exception): - basis_test(x, y, cap=45.5) - with pytest.raises(Exception): - basis_test(x, y, cvxopt_maxiter=22.2) - -def test_directory(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(0,1,Ndat) - y = 1 + x + x**2 + x**3 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - basis_test(x, y, base_dir='new_dir/', N=[4, 5], pivot_point=0) - - assert(os.path.exists('new_dir/') is True) - assert(os.path.exists('new_dir/Basis_functions.pdf') is True) - -def test_loglog(): - - Ndat = 100 - x = np.linspace(50, 150, Ndat) - y = 5e7*x**(-2.5) - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - basis_test(x, y, base_dir='new_dir/', N=[4, 5], chi_squared_limit=200) - - assert(os.path.exists('new_dir/') is True) - assert(os.path.exists('new_dir/Basis_functions.pdf') is True) diff --git a/tests/test_chi_dist_plotter.py b/tests/test_chi_dist_plotter.py deleted file mode 100644 index 38e16cb..0000000 --- a/tests/test_chi_dist_plotter.py +++ /dev/null @@ -1,217 +0,0 @@ -import numpy as np -from maxsmooth.chidist_plotter import chi_plotter -from maxsmooth.DCF import smooth -import pytest -import os -import shutil - -def test_keywords(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(0,1,Ndat) - y = 1 + x + x**2 + x**3 - - N = 4 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth(x, y, N, base_dir='new_dir/', data_save=True) - - with pytest.raises(Exception): - chi_plotter(N='Banana', base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N=3.3, base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, base_dir='new_dir') - with pytest.raises(Exception): - chi_plotter(N, base_dir=5) - with pytest.raises(Exception): - chi_plotter(N, color='pink', base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, constraints=4.3, base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, constraints=7, base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, zero_crossings=[3.3], base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, zero_crossings=[1], base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, zero_crossings='string', base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, fit_type='pink', base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, chi_squared_limit='banana', base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, cap=5.5, base_dir='new_dir/') - with pytest.raises(Exception): - chi_plotter(N, plot_limits='pink', base_dir='new_dir/') - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - with pytest.raises(Exception): - chi_plotter(N, base_dir='new_dir/') - -def test_directory(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(0,1,Ndat) - y = 1 + x + x**2 + x**3 - - N = 4 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth(x, y, N, base_dir='new_dir/') - - with pytest.raises(Exception): - chi_plotter(N) - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth(x, y, N, base_dir='new_dir/', data_save=True) - - if os.path.isdir('new_dir/Output_Signs/'): - shutil.rmtree('new_dir/Output_Signs/') - - with pytest.raises(Exception): - chi_plotter(N, base_dir='new_dir/') - -def test_files(): - - Ndat = 100 - x = np.linspace(50, 150, Ndat) - y = 5e7*x**(-2.5) - - N = 4 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth( - x, y, N, base_dir='new_dir/', - data_save=True, model_type='log_polynomial') - - plot = chi_plotter(N, base_dir='new_dir/') - - assert(plot.chi is None) - assert(plot.cap == ((2**(N-2))//N) + N) - assert(os.path.exists('new_dir/chi_distribution.pdf') is True) - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth(x, y, N, base_dir='new_dir/', data_save=True, fit_type='qp') - - plot = chi_plotter(N, base_dir='new_dir/', fit_type='qp') - - assert(plot.chi is None) - assert(plot.cap == ((2**(N-2))//N) + N) - assert(os.path.exists('new_dir/chi_distribution.pdf') is True) - -def test_chi(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(50, 150, Ndat) - y = 5e7*x**(-2.5) - - N = 4 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth( - x, y, N, base_dir='new_dir/', data_save=True, - model_type='loglog_polynomial', fit_type='qp') - - def model(x, N, params): - y_sum = 10**(np.sum([ - params[i]*np.log10(x)**i - for i in range(N)], - axis=0)) - return y_sum - - parameters = np.loadtxt( - 'new_dir/Output_Parameters/4_qp.txt') - - chi_out = [] - for i in range(len(parameters)): - chi_out.append(np.sum((y - model(x, N, parameters[i]))**2)) - chi_out = np.array(chi_out) - - plot = chi_plotter(N, base_dir='new_dir/', chi=chi_out, - fit_type='qp') - - assert(np.all(plot.chi == chi_out)) - assert(plot.chi_squared_limit == 2*min(chi_out)) - assert(os.path.exists('new_dir/chi_distribution.pdf') is True) - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - smooth( - x, y, N, base_dir='new_dir/', data_save=True, - model_type='loglog_polynomial') - - parameters = np.loadtxt( - 'new_dir/Output_Parameters/4_qp-sign_flipping.txt') - - chi_out = [] - for i in range(len(parameters)): - chi_out.append(np.sum((y - model(x, N, parameters[i]))**2)) - chi_out = np.array(chi_out) - - plot = chi_plotter(N, base_dir='new_dir/', chi=chi_out) - - assert(np.all(plot.chi == chi_out)) - assert(plot.chi_squared_limit == 2*min(chi_out)) - assert(os.path.exists('new_dir/chi_distribution.pdf') is True) - -def test_ifp(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(-1, 1, Ndat) - y = 1 + x + x**2 + x**3 + np.random.normal(0, 0.05, 100) - - N = 10 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - sol = smooth( - x, y, N, zero_crossings=[4, 5, 6], constraints=1, base_dir='new_dir/', - data_save=True) - - chi_plotter(N, base_dir='new_dir/', zero_crossings=[4, 5, 6], constraints=1) - assert(os.path.exists('new_dir/chi_distribution.pdf') is True) - -def test_limits(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(-1, 1, Ndat) - y = 1 + x + x**2 + x**3 + np.random.normal(0, 0.05, 100) - - N = 10 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - sol = smooth(x, y, N, base_dir='new_dir/', data_save=True) - - chi_plotter( - N, base_dir='new_dir/', cap=10, chi_squared_limit=1e4, - plot_limits=True) - assert(os.path.exists('new_dir/chi_distribution.pdf') is True) diff --git a/tests/test_param_plotter.py b/tests/test_param_plotter.py deleted file mode 100644 index 869d422..0000000 --- a/tests/test_param_plotter.py +++ /dev/null @@ -1,303 +0,0 @@ -import numpy as np -import math -import pytest -import os -import shutil -from maxsmooth.DCF import smooth -from maxsmooth.parameter_plotter import param_plotter - -def test_keywords(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(0,1,Ndat) - y = 1 + x + x**2 + x**3 - - N = 4 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - res = smooth(x, y, N, base_dir='new_dir/', data_save=True) - - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, color='pink') - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, 5.5) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, model_type='banana') - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, pivot_point='string') - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, pivot_point=len(x)+10) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, base_dir=5) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, base_dir='string') - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, constraints=3.3) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, constraints=20) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, zero_crossings=[3.3]) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, zero_crossings=[1]) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, samples=50.2) - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, width='string') - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, warnings='string') - with pytest.raises(Exception): - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, gridlines=9) - -def test_files(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(0,1,Ndat) - y = 1 + x + x**2 + x**3 - - N = 6 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - res = smooth(x, y, N, zero_crossings=[4]) - - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, base_dir='new_dir/', samples=10, zero_crossings=[4]) - - assert(os.path.exists('new_dir/') is True) - assert(os.path.exists('new_dir/Parameter_plot.pdf') is True) - - N = 10 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - res = smooth(x, y, N) - - param_plotter( - res.optimum_params, res.optimum_signs, - x, y, N, base_dir='new_dir/', samples=10, gridlines=True) - - assert(os.path.exists('new_dir/') is True) - assert(os.path.exists('new_dir/Parameter_plot.pdf') is True) - -def test_new_basis(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(-1, 1, Ndat) - y = 1 + x + x**2 + x**3 + np.random.normal(0, 0.05, 100) - - N = 4 - - arguments = [x[-1]*10, y[-1]*10] - - def basis_functions(x, y, pivot_point, N, *args): - - phi = np.empty([len(x), N]) - for h in range(len(x)): - for i in range(N): - phi[h, i] = args[1]*(x[h]/args[0])**i - - return phi - - def model(x, y, pivot_point, N, params, *args): - - y_sum = args[1]*np.sum([ - params[i]*(x/args[0])**i - for i in range(N)], axis=0) - - return y_sum - - def derivative(m, x, y, N, pivot_point, params, *args): - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = args[1]*math.factorial(m+i) / \ - math.factorial(i) * \ - params[int(m)+i]*(x)**i / \ - (args[0])**(i + 1) - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - - def derivative_pre(m, x, y, N, pivot_point, *args): - - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = args[1]*math.factorial(m+i) / \ - math.factorial(i) * \ - (x)**i / \ - (args[0])**(i + 1) - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - - sol = smooth(x, y, N, basis_functions=basis_functions, model=model, - derivatives=derivative, der_pres=derivative_pre, args=arguments) - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - param_plotter( - sol.optimum_params, sol.optimum_signs, - x, y, N, samples=10, base_dir='new_dir/', - basis_functions=basis_functions, - model=model, - derivatives=derivative, der_pres=derivative_pre, args=arguments) - - assert(os.path.exists('new_dir/') is True) - assert(os.path.exists('new_dir/Parameter_plot.pdf') is True) - - with pytest.raises(Exception): - param_plotter( - sol.optimum_params, sol.optimum_signs, - x, y, N, samples=10, base_dir='new_dir/', - basis_functions=basis_functions, - model=None, - derivatives=derivative, der_pres=derivative_pre, args=arguments) - -def test_new_basis_without_args(): - - np.random.seed(0) - - Ndat = 100 - x = np.linspace(-1, 1, Ndat) - y = 1 + x + x**2 + x**3 + np.random.normal(0, 0.05, 100) - - N = 4 - - def basis_functions(x, y, pivot_point, N, *args): - - phi = np.empty([len(x), N]) - for h in range(len(x)): - for i in range(N): - phi[h, i] = (x[h])**i - - return phi - - def model(x, y, pivot_point, N, params, *args): - - y_sum = np.sum([ - params[i]*(x)**i - for i in range(N)], axis=0) - - return y_sum - - def derivative(m, x, y, N, pivot_point, params, *args): - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = math.factorial(m+i) / \ - math.factorial(i) * \ - params[int(m)+i]*(x)**i - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - - def derivative_pre(m, x, y, N, pivot_point, *args): - - mth_order_derivative = [] - for i in range(N): - if i <= m - 1: - mth_order_derivative.append([0]*len(x)) - for i in range(N - m): - mth_order_derivative_term = math.factorial(m+i) / \ - math.factorial(i) * \ - (x)**i - mth_order_derivative.append( - mth_order_derivative_term) - - return mth_order_derivative - - sol = smooth(x, y, N, basis_functions=basis_functions, model=model, - derivatives=derivative, der_pres=derivative_pre) - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - param_plotter( - sol.optimum_params, sol.optimum_signs, - x, y, N, samples=10, base_dir='new_dir/', - basis_functions=basis_functions, - model=model, - derivatives=derivative, der_pres=derivative_pre) - - assert(os.path.exists('new_dir/') is True) - assert(os.path.exists('new_dir/Parameter_plot.pdf') is True) - - with pytest.raises(Exception): - param_plotter( - sol.optimum_params, sol.optimum_signs, - x, y, N, samples=10, base_dir='new_dir/', - basis_functions=basis_functions, - model=None, - derivatives=derivative, der_pres=None) - -def test_loglog(): - - Ndat = 100 - x = np.linspace(50, 150, Ndat) - y = 5e7*x**(-2.5) - - N = 4 - - if os.path.isdir('new_dir/'): - shutil.rmtree('new_dir/') - - sol = smooth(x, y, N, model_type='loglog_polynomial') - - param_plotter( - sol.optimum_params, sol.optimum_signs, - x, y, N, samples=10, base_dir='new_dir/', - model_type='loglog_polynomial', data_plot=True, center_plot=True) - - assert(os.path.exists('new_dir/Parameter_plot.pdf') is True)