pharmapy-org 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pharmapy_org-0.1.0/LICENSE.md +41 -0
- pharmapy_org-0.1.0/PKG-INFO +64 -0
- pharmapy_org-0.1.0/PharmaPy/CakePhysics.py +117 -0
- pharmapy_org-0.1.0/PharmaPy/Calibration.py +300 -0
- pharmapy_org-0.1.0/PharmaPy/CheckModule.py +70 -0
- pharmapy_org-0.1.0/PharmaPy/Classifier.py +275 -0
- pharmapy_org-0.1.0/PharmaPy/Commons.py +718 -0
- pharmapy_org-0.1.0/PharmaPy/Connections.py +581 -0
- pharmapy_org-0.1.0/PharmaPy/Containers.py +1006 -0
- pharmapy_org-0.1.0/PharmaPy/Crystallizers.py +2229 -0
- pharmapy_org-0.1.0/PharmaPy/Crystallizers_Refactored.py +215 -0
- pharmapy_org-0.1.0/PharmaPy/DataClasses.py +529 -0
- pharmapy_org-0.1.0/PharmaPy/Distillation.py +905 -0
- pharmapy_org-0.1.0/PharmaPy/Distributions.py +142 -0
- pharmapy_org-0.1.0/PharmaPy/Drying_Model.py +734 -0
- pharmapy_org-0.1.0/PharmaPy/DynamicExtraction.py +600 -0
- pharmapy_org-0.1.0/PharmaPy/Errors.py +23 -0
- pharmapy_org-0.1.0/PharmaPy/Evaporators.py +1972 -0
- pharmapy_org-0.1.0/PharmaPy/Extractors.py +305 -0
- pharmapy_org-0.1.0/PharmaPy/Gaussians.py +138 -0
- pharmapy_org-0.1.0/PharmaPy/IntegratorBackends.py +1885 -0
- pharmapy_org-0.1.0/PharmaPy/Interpolation.py +305 -0
- pharmapy_org-0.1.0/PharmaPy/Kinetics.py +1078 -0
- pharmapy_org-0.1.0/PharmaPy/LevMarq.py +176 -0
- pharmapy_org-0.1.0/PharmaPy/Mechanisms.py +2034 -0
- pharmapy_org-0.1.0/PharmaPy/MetaModeler.py +273 -0
- pharmapy_org-0.1.0/PharmaPy/MixedPhases.py +648 -0
- pharmapy_org-0.1.0/PharmaPy/MixedPhases_Refactored.py +366 -0
- pharmapy_org-0.1.0/PharmaPy/MultiPhaseVessel.py +3204 -0
- pharmapy_org-0.1.0/PharmaPy/NameAnalysis.py +368 -0
- pharmapy_org-0.1.0/PharmaPy/ParamEstim.py +1382 -0
- pharmapy_org-0.1.0/PharmaPy/Phases.py +1227 -0
- pharmapy_org-0.1.0/PharmaPy/Phases_Refactored.py +1206 -0
- pharmapy_org-0.1.0/PharmaPy/Plotting.py +386 -0
- pharmapy_org-0.1.0/PharmaPy/ProcessControl.py +64 -0
- pharmapy_org-0.1.0/PharmaPy/ProcessControl_Refactored.py +491 -0
- pharmapy_org-0.1.0/PharmaPy/Reactors.py +2071 -0
- pharmapy_org-0.1.0/PharmaPy/Reactors_Refactored.py +113 -0
- pharmapy_org-0.1.0/PharmaPy/Results.py +345 -0
- pharmapy_org-0.1.0/PharmaPy/SimExec.py +719 -0
- pharmapy_org-0.1.0/PharmaPy/SolidLiquidSep.py +1299 -0
- pharmapy_org-0.1.0/PharmaPy/SolidLiquidSep_Refactored.py +871 -0
- pharmapy_org-0.1.0/PharmaPy/StatsModule.py +508 -0
- pharmapy_org-0.1.0/PharmaPy/Streams.py +287 -0
- pharmapy_org-0.1.0/PharmaPy/Streams_Refactored.py +582 -0
- pharmapy_org-0.1.0/PharmaPy/ThermoModule.py +967 -0
- pharmapy_org-0.1.0/PharmaPy/ThreePhaseSettler.py +236 -0
- pharmapy_org-0.1.0/PharmaPy/Utilities.py +71 -0
- pharmapy_org-0.1.0/PharmaPy/__init__.py +0 -0
- pharmapy_org-0.1.0/PharmaPy/_assimulo.py +203 -0
- pharmapy_org-0.1.0/PharmaPy/animate_profiles.py +255 -0
- pharmapy_org-0.1.0/PharmaPy/data/evaporator/props_nitrogen.json +18 -0
- pharmapy_org-0.1.0/PharmaPy/data/minimum_modeling_objects.json +7 -0
- pharmapy_org-0.1.0/PharmaPy/data/thermodynamics/unifac_interaction_params.csv +757 -0
- pharmapy_org-0.1.0/PharmaPy/data/thermodynamics/unifac_rk_qk.csv +126 -0
- pharmapy_org-0.1.0/PharmaPy/general_interpolation.py +29 -0
- pharmapy_org-0.1.0/PharmaPy/jac_module.py +184 -0
- pharmapy_org-0.1.0/README.md +36 -0
- pharmapy_org-0.1.0/pharmapy_org.egg-info/PKG-INFO +64 -0
- pharmapy_org-0.1.0/pharmapy_org.egg-info/SOURCES.txt +72 -0
- pharmapy_org-0.1.0/pharmapy_org.egg-info/dependency_links.txt +1 -0
- pharmapy_org-0.1.0/pharmapy_org.egg-info/requires.txt +12 -0
- pharmapy_org-0.1.0/pharmapy_org.egg-info/top_level.txt +1 -0
- pharmapy_org-0.1.0/pyproject.toml +70 -0
- pharmapy_org-0.1.0/setup.cfg +4 -0
- pharmapy_org-0.1.0/tests/test_distribution_basis.py +252 -0
- pharmapy_org-0.1.0/tests/test_multiphase_vessel_batch_crystallizer.py +617 -0
- pharmapy_org-0.1.0/tests/test_multiphase_vessel_continuous_crystallizer.py +446 -0
- pharmapy_org-0.1.0/tests/test_multiphase_vessel_reactor.py +351 -0
- pharmapy_org-0.1.0/tests/test_optional_assimulo_imports.py +301 -0
- pharmapy_org-0.1.0/tests/test_package_data.py +32 -0
- pharmapy_org-0.1.0/tests/test_scipy_backend.py +393 -0
- pharmapy_org-0.1.0/tests/test_solid_liquid_sep_refactored.py +296 -0
- pharmapy_org-0.1.0/tests/test_vessel_state_packing.py +175 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
License
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Pythonic framework for the digital analysis of pharmaceutical processes: PharmaPy
|
|
5
|
+
Copyright (c) 2021, by the software owners: Purdue University.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
8
|
+
are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation and/or
|
|
15
|
+
other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name PharmaPy, Purdue University, nor the names of its contributors
|
|
18
|
+
may be used to endorse or promote products derived from this software without specific
|
|
19
|
+
prior written permission.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
23
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
24
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
25
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
26
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
27
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
28
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
29
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
30
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
31
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
+
|
|
33
|
+
You are under no obligation whatsoever to provide any bug fixes, patches, or
|
|
34
|
+
upgrades to the features, functionality or performance of the source code
|
|
35
|
+
("Enhancements") to anyone; however, if you choose to make your Enhancements
|
|
36
|
+
available either publicly, or directly to Purdue University, without imposing
|
|
37
|
+
a separate written license agreement for such Enhancements, then you hereby
|
|
38
|
+
grant Purdue Univeristy the following license: a non-exclusive, royalty-free
|
|
39
|
+
perpetual license to install, use, modify, prepare derivative works, incorporate
|
|
40
|
+
into other computer software, distribute, and sublicense such enhancements or
|
|
41
|
+
derivative works thereof, in binary and source code form.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pharmapy-org
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Pythonic framework for the digital analysis of pharmaceutical manufacturing processes.
|
|
5
|
+
Author-email: Daniel Casas-Orozco <dcasasor@purdue.edu>, Zachary Hillman <zachary.hillman18@gmail.com>
|
|
6
|
+
Maintainer-email: Zachary Hillman <zachary.hillman18@gmail.com>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/PharmaPy-org/PharmaPy
|
|
9
|
+
Project-URL: Repository, https://github.com/PharmaPy-org/PharmaPy
|
|
10
|
+
Keywords: pharmaceutical,process simulation,flowsheet,crystallization,chemical engineering
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.md
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: scipy
|
|
21
|
+
Requires-Dist: matplotlib
|
|
22
|
+
Requires-Dist: pandas
|
|
23
|
+
Provides-Extra: julia
|
|
24
|
+
Requires-Dist: diffeqpy>=2.6; python_version >= "3.10" and extra == "julia"
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# PharmaPy
|
|
30
|
+
|
|
31
|
+
PharmaPy is a pythonic library for the analysis of pharmaceutical manufacturing systems.
|
|
32
|
+
|
|
33
|
+
It allows to simulate the dynamics of standalone, drug substance unit operations in a variety of operating modes (batch, continuous, semibatch). Also, PharmaPy facilitates setting up and simulating pharmaceutical **flowsheets**, i.e., interconnected unit operations in a variety of operation modes, ranging from end-to-end batch, end-to-end continuous, and hybrid operation (combination of batch and/or continuous and semicontinuous unit operations).
|
|
34
|
+
|
|
35
|
+
## Getting started
|
|
36
|
+
Source code, examples and documentation sources are on [GitHub](https://github.com/PharmaPy-org/PharmaPy).
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
PharmaPy is published on PyPI as `pharmapy-org`, and you import it as `PharmaPy`:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install pharmapy-org
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import PharmaPy
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The only integrator this installs is scipy's. Unit operations built on `MultiPhaseVessel` (the `*_Refactored` modules) default to `ScipyBackend`. Two other solver backends are optional:
|
|
50
|
+
|
|
51
|
+
- **Assimulo (SUNDIALS CVode/IDA)** is used by `AssimuloBackend` and `AssimuloDAEBackend`. It is also needed by the legacy unit operations that have not been refactored yet: `Reactors`, `Crystallizers`, `Evaporators`, `Distillation`, `SolidLiquidSep`, `Containers`, `Drying_Model`, `DynamicExtraction` and `ThreePhaseSettler`. Install it from conda-forge, because the PyPI `assimulo` package is outdated and has no wheels:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
conda install -c conda-forge assimulo
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **Julia (DifferentialEquations.jl)** is used by `DiffeqpyBackend` and needs Python 3.10 or newer:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
pip install "pharmapy-org[julia]"
|
|
61
|
+
python -c "import diffeqpy; diffeqpy.install()"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To get a development install with every backend, clone the [GitHub repository](https://github.com/PharmaPy-org/PharmaPy) and follow `install_instructions.txt`. It sets up a conda environment that includes Assimulo and installs PharmaPy in editable mode.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Cake physics correlations shared by the solid-liquid separation units.
|
|
4
|
+
|
|
5
|
+
These four functions carry no state and belong to no class. They were defined
|
|
6
|
+
at the top of ``SolidLiquidSep`` and imported from there by ``Drying_Model``,
|
|
7
|
+
which made a legacy module part of another module's public API. Keeping them
|
|
8
|
+
here lets the legacy units, the refactored ones and the dryer draw on one copy
|
|
9
|
+
instead of drifting apart.
|
|
10
|
+
|
|
11
|
+
``SolidLiquidSep`` re-exports every name below, so the original import path
|
|
12
|
+
still works.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
eps = np.finfo(float).eps * 1.1
|
|
18
|
+
grav = 9.8 # m/s**2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def high_resolution_fvm(f, boundary_cond, limiter_type='Van Leer'):
|
|
22
|
+
|
|
23
|
+
# Ghost cells -1, 0 and N + 1 (see LeVeque 2002, Chapter 9)
|
|
24
|
+
f_extrap = 2*f[-1] - f[-2]
|
|
25
|
+
f_aug = np.concatenate(([boundary_cond]*2, f, [f_extrap]))
|
|
26
|
+
|
|
27
|
+
f_diff = np.diff(f_aug, axis=0)
|
|
28
|
+
|
|
29
|
+
theta = (f_diff[:-1]) / (f_diff[1:] + eps)
|
|
30
|
+
|
|
31
|
+
if limiter_type == 'Van Leer':
|
|
32
|
+
limiter = (np.abs(theta) + theta) / (1 + np.abs(theta))
|
|
33
|
+
else: # TODO: include more limiters
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
fluxes = f_aug[1:-1] + 0.5 * f_diff[1:] * limiter
|
|
37
|
+
|
|
38
|
+
return fluxes
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def upwind_fvm(f, boundary_cond):
|
|
42
|
+
f_aug = np.concatenate(([boundary_cond], f))
|
|
43
|
+
|
|
44
|
+
return f_aug
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_alpha(solid_phase, porosity, sphericity, rho_sol, csd=None):
|
|
48
|
+
# if csd is None:
|
|
49
|
+
# csd = solid_phase.distrib
|
|
50
|
+
|
|
51
|
+
# x_grid = solid_phase.x_distrib
|
|
52
|
+
|
|
53
|
+
# alpha_x = 180 * (1 - porosity) / \
|
|
54
|
+
# (porosity**3 * (x_grid*1e-6)**2 * rho_sol * sphericity**2)
|
|
55
|
+
|
|
56
|
+
# numerator = trapezoidal_rule(x_grid, csd * alpha_x)
|
|
57
|
+
# denominator = solid_phase.moments[0]
|
|
58
|
+
|
|
59
|
+
# alpha = numerator / (denominator + eps)
|
|
60
|
+
csd = solid_phase.distrib
|
|
61
|
+
rho_sol = solid_phase.getDensity()
|
|
62
|
+
x_grid = solid_phase.x_distrib * 1e-6
|
|
63
|
+
|
|
64
|
+
kv = 0.524 # converting number based CSD to volume based:
|
|
65
|
+
|
|
66
|
+
del_x_dist = np.diff(x_grid)
|
|
67
|
+
node_x_dist = (x_grid[:-1] + x_grid[1:]) / 2
|
|
68
|
+
node_CSD = (csd[:-1] + csd[1:]) / 2
|
|
69
|
+
|
|
70
|
+
# Volume of crystals in each bin
|
|
71
|
+
vol_cry = node_CSD * del_x_dist * (kv * node_x_dist**3)
|
|
72
|
+
frac_vol_cry = vol_cry / (np.sum(vol_cry) + eps)
|
|
73
|
+
|
|
74
|
+
csd = vol_cry
|
|
75
|
+
|
|
76
|
+
# Calculate irreducible saturation in weighted csd (volume based)
|
|
77
|
+
vol_frac = vol_cry/ np.sum(vol_cry)
|
|
78
|
+
x_grid = node_x_dist
|
|
79
|
+
alpha_x = 180 * (1 - porosity) / porosity**3 / x_grid**2 / rho_sol
|
|
80
|
+
alpha = np.sum(alpha_x * vol_frac)
|
|
81
|
+
|
|
82
|
+
return alpha
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_sat_inf(x_vec, csd, deltaP, porosity, height, mu_zero, props):
|
|
86
|
+
surf_tens, rho_liq = props
|
|
87
|
+
|
|
88
|
+
kv = 0.524 # converting number based CSD to volume based:
|
|
89
|
+
|
|
90
|
+
del_x_dist = np.diff(x_vec)
|
|
91
|
+
node_x_dist = (x_vec[:-1] + x_vec[1:]) / 2
|
|
92
|
+
node_CSD = (csd[:-1] + csd[1:]) / 2
|
|
93
|
+
|
|
94
|
+
x_vec = node_x_dist
|
|
95
|
+
if isinstance(surf_tens, float) or isinstance(rho_liq, float):
|
|
96
|
+
capillary_number = porosity**3 * x_vec**2 * \
|
|
97
|
+
(rho_liq*grav*height + deltaP) / (1 - porosity)**2 / height / surf_tens
|
|
98
|
+
else:
|
|
99
|
+
capillary_number = np.outer(
|
|
100
|
+
porosity**3 * x_vec**2,
|
|
101
|
+
(rho_liq*grav*height + deltaP)/(1 - porosity)**2 / height / surf_tens
|
|
102
|
+
)
|
|
103
|
+
# Volume of crystals in each bin
|
|
104
|
+
vol_cry = node_CSD * del_x_dist * (kv * node_x_dist**3)
|
|
105
|
+
frac_vol_cry = vol_cry / (np.sum(vol_cry) + eps)
|
|
106
|
+
|
|
107
|
+
csd = vol_cry
|
|
108
|
+
|
|
109
|
+
s_inf = 0.155 * (1 + 0.031*capillary_number**(-0.49))
|
|
110
|
+
s_inf = np.where(s_inf > 1, 1, s_inf)
|
|
111
|
+
|
|
112
|
+
# Calculate irreducible saturation in weighted csd (volume based)
|
|
113
|
+
vol_frac = vol_cry/ np.sum(vol_cry)
|
|
114
|
+
|
|
115
|
+
s_inf = np.sum(vol_frac *s_inf)
|
|
116
|
+
|
|
117
|
+
return s_inf
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created on Tue Nov 3 11:29:05 2020
|
|
5
|
+
|
|
6
|
+
@author: dcasasor
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import itertools
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
from matplotlib.ticker import AutoMinorLocator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PCR_calibration:
|
|
16
|
+
def __init__(self, data, num_comp=None, standardize=True, snv=False,
|
|
17
|
+
y_name=None, y_suffixes=None):
|
|
18
|
+
|
|
19
|
+
self.data = data
|
|
20
|
+
self.standardize = standardize
|
|
21
|
+
self.snv = snv
|
|
22
|
+
|
|
23
|
+
data_mean = data.mean(axis=0)
|
|
24
|
+
data_std = data.std(axis=0)
|
|
25
|
+
|
|
26
|
+
if snv:
|
|
27
|
+
self.data_centered = self.__center_data(data)
|
|
28
|
+
else:
|
|
29
|
+
self.data_centered = self.__center_data(data, data_mean, data_std)
|
|
30
|
+
# self.data_centered = self.__center_data(data, None, None)
|
|
31
|
+
|
|
32
|
+
self.data_mean = data_mean
|
|
33
|
+
self.data_std = data_std
|
|
34
|
+
|
|
35
|
+
(self.projections, self.explained_variance,
|
|
36
|
+
self.svd_dict) = self.__get_projections()
|
|
37
|
+
|
|
38
|
+
if num_comp is None:
|
|
39
|
+
self.num_comp = len(self.svd_dict['sv'])
|
|
40
|
+
else:
|
|
41
|
+
self.num_comp = num_comp
|
|
42
|
+
|
|
43
|
+
if y_name is None:
|
|
44
|
+
y_name = 'y_'
|
|
45
|
+
|
|
46
|
+
self.y_name = y_name
|
|
47
|
+
self.y_suffixes = y_suffixes
|
|
48
|
+
|
|
49
|
+
def __center_data(self, data=None, mean=None, std=None):
|
|
50
|
+
if mean is None and std is None:
|
|
51
|
+
mean = data.mean(axis=0)
|
|
52
|
+
std = data.std(axis=0)
|
|
53
|
+
|
|
54
|
+
data_centered = data - mean
|
|
55
|
+
|
|
56
|
+
if self.snv or self.standardize:
|
|
57
|
+
data_centered *= 1 / std
|
|
58
|
+
|
|
59
|
+
return data_centered
|
|
60
|
+
|
|
61
|
+
def __get_projections(self, data=None, n_comp=None):
|
|
62
|
+
|
|
63
|
+
if data is None:
|
|
64
|
+
data = self.data_centered
|
|
65
|
+
|
|
66
|
+
# Perform SVD
|
|
67
|
+
u_m, sv, v_nt = np.linalg.svd(data)
|
|
68
|
+
v_n = v_nt.T
|
|
69
|
+
|
|
70
|
+
# Percent of explained variance
|
|
71
|
+
explained_var = sv**2 / (sv**2).sum() * 100
|
|
72
|
+
|
|
73
|
+
# Store SVD in a dict
|
|
74
|
+
v_trunc = v_nt[:len(sv)].T
|
|
75
|
+
|
|
76
|
+
svd_dict = {'U': u_m, 'sv': sv, 'V': v_n, 'V_trunc': v_trunc}
|
|
77
|
+
|
|
78
|
+
# Projections
|
|
79
|
+
projections = np.dot(data, v_n)
|
|
80
|
+
|
|
81
|
+
if n_comp is not None:
|
|
82
|
+
projections = projections[:, :n_comp]
|
|
83
|
+
|
|
84
|
+
return projections, explained_var, svd_dict
|
|
85
|
+
|
|
86
|
+
def plot_projections(self, fig_size=None, num_comp=None):
|
|
87
|
+
|
|
88
|
+
if num_comp is None:
|
|
89
|
+
num_comp = self.num_comp
|
|
90
|
+
|
|
91
|
+
comb = itertools.combinations(range(num_comp), 2)
|
|
92
|
+
|
|
93
|
+
combs = [item for item in comb]
|
|
94
|
+
num_plots = len(combs)
|
|
95
|
+
|
|
96
|
+
if num_plots == 1:
|
|
97
|
+
fig, axes = plt.subplots(figsize=fig_size)
|
|
98
|
+
axes = np.atleast_1d(axes)
|
|
99
|
+
else:
|
|
100
|
+
ncols = 2
|
|
101
|
+
nrows = num_plots // ncols + num_plots % ncols
|
|
102
|
+
|
|
103
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=fig_size)
|
|
104
|
+
|
|
105
|
+
axes_flat = axes.flatten()
|
|
106
|
+
num_axes = len(axes_flat)
|
|
107
|
+
for ind in range(num_plots):
|
|
108
|
+
axis = axes_flat[ind]
|
|
109
|
+
pc_one, pc_two = combs[ind]
|
|
110
|
+
|
|
111
|
+
my_map = plt.get_cmap('Reds')
|
|
112
|
+
data_plot = self.projections[:, combs[ind]].T
|
|
113
|
+
axis.scatter(data_plot[0], data_plot[1],
|
|
114
|
+
# 'o', mfc='None',
|
|
115
|
+
s=30/(num_axes/2), c=range(data_plot.shape[1]),
|
|
116
|
+
cmap=my_map,
|
|
117
|
+
marker='o', edgecolor='k')
|
|
118
|
+
|
|
119
|
+
axis.text(0.5, -0.08, 'PC%i' % (pc_one + 1),
|
|
120
|
+
transform=axis.transAxes, ha='center')
|
|
121
|
+
|
|
122
|
+
axis.text(-0.08, 0.5, 'PC%i' % (pc_two + 1), rotation=90,
|
|
123
|
+
transform=axis.transAxes, va='center')
|
|
124
|
+
|
|
125
|
+
axis.spines['bottom'].set_position('zero')
|
|
126
|
+
axis.spines['left'].set_position('zero')
|
|
127
|
+
|
|
128
|
+
axis.spines['top'].set_visible(False)
|
|
129
|
+
axis.spines['right'].set_visible(False)
|
|
130
|
+
|
|
131
|
+
if num_plots < num_axes:
|
|
132
|
+
fig.delaxes(axes_flat[-1])
|
|
133
|
+
|
|
134
|
+
fig.tight_layout()
|
|
135
|
+
|
|
136
|
+
return fig, axes
|
|
137
|
+
|
|
138
|
+
def get_regression(self, y_data, num_comp=None, update_instance=True):
|
|
139
|
+
|
|
140
|
+
if self.y_suffixes is None:
|
|
141
|
+
self.y_suffixes = ['%i' % num for num in range(1, len(y_data))]
|
|
142
|
+
|
|
143
|
+
self.y_labels = [r'$' + self.y_name + ('{%s}' % suffix) + '$'
|
|
144
|
+
for suffix in self.y_suffixes]
|
|
145
|
+
|
|
146
|
+
if num_comp is None:
|
|
147
|
+
num_comp = self.num_comp
|
|
148
|
+
|
|
149
|
+
# scores = self.projections[:, :num_comp]
|
|
150
|
+
scores = self.projections[:, :num_comp]
|
|
151
|
+
|
|
152
|
+
scores_inv = np.linalg.pinv(scores) # pseudoinverse of scores
|
|
153
|
+
|
|
154
|
+
# Regression coefficients w.r.t. principal components
|
|
155
|
+
if y_data.ndim == 1:
|
|
156
|
+
y_data = y_data[..., np.newaxis]
|
|
157
|
+
|
|
158
|
+
y_means = y_data.mean(axis=0)
|
|
159
|
+
y_center = y_data - y_means
|
|
160
|
+
q_coeff = np.dot(scores_inv, y_center)
|
|
161
|
+
|
|
162
|
+
regression_coeff = q_coeff
|
|
163
|
+
|
|
164
|
+
self.y_data = y_data
|
|
165
|
+
self.y_center = y_center
|
|
166
|
+
self.y_means = y_means
|
|
167
|
+
|
|
168
|
+
if update_instance:
|
|
169
|
+
self.regression_coeff = regression_coeff
|
|
170
|
+
self.num_comp = num_comp
|
|
171
|
+
|
|
172
|
+
y_pred = self.predict(self.data)
|
|
173
|
+
residuals = y_data - y_pred
|
|
174
|
+
self.residuals = residuals
|
|
175
|
+
|
|
176
|
+
# # Regression coefficients w.r.t. original X
|
|
177
|
+
# regression_coeff = np.dot(self.svd_dict['V'][:, :num_comp],
|
|
178
|
+
# q_coeff)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# if update_instance:
|
|
182
|
+
# self.regression_coeff = regression_coeff
|
|
183
|
+
|
|
184
|
+
# y_pred = self.predict(self.data)
|
|
185
|
+
# residuals = y_data - y_pred
|
|
186
|
+
# self.residuals = residuals
|
|
187
|
+
|
|
188
|
+
return regression_coeff
|
|
189
|
+
|
|
190
|
+
def predict(self, inputs, num_comp=None, regression_coeff=None,
|
|
191
|
+
full_output=False):
|
|
192
|
+
inputs = np.atleast_2d(inputs)
|
|
193
|
+
|
|
194
|
+
if self.snv:
|
|
195
|
+
inputs_centered = self.__center_data(inputs)
|
|
196
|
+
else:
|
|
197
|
+
inputs_centered = self.__center_data(inputs)
|
|
198
|
+
|
|
199
|
+
if regression_coeff is None:
|
|
200
|
+
coeff = self.regression_coeff
|
|
201
|
+
else:
|
|
202
|
+
coeff = regression_coeff
|
|
203
|
+
|
|
204
|
+
if num_comp is None:
|
|
205
|
+
num_comp = self.num_comp
|
|
206
|
+
|
|
207
|
+
p_matrix = self.svd_dict['V'][:, :num_comp]
|
|
208
|
+
new_projections = np.dot(inputs_centered, p_matrix)
|
|
209
|
+
|
|
210
|
+
resid_x = inputs_centered[0] - np.dot(new_projections[0],
|
|
211
|
+
p_matrix.T)
|
|
212
|
+
|
|
213
|
+
SPE_x = np.dot(resid_x, resid_x)
|
|
214
|
+
print(SPE_x)
|
|
215
|
+
|
|
216
|
+
# new_projections, _, di = self.__get_projections(inputs_centered,
|
|
217
|
+
# num_comp)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
response = np.dot(new_projections, coeff) + self.y_means
|
|
221
|
+
|
|
222
|
+
num_data = self.y_data.size
|
|
223
|
+
|
|
224
|
+
if full_output:
|
|
225
|
+
resid = response - self.y_data
|
|
226
|
+
mse = 1 / num_data * np.dot(resid.T, resid)
|
|
227
|
+
info_out = {'x_projected': new_projections, 'y_pred': response,
|
|
228
|
+
'MSE': mse}
|
|
229
|
+
|
|
230
|
+
return info_out
|
|
231
|
+
else:
|
|
232
|
+
|
|
233
|
+
return response
|
|
234
|
+
|
|
235
|
+
def evaluate_mse(self, num_comp=None):
|
|
236
|
+
|
|
237
|
+
if num_comp is None:
|
|
238
|
+
pc_counter = range(len(self.svd_dict['sv']))
|
|
239
|
+
else:
|
|
240
|
+
pc_counter = range(num_comp)
|
|
241
|
+
|
|
242
|
+
mse = []
|
|
243
|
+
residuals = []
|
|
244
|
+
|
|
245
|
+
n_data = np.prod(self.y_data.shape)
|
|
246
|
+
for n_component in pc_counter:
|
|
247
|
+
coeff = self.get_regression(self.y_data, n_component + 1,
|
|
248
|
+
update_instance=False)
|
|
249
|
+
|
|
250
|
+
pred = self.predict(self.data, regression_coeff=coeff,
|
|
251
|
+
num_comp=n_component + 1)
|
|
252
|
+
|
|
253
|
+
resid = self.y_data - pred
|
|
254
|
+
|
|
255
|
+
mse_val = 1 / n_data * (resid**2).sum()
|
|
256
|
+
|
|
257
|
+
mse.append(mse_val)
|
|
258
|
+
residuals.append(resid)
|
|
259
|
+
|
|
260
|
+
return mse, residuals
|
|
261
|
+
|
|
262
|
+
def plot_parity(self, figsize=None):
|
|
263
|
+
if figsize is None:
|
|
264
|
+
figsize = (4, 3.5)
|
|
265
|
+
fig, axis = plt.subplots(figsize=figsize)
|
|
266
|
+
|
|
267
|
+
markers = ['o', 's', 'd', '*']
|
|
268
|
+
|
|
269
|
+
y_pred = self.y_data - self.residuals
|
|
270
|
+
|
|
271
|
+
minim = (np.minimum(self.y_data, y_pred)).min()
|
|
272
|
+
maxim = (np.maximum(self.y_data, y_pred)).max()
|
|
273
|
+
|
|
274
|
+
range_vals = maxim - minim
|
|
275
|
+
|
|
276
|
+
left_bottom = [minim - range_vals*0.01]*2
|
|
277
|
+
right_top = [maxim + range_vals*0.01]*2
|
|
278
|
+
|
|
279
|
+
axis.plot(*zip(left_bottom, right_top), '--k', alpha=0.5)
|
|
280
|
+
|
|
281
|
+
for ind in range(self.y_data.shape[1]):
|
|
282
|
+
axis.plot(self.y_data[:, ind], y_pred[:, ind],
|
|
283
|
+
marker=markers[ind], mfc='None', ls='',
|
|
284
|
+
label=self.y_labels[ind])
|
|
285
|
+
|
|
286
|
+
axis.legend()
|
|
287
|
+
|
|
288
|
+
axis.set_xlabel('$%s{data}$' % self.y_name)
|
|
289
|
+
axis.set_ylabel('$%s{model}$' % self.y_name)
|
|
290
|
+
|
|
291
|
+
axis.xaxis.set_minor_locator(AutoMinorLocator(2))
|
|
292
|
+
axis.yaxis.set_minor_locator(AutoMinorLocator(2))
|
|
293
|
+
|
|
294
|
+
axis.text(1, 1.04, 'num_components = %i' % self.num_comp,
|
|
295
|
+
transform=axis.transAxes, ha='right')
|
|
296
|
+
|
|
297
|
+
return fig, axis
|
|
298
|
+
|
|
299
|
+
def cross_validation(self, num_groups=10):
|
|
300
|
+
perm = np.random.permutation(self.data.shape[0])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on Tue Mar 7 14:49:31 2023
|
|
4
|
+
|
|
5
|
+
@author: dcasasor
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import pathlib
|
|
10
|
+
|
|
11
|
+
from PharmaPy.Errors import PharmaPySpecificationError
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
root = pathlib.Path(__file__).parent / 'data'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_modeling_objects(uo, instance_name=None):
|
|
19
|
+
with open(root / 'minimum_modeling_objects.json') as fi:
|
|
20
|
+
checks = json.load(fi)
|
|
21
|
+
|
|
22
|
+
class_name = uo.__class__.__name__
|
|
23
|
+
|
|
24
|
+
if instance_name is None:
|
|
25
|
+
instance_name = '<InstanceName>'
|
|
26
|
+
instance_descr = "a " + class_name + ' instance'
|
|
27
|
+
else:
|
|
28
|
+
instance_descr = "the '%s' %s instance" % (instance_name, class_name)
|
|
29
|
+
|
|
30
|
+
if class_name in checks['special']:
|
|
31
|
+
modeling_objs = checks['special'][class_name]
|
|
32
|
+
else:
|
|
33
|
+
modeling_objs = checks[uo.oper_mode]
|
|
34
|
+
|
|
35
|
+
module_name = uo.__module__.split('.')[-1]
|
|
36
|
+
|
|
37
|
+
cond_kin = (module_name in checks['has_kinetics']['modules'] or
|
|
38
|
+
class_name in checks['has_kinetics']['classes']) and \
|
|
39
|
+
'Kinetics' not in modeling_objs
|
|
40
|
+
|
|
41
|
+
cond_utility = (module_name in checks['has_utility']['modules'] or
|
|
42
|
+
class_name in checks['has_utility']['classes']) and \
|
|
43
|
+
'Utility' not in modeling_objs
|
|
44
|
+
|
|
45
|
+
if cond_kin:
|
|
46
|
+
modeling_objs.append('Kinetics')
|
|
47
|
+
|
|
48
|
+
if cond_utility:
|
|
49
|
+
modeling_objs.append('Utility')
|
|
50
|
+
|
|
51
|
+
missing_obj = []
|
|
52
|
+
for obj in modeling_objs:
|
|
53
|
+
if not hasattr(uo, obj) or getattr(uo, obj) is None:
|
|
54
|
+
missing_obj.append(obj)
|
|
55
|
+
|
|
56
|
+
if len(missing_obj) > 0:
|
|
57
|
+
intro = "The following PharmaPy modeling objects were " \
|
|
58
|
+
"not detected in %s:\n" % instance_descr
|
|
59
|
+
|
|
60
|
+
obj_enum = '\t' + ',\n\t'.join(missing_obj) + '.\n\n'
|
|
61
|
+
|
|
62
|
+
recommend = "Please create the missing modeling objects listed above" \
|
|
63
|
+
" and then aggregate them one by one to the corresponding unit " \
|
|
64
|
+
"operation instance, e.g. %s.%s = <%sClass>(...)" % (
|
|
65
|
+
instance_name, missing_obj[0], missing_obj[0])
|
|
66
|
+
|
|
67
|
+
message = intro + obj_enum + recommend
|
|
68
|
+
|
|
69
|
+
# raise PharmaPySpecificationError(message)
|
|
70
|
+
warnings.warn(message)
|