cosmopharm 0.0.26__py3-none-any.whl → 0.0.29__py3-none-any.whl

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.
@@ -1,244 +1,244 @@
1
- import numpy as np
2
- import pandas as pd
3
- from scipy.optimize import fsolve, root
4
- from numpy.typing import NDArray
5
- from typing import Literal, Optional, Type, Union, List, Tuple, Generator, Dict
6
-
7
- from ..components import Component
8
- from ..actmodels import ActModel
9
- from ..utils.spacing import spacing
10
-
11
- NumericOrFrame = Union[float, List[float], Tuple[float, ...], NDArray[np.float64], pd.DataFrame]
12
-
13
- class SLE:
14
- def __init__(self,
15
- actmodel: Union[ActModel, Type[ActModel]],
16
- mixture: Optional[List[Component]] = None) -> None:
17
- self.actmodel = actmodel
18
- self.mixture = mixture
19
- self._validate_arguments()
20
- # Assign 'solute' and 'solvent' based on order in 'mixture'
21
- # Default assignment can be changed in e.g. 'solubility()'
22
- self.solute, self.solvent = self.mixture
23
-
24
- def solubility(self,
25
- solute: Optional[Component] = None,
26
- solvent: Optional[Component] = None,
27
- vary: Literal['T', 'w', 'auto'] = 'auto',
28
- mix_type: Literal['ideal', 'real'] = 'real',
29
- args: Optional[NumericOrFrame] = None,
30
- init: Optional[NumericOrFrame] = None,
31
- solver: Literal['root', 'fsolve'] = 'root',
32
- show_progress=False, **kwargs):
33
- ''' Calculate solubility curve of solute in solvent.'''
34
- self.solute = solute or self.solute
35
- self.solvent = solvent or self.solvent
36
- self.vary, self.mix_type = vary, mix_type
37
- self.show_progress = show_progress
38
- self.config = getattr(self.actmodel, 'config', self.mix_type)
39
- if self.vary == 'auto':
40
- gen = self.auto_solve(solver)
41
- else:
42
- self._vary = self.vary
43
- args = self.set_args(args)
44
- init = self.set_x0(init)
45
- gen = self.solve_sle(args, init, solver)
46
- try:
47
- res = [k for k in gen]
48
- res = pd.DataFrame(res, columns=['T', 'x', 'vary', 'w'])
49
- res = res[['T', 'w', 'x', 'vary']]
50
- return res
51
- except self.actmodel.InvalidFreeVolumeParametersException as e:
52
- print(f"Warning: {e}") # Inform the user
53
- return pd.DataFrame(columns=['T', 'w', 'x', 'vary'])
54
-
55
-
56
- # =============================================================================
57
- # MATHEMATICS
58
- # =============================================================================
59
- def solve_sle(self, args: NDArray[np.float64], init: NDArray[np.float64],
60
- solver: Literal['root', 'fsolve'] = 'root'
61
- ) -> Generator[Dict[str, Union[float, str]], None, None]:
62
- # Check compatibility of the "init" values
63
- is_iterable = init.size > 1
64
- if is_iterable and not init.size == args.size:
65
- msg = 'The length of "init" must be the same as "args".'
66
- raise ValueError(msg)
67
- x0 = init
68
- # Setup solver and handle pure component case
69
- key, lock = ['T', 'x'] if self._vary == 'T' else ['x', 'T']
70
- solve = self.set_solver(solver=solver)
71
- args, pure_component = self._handle_pure_component(args)
72
- if pure_component: # no need to calculate pure component
73
- yield pure_component
74
- for i, arg in enumerate(args):
75
- x0 = init[i] if is_iterable else x0
76
- out = float(solve(x0, arg))
77
- x0 = out if not is_iterable else x0
78
- res = {key: arg, lock: out, 'vary': self._vary}
79
- res['w'] = self.actmodel._convert(res['x'])[0]
80
- text = (f"T={res['T']:.2f}", f"x={res['x']:.4f}", f"w={res['w']:.4f}")
81
- if self.show_progress:
82
- print(f'SLE ({self.config}): ', *text)
83
- yield res
84
-
85
- def auto_solve(self, solver: Literal['root', 'fsolve'] = 'root'):
86
- if self.show_progress:
87
- print()
88
- print(f"Calculating SLE ({self.config})...")
89
- # Start with varying 'w' until dTdw > THRESHOLD
90
- self._vary = 'w'
91
- args = self.set_args()
92
- x0 = self.set_x0()
93
- gen = self.solve_sle(args, x0, solver)
94
- previous = None
95
- for i, current in enumerate(gen):
96
- yield current
97
- if self._should_stop_generator(i, previous, current):
98
- break # This will end the generator
99
- previous = current
100
- # Switch to varying 'T'
101
- self._vary = 'T'
102
- T0, x0 = current['T'], current['x']
103
- args = self.set_args(xmax=T0)[1:] # exclude initial point
104
- gen = self.solve_sle(args, x0)
105
- yield from gen
106
-
107
-
108
- # =============================================================================
109
- # THERMODYNAMICS
110
- # =============================================================================
111
- def ideal_mix(self, T):
112
- return np.exp(-self.gibbs_fusion(T))
113
-
114
- def real_mix(self, T, x):
115
- lngamma = self.actmodel.lngamma(T, x)[0]
116
- return np.log(x) + lngamma + self.gibbs_fusion(T)
117
-
118
- # Gibbs energy of fusion, i.e., the right-hand side of the solubility equation:
119
- def gibbs_fusion(self, T):
120
- T_fus = self.solute.T_fus
121
- H_fus = self.solute.H_fus
122
- Cp_fus_A = self.solute.Cp_fus_A
123
- Cp_fus_BT = self.solute.Cp_fus_BT
124
-
125
- R = 8.314 # J/(mol K)
126
- RT = R*T # J/mol
127
- A, B = Cp_fus_A, Cp_fus_BT
128
- G1 = H_fus*(1-T/T_fus) # J/mol
129
- G2 = A * (T-T_fus) + 0.5*B*(T**2-T_fus**2)
130
- G3 = -T * (A * np.log(T/T_fus) + B*(T-T_fus))
131
- G_fus = G1 + G2 + G3 # J/mol
132
- return G_fus/RT
133
-
134
-
135
- # =============================================================================
136
- # HELPER FUNCTIONS
137
- # =============================================================================
138
- def set_args(self,
139
- args: Optional[NumericOrFrame] = None,
140
- xmin: Optional[float] = None,
141
- xmax: Optional[float] = None,
142
- dx: Optional[float] = None
143
- ) -> NDArray[np.float64]:
144
- vary = self._vary
145
- # Determine argument values based on input data or generate
146
- # them based on range and type
147
- defaults = {
148
- 'T': {'min': 310, 'max': self.solute.T_fus, 'step': 10},
149
- 'w': {'min': 0.01, 'max': 1, 'step': 0.08}
150
- }
151
- mi = defaults[vary]['min'] if xmin is None else xmin
152
- ma = defaults[vary]['max'] if xmax is None else xmax
153
- dx = defaults[vary]['step'] if dx is None else dx
154
-
155
- if args is None:
156
- if self.vary != 'auto': # auto_vary == False
157
- args = np.arange(ma, mi-dx, -dx)
158
- args[-1] = np.maximum(args[-1], mi)
159
- elif vary == 'T': # auto_vary == True
160
- num, dT = 16, 175 # How many data points in this T-range
161
- num = int((ma-mi)/dT*num) # fraction of points if dT smaller
162
- num = max(6, num)
163
- kwargs = dict(reverse=True, n=1.5)
164
- args = spacing(ma, mi, num, 'poly', **kwargs)
165
- else: # vary == 'w'
166
- num = 16 if self.mix_type == 'ideal' else 21
167
- args = spacing(ma, mi, num, 'quadratic')
168
- args = np.asarray(args)
169
- args = args if vary != 'w' else self.actmodel._convert(args, to='mole')
170
- return args
171
-
172
- def set_x0(self, init: Optional[NumericOrFrame] = None) -> NDArray[np.float64]:
173
- vary = self._vary
174
- # Set up initial values based on the type of variable ('T' or 'w')
175
- if vary == 'T':
176
- x0 = 1. if init is None else self.actmodel._convert(init, to='mole')
177
- else: # vary == 'w'
178
- x0 = self.solute.T_fus if init is None else init
179
- x0 = np.asarray(x0)
180
- return x0
181
-
182
- def set_solver(self, solver: Literal['root', 'fsolve'] = 'root'):
183
- vary, mix = self._vary, self.mix_type
184
- # Define the objective function (fobj) and the solver function (solve)
185
- # based on the mixture type (mix) and the variable type (vary)
186
- if mix == 'ideal' and vary == 'T':
187
- def fobj(x, T): return self.ideal_mix(T)
188
- def solve(x0, args): return fobj(x0, args)
189
- else:
190
- if mix == 'ideal':
191
- def fobj(T, x): return x - self.ideal_mix(T)
192
- elif vary == 'T': # mix != 'ideal'
193
- def fobj(x, T): return self.real_mix(T, x)
194
- else: # vary == 'w'
195
- def fobj(T, x): return self.real_mix(T, x)
196
- kwargs = dict(method='krylov', options={'maxiter': 5, 'xtol': 1e-3})
197
- if solver == 'fsolve':
198
- def solve(x0, args): return fsolve(fobj, x0, args)
199
- else:
200
- def solve(x0, args): return root(fobj, x0, args, **kwargs).x
201
- # Problem: "fsolve" and "root" return different types of np.arrays
202
- # (1) fsolve returns (1,) 1D array
203
- # (2) root returns () 0D array
204
- # Therefore, it is necessary to use float(solve(...)) to extract the
205
- # single value from the array, since solve()[0] does not work for root.
206
- return solve
207
-
208
-
209
- # =============================================================================
210
- # AUXILLIARY FUNCTIONS
211
- # =============================================================================
212
- def _should_stop_generator(self, i, previous, current):
213
- THRESHOLD = 60
214
- if i > 1: # ensuring there was a previous result
215
- dT = current['T'] - previous['T']
216
- dw = current['w'] - previous['w']
217
- return (dT / dw) > THRESHOLD
218
- return False # If not enough elements, continue the generator
219
-
220
- def _handle_pure_component(self, args):
221
- res = {'T': self.solute.T_fus, 'x': 1, 'vary': self._vary, 'w': 1}
222
- if self._vary == 'T' and self.solute.T_fus in args:
223
- args = args[args != self.solute.T_fus]
224
- return args, res
225
- elif self._vary == 'w' and 1 in args:
226
- args = args[args != 1]
227
- return args, res
228
- return args, None
229
-
230
- def _validate_arguments(self):
231
- """Validate the arguments for the SLE class."""
232
- # TODO: Insert case where both actmodel and mixture are provided (check if acmodel.mixture == mixture, if not raise warning)
233
- if isinstance(self.actmodel, ActModel):
234
- # If actmodel is an instance of ActModel
235
- self.mixture: List[Component] = self.mixture or self.actmodel.mixture
236
- elif isinstance(self.actmodel, type) and issubclass(self.actmodel, ActModel):
237
- # If actmodel is a class (subclass of ActModel)
238
- if self.mixture is None:
239
- raise ValueError("Please provide a valid mixture:Mixture.")
240
- self.actmodel: ActModel = self.actmodel(self.mixture)
241
- else:
242
- # If actmodel is neither an instance nor a subclass of ActModel
243
- err = "'actmodel' must be an instance or a subclass of 'ActModel'"
244
- raise ValueError(err)
1
+ import numpy as np
2
+ import pandas as pd
3
+ from scipy.optimize import fsolve, root
4
+ from numpy.typing import NDArray
5
+ from typing import Literal, Optional, Type, Union, List, Tuple, Generator, Dict
6
+
7
+ from ..components import Component
8
+ from ..actmodels import ActModel
9
+ from ..utils.spacing import spacing
10
+
11
+ NumericOrFrame = Union[float, List[float], Tuple[float, ...], NDArray[np.float64], pd.DataFrame]
12
+
13
+ class SLE:
14
+ def __init__(self,
15
+ actmodel: Union[ActModel, Type[ActModel]],
16
+ mixture: Optional[List[Component]] = None) -> None:
17
+ self.actmodel = actmodel
18
+ self.mixture = mixture
19
+ self._validate_arguments()
20
+ # Assign 'solute' and 'solvent' based on order in 'mixture'
21
+ # Default assignment can be changed in e.g. 'solubility()'
22
+ self.solute, self.solvent = self.mixture
23
+
24
+ def solubility(self,
25
+ solute: Optional[Component] = None,
26
+ solvent: Optional[Component] = None,
27
+ vary: Literal['T', 'w', 'auto'] = 'auto',
28
+ mix_type: Literal['ideal', 'real'] = 'real',
29
+ args: Optional[NumericOrFrame] = None,
30
+ init: Optional[NumericOrFrame] = None,
31
+ solver: Literal['root', 'fsolve'] = 'root',
32
+ show_progress=False, **kwargs):
33
+ ''' Calculate solubility curve of solute in solvent.'''
34
+ self.solute = solute or self.solute
35
+ self.solvent = solvent or self.solvent
36
+ self.vary, self.mix_type = vary, mix_type
37
+ self.show_progress = show_progress
38
+ self.config = getattr(self.actmodel, 'config', self.mix_type)
39
+ if self.vary == 'auto':
40
+ gen = self.auto_solve(solver)
41
+ else:
42
+ self._vary = self.vary
43
+ args = self.set_args(args)
44
+ init = self.set_x0(init)
45
+ gen = self.solve_sle(args, init, solver)
46
+ try:
47
+ res = [k for k in gen]
48
+ res = pd.DataFrame(res, columns=['T', 'x', 'vary', 'w'])
49
+ res = res[['T', 'w', 'x', 'vary']]
50
+ return res
51
+ except self.actmodel.InvalidFreeVolumeParametersException as e:
52
+ print(f"Warning: {e}") # Inform the user
53
+ return pd.DataFrame(columns=['T', 'w', 'x', 'vary'])
54
+
55
+
56
+ # =============================================================================
57
+ # MATHEMATICS
58
+ # =============================================================================
59
+ def solve_sle(self, args: NDArray[np.float64], init: NDArray[np.float64],
60
+ solver: Literal['root', 'fsolve'] = 'root'
61
+ ) -> Generator[Dict[str, Union[float, str]], None, None]:
62
+ # Check compatibility of the "init" values
63
+ is_iterable = init.size > 1
64
+ if is_iterable and not init.size == args.size:
65
+ msg = 'The length of "init" must be the same as "args".'
66
+ raise ValueError(msg)
67
+ x0 = init
68
+ # Setup solver and handle pure component case
69
+ key, lock = ['T', 'x'] if self._vary == 'T' else ['x', 'T']
70
+ solve = self.set_solver(solver=solver)
71
+ args, pure_component = self._handle_pure_component(args)
72
+ if pure_component: # no need to calculate pure component
73
+ yield pure_component
74
+ for i, arg in enumerate(args):
75
+ x0 = init[i] if is_iterable else x0
76
+ out = float(solve(x0, arg))
77
+ x0 = out if not is_iterable else x0
78
+ res = {key: arg, lock: out, 'vary': self._vary}
79
+ res['w'] = self.actmodel._convert(res['x'])[0]
80
+ text = (f"T={res['T']:.2f}", f"x={res['x']:.4f}", f"w={res['w']:.4f}")
81
+ if self.show_progress:
82
+ print(f'SLE ({self.config}): ', *text)
83
+ yield res
84
+
85
+ def auto_solve(self, solver: Literal['root', 'fsolve'] = 'root'):
86
+ if self.show_progress:
87
+ print()
88
+ print(f"Calculating SLE ({self.config})...")
89
+ # Start with varying 'w' until dTdw > THRESHOLD
90
+ self._vary = 'w'
91
+ args = self.set_args()
92
+ x0 = self.set_x0()
93
+ gen = self.solve_sle(args, x0, solver)
94
+ previous = None
95
+ for i, current in enumerate(gen):
96
+ yield current
97
+ if self._should_stop_generator(i, previous, current):
98
+ break # This will end the generator
99
+ previous = current
100
+ # Switch to varying 'T'
101
+ self._vary = 'T'
102
+ T0, x0 = current['T'], current['x']
103
+ args = self.set_args(xmax=T0)[1:] # exclude initial point
104
+ gen = self.solve_sle(args, x0)
105
+ yield from gen
106
+
107
+
108
+ # =============================================================================
109
+ # THERMODYNAMICS
110
+ # =============================================================================
111
+ def ideal_mix(self, T):
112
+ return np.exp(-self.gibbs_fusion(T))
113
+
114
+ def real_mix(self, T, x):
115
+ lngamma = self.actmodel.lngamma(T, x)[0]
116
+ return np.log(x) + lngamma + self.gibbs_fusion(T)
117
+
118
+ # Gibbs energy of fusion, i.e., the right-hand side of the solubility equation:
119
+ def gibbs_fusion(self, T):
120
+ T_fus = self.solute.T_fus
121
+ H_fus = self.solute.H_fus
122
+ Cp_fus_A = self.solute.Cp_fus_A
123
+ Cp_fus_BT = self.solute.Cp_fus_BT
124
+
125
+ R = 8.314 # J/(mol K)
126
+ RT = R*T # J/mol
127
+ A, B = Cp_fus_A, Cp_fus_BT
128
+ G1 = H_fus*(1-T/T_fus) # J/mol
129
+ G2 = A * (T-T_fus) + 0.5*B*(T**2-T_fus**2)
130
+ G3 = -T * (A * np.log(T/T_fus) + B*(T-T_fus))
131
+ G_fus = G1 + G2 + G3 # J/mol
132
+ return G_fus/RT
133
+
134
+
135
+ # =============================================================================
136
+ # HELPER FUNCTIONS
137
+ # =============================================================================
138
+ def set_args(self,
139
+ args: Optional[NumericOrFrame] = None,
140
+ xmin: Optional[float] = None,
141
+ xmax: Optional[float] = None,
142
+ dx: Optional[float] = None
143
+ ) -> NDArray[np.float64]:
144
+ vary = self._vary
145
+ # Determine argument values based on input data or generate
146
+ # them based on range and type
147
+ defaults = {
148
+ 'T': {'min': 310, 'max': self.solute.T_fus, 'step': 10},
149
+ 'w': {'min': 0.01, 'max': 1, 'step': 0.08}
150
+ }
151
+ mi = defaults[vary]['min'] if xmin is None else xmin
152
+ ma = defaults[vary]['max'] if xmax is None else xmax
153
+ dx = defaults[vary]['step'] if dx is None else dx
154
+
155
+ if args is None:
156
+ if self.vary != 'auto': # auto_vary == False
157
+ args = np.arange(ma, mi-dx, -dx)
158
+ args[-1] = np.maximum(args[-1], mi)
159
+ elif vary == 'T': # auto_vary == True
160
+ num, dT = 16, 175 # How many data points in this T-range
161
+ num = int((ma-mi)/dT*num) # fraction of points if dT smaller
162
+ num = max(6, num)
163
+ kwargs = dict(reverse=True, n=1.5)
164
+ args = spacing(ma, mi, num, 'poly', **kwargs)
165
+ else: # vary == 'w'
166
+ num = 16 if self.mix_type == 'ideal' else 21
167
+ args = spacing(ma, mi, num, 'quadratic')
168
+ args = np.asarray(args)
169
+ args = args if vary != 'w' else self.actmodel._convert(args, to='mole')
170
+ return args
171
+
172
+ def set_x0(self, init: Optional[NumericOrFrame] = None) -> NDArray[np.float64]:
173
+ vary = self._vary
174
+ # Set up initial values based on the type of variable ('T' or 'w')
175
+ if vary == 'T':
176
+ x0 = 1. if init is None else self.actmodel._convert(init, to='mole')
177
+ else: # vary == 'w'
178
+ x0 = self.solute.T_fus if init is None else init
179
+ x0 = np.asarray(x0)
180
+ return x0
181
+
182
+ def set_solver(self, solver: Literal['root', 'fsolve'] = 'root'):
183
+ vary, mix = self._vary, self.mix_type
184
+ # Define the objective function (fobj) and the solver function (solve)
185
+ # based on the mixture type (mix) and the variable type (vary)
186
+ if mix == 'ideal' and vary == 'T':
187
+ def fobj(x, T): return self.ideal_mix(T)
188
+ def solve(x0, args): return fobj(x0, args)
189
+ else:
190
+ if mix == 'ideal':
191
+ def fobj(T, x): return x - self.ideal_mix(T)
192
+ elif vary == 'T': # mix != 'ideal'
193
+ def fobj(x, T): return self.real_mix(T, x)
194
+ else: # vary == 'w'
195
+ def fobj(T, x): return self.real_mix(T, x)
196
+ kwargs = dict(method='krylov', options={'maxiter': 5, 'xtol': 1e-3})
197
+ if solver == 'fsolve':
198
+ def solve(x0, args): return fsolve(fobj, x0, args)
199
+ else:
200
+ def solve(x0, args): return root(fobj, x0, args, **kwargs).x
201
+ # Problem: "fsolve" and "root" return different types of np.arrays
202
+ # (1) fsolve returns (1,) 1D array
203
+ # (2) root returns () 0D array
204
+ # Therefore, it is necessary to use float(solve(...)) to extract the
205
+ # single value from the array, since solve()[0] does not work for root.
206
+ return solve
207
+
208
+
209
+ # =============================================================================
210
+ # AUXILLIARY FUNCTIONS
211
+ # =============================================================================
212
+ def _should_stop_generator(self, i, previous, current):
213
+ THRESHOLD = 60
214
+ if i > 1: # ensuring there was a previous result
215
+ dT = current['T'] - previous['T']
216
+ dw = current['w'] - previous['w']
217
+ return (dT / dw) > THRESHOLD
218
+ return False # If not enough elements, continue the generator
219
+
220
+ def _handle_pure_component(self, args):
221
+ res = {'T': self.solute.T_fus, 'x': 1, 'vary': self._vary, 'w': 1}
222
+ if self._vary == 'T' and self.solute.T_fus in args:
223
+ args = args[args != self.solute.T_fus]
224
+ return args, res
225
+ elif self._vary == 'w' and 1 in args:
226
+ args = args[args != 1]
227
+ return args, res
228
+ return args, None
229
+
230
+ def _validate_arguments(self):
231
+ """Validate the arguments for the SLE class."""
232
+ # TODO: Insert case where both actmodel and mixture are provided (check if acmodel.mixture == mixture, if not raise warning)
233
+ if isinstance(self.actmodel, ActModel):
234
+ # If actmodel is an instance of ActModel
235
+ self.mixture: List[Component] = self.mixture or self.actmodel.mixture
236
+ elif isinstance(self.actmodel, type) and issubclass(self.actmodel, ActModel):
237
+ # If actmodel is a class (subclass of ActModel)
238
+ if self.mixture is None:
239
+ raise ValueError("Please provide a valid mixture:Mixture.")
240
+ self.actmodel: ActModel = self.actmodel(self.mixture)
241
+ else:
242
+ # If actmodel is neither an instance nor a subclass of ActModel
243
+ err = "'actmodel' must be an instance or a subclass of 'ActModel'"
244
+ raise ValueError(err)
@@ -1,3 +1,3 @@
1
- from .helpers import read_params, create_components
2
- from .convert import convert
3
- from .spacing import spacing
1
+ from .helpers import read_params, create_components
2
+ from .convert import convert
3
+ from .spacing import spacing
@@ -1,58 +1,58 @@
1
- import numpy as np
2
-
3
-
4
- def convert(x: np.ndarray, Mw: np.ndarray, to: str = 'weight') -> np.ndarray:
5
- """
6
- Convert between mole fraction (x) and mass/weight fraction (w) for a mixture.
7
-
8
- Parameters:
9
- x (numpy.ndarray): A 1D or 2D NumPy array representing mole fractions of components in the mixture.
10
- - If 1D, it should have the shape (n,) with 'n' as the number of components.
11
- - If 2D, it should have the shape (n, m) where 'n' is the number of components
12
- and 'm' is the number of data points.
13
- to (str, optional): The conversion direction. Should be either 'weight' (default) to convert from mole
14
- fraction to weight fraction, or 'mole' to convert from weight fraction to mole fraction.
15
-
16
- Returns:
17
- numpy.ndarray: The converted fractions, with the same shape as 'x'.
18
-
19
- Example:
20
- >>> sle = SLE(mix=mix) # Replace with the actual mixture object
21
- >>> x = np.array([0.4, 0.6]) # Mole fractions of two components
22
- >>> w = mix.convert(x, to='weight') # Convert to weight fractions
23
- >>> print(w)
24
- array([[0.01373165],
25
- [0.98626835]])
26
- """
27
-
28
- # Check if x is a NumPy array
29
- if not isinstance(x, np.ndarray):
30
- raise ValueError("Input 'x' must be a 1D or 2D NumPy array.")
31
-
32
- # Check if x is a scalar (0-dimensional)
33
- if x.shape == ():
34
- raise ValueError(
35
- "Input 'x' must be a 1D or 2D NumPy array, not a scalar.")
36
-
37
- if to not in ['weight', 'mole']:
38
- raise ValueError("Invalid 'to' argument. Use 'weight' or 'mole'.")
39
-
40
- # Check and reshape Mw if needed
41
- Mw = Mw[:, np.newaxis] if Mw.ndim == 1 else Mw # Reshape to (n, 1)
42
- # Check and reshape x if needed
43
- x = x[:, np.newaxis] if x.ndim == 1 else x # Reshape to (n, 1)
44
-
45
- # Check if Mw and x have the same number of components
46
- if Mw.shape[0] != x.shape[0]:
47
- raise ValueError(
48
- "Number of components in 'Mw' and 'x' must match.")
49
-
50
- # Calculate the numerator for conversion based on 'to' argument
51
- num = x * Mw if to == 'weight' else x / Mw
52
- # Calculate the denominator for conversion
53
- den = np.sum(num, axis=0)
54
- # Calculate the final conversion using the numerator and denominator
55
- res = num / den
56
- # Replace nan values with 0
57
- res = np.nan_to_num(res)
58
- return res
1
+ import numpy as np
2
+
3
+
4
+ def convert(x: np.ndarray, Mw: np.ndarray, to: str = 'weight') -> np.ndarray:
5
+ """
6
+ Convert between mole fraction (x) and mass/weight fraction (w) for a mixture.
7
+
8
+ Parameters:
9
+ x (numpy.ndarray): A 1D or 2D NumPy array representing mole fractions of components in the mixture.
10
+ - If 1D, it should have the shape (n,) with 'n' as the number of components.
11
+ - If 2D, it should have the shape (n, m) where 'n' is the number of components
12
+ and 'm' is the number of data points.
13
+ to (str, optional): The conversion direction. Should be either 'weight' (default) to convert from mole
14
+ fraction to weight fraction, or 'mole' to convert from weight fraction to mole fraction.
15
+
16
+ Returns:
17
+ numpy.ndarray: The converted fractions, with the same shape as 'x'.
18
+
19
+ Example:
20
+ >>> sle = SLE(mix=mix) # Replace with the actual mixture object
21
+ >>> x = np.array([0.4, 0.6]) # Mole fractions of two components
22
+ >>> w = mix.convert(x, to='weight') # Convert to weight fractions
23
+ >>> print(w)
24
+ array([[0.01373165],
25
+ [0.98626835]])
26
+ """
27
+
28
+ # Check if x is a NumPy array
29
+ if not isinstance(x, np.ndarray):
30
+ raise ValueError("Input 'x' must be a 1D or 2D NumPy array.")
31
+
32
+ # Check if x is a scalar (0-dimensional)
33
+ if x.shape == ():
34
+ raise ValueError(
35
+ "Input 'x' must be a 1D or 2D NumPy array, not a scalar.")
36
+
37
+ if to not in ['weight', 'mole']:
38
+ raise ValueError("Invalid 'to' argument. Use 'weight' or 'mole'.")
39
+
40
+ # Check and reshape Mw if needed
41
+ Mw = Mw[:, np.newaxis] if Mw.ndim == 1 else Mw # Reshape to (n, 1)
42
+ # Check and reshape x if needed
43
+ x = x[:, np.newaxis] if x.ndim == 1 else x # Reshape to (n, 1)
44
+
45
+ # Check if Mw and x have the same number of components
46
+ if Mw.shape[0] != x.shape[0]:
47
+ raise ValueError(
48
+ "Number of components in 'Mw' and 'x' must match.")
49
+
50
+ # Calculate the numerator for conversion based on 'to' argument
51
+ num = x * Mw if to == 'weight' else x / Mw
52
+ # Calculate the denominator for conversion
53
+ den = np.sum(num, axis=0)
54
+ # Calculate the final conversion using the numerator and denominator
55
+ res = num / den
56
+ # Replace nan values with 0
57
+ res = np.nan_to_num(res)
58
+ return res