Module pychnosz.core.unicurve
CHNOSZ unicurve() function - Calculate univariant curves for geothermometry/geobarometry.
This module implements functions to solve for temperatures or pressures of equilibration for a given logK value, producing univariant curves useful for aqueous geothermometry and geobarometry applications.
Author: Based on pyCHNOSZ univariant.r by Grayson Boyer Optimized: Uses scipy.optimize.brentq() for efficient root-finding
Functions
def unicurve(logK: int | float | List[int | float],
species: str | List[str] | int | List[int],
coeff: int | float | List[int | float],
state: str | List[str],
pressures: float | List[float] = 1,
temperatures: float | List[float] = 25,
IS: float = 0,
minT: float = 0.1,
maxT: float = 100,
minP: float = 1,
maxP: float = 500,
tol: float | None = None,
solve: str = 'T',
messages: bool = True,
show: bool = True,
plot_it: bool = True,
width: int = 600,
height: int = 400,
res: int = 200) ‑> UnivariantResult | List[UnivariantResult]-
Expand source code
def unicurve(logK: Union[float, int, List[Union[float, int]]], species: Union[str, List[str], int, List[int]], coeff: Union[int, float, List[Union[int, float]]], state: Union[str, List[str]], pressures: Union[float, List[float]] = 1, temperatures: Union[float, List[float]] = 25, IS: float = 0, minT: float = 0.1, maxT: float = 100, minP: float = 1, maxP: float = 500, tol: Optional[float] = None, solve: str = "T", messages: bool = True, show: bool = True, plot_it: bool = True, width: int = 600, height: int = 400, res: int = 200) -> Union[UnivariantResult, List[UnivariantResult]]: """ Solve for temperatures or pressures of equilibration for a given logK value(s). This function calculates univariant curves useful for aqueous geothermometry and geobarometry. Given a measured equilibrium constant (logK) for a reaction, it solves for the temperatures (at specified pressures) or pressures (at specified temperatures) where the reaction would produce that logK value. The solver uses scipy.optimize.brentq (Brent's method), which combines bisection, secant, and inverse quadratic interpolation for efficient and robust convergence. This is ~100x faster than the original binary search algorithm while maintaining identical numerical accuracy. Parameters ---------- logK : float, int, or list of float or int Logarithm (base 10) of the equilibrium constant(s). When a list is provided, each logK value is processed separately and a list of results is returned. species : str, int, or list of str or int Name, formula, or database index of species involved in the reaction coeff : int, float, or list Reaction stoichiometric coefficients (negative for reactants, positive for products) state : str or list of str Physical state(s) of species: "aq", "cr", "gas", "liq" pressures : float or list of float, default 1 Pressure(s) in bars (used when solving for temperature) temperatures : float or list of float, default 25 Temperature(s) in °C (used when solving for pressure) IS : float, default 0 Ionic strength for activity corrections (mol/kg) minT : float, default 0.1 Minimum temperature (°C) to search (ignored when solving for pressure) maxT : float, default 100 Maximum temperature (°C) to search (ignored when solving for pressure) minP : float, default 1 Minimum pressure (bar) to search (ignored when solving for temperature) maxP : float, default 500 Maximum pressure (bar) to search (ignored when solving for temperature) tol : float, optional Tolerance for convergence. Default: 1/(10^(n+2)) where n is number of decimal places in logK, with maximum default of 1e-5 solve : str, default "T" What to solve for: "T" for temperature or "P" for pressure messages : bool, default True Print informational messages show : bool, default True Display result table plot_it : bool, default True Display interactive plotly plot showing logK vs T (or P) with target logK as horizontal line and intersection points marked width : int, default 600 Plot width in pixels (used if plot_it=True) height : int, default 400 Plot height in pixels (used if plot_it=True) res : int, default 200 Number of points to calculate for plotting the logK curve (used if plot_it=True) Returns ------- UnivariantResult or list of UnivariantResult When logK is a single value: returns a UnivariantResult object. When logK is a list: returns a list of UnivariantResult objects. Each result contains: - reaction: DataFrame with reaction stoichiometry - out: DataFrame with solved T or P values and thermodynamic properties - warnings: List of warning messages Examples -------- >>> from pychnosz import unicurve, reset >>> reset() >>> >>> # Solve for temperature: quartz dissolution >>> # SiO2(quartz) = SiO2(aq) >>> result = unicurve(logK=-2.71, species=["quartz", "SiO2"], ... state=["cr", "aq"], coeff=[-1, 1], ... pressures=200, minT=1, maxT=350) >>> print(result.out[["P", "T", "logK"]]) >>> >>> # Solve for pressure: water dissociation >>> result = unicurve(logK=-14, species=["H2O", "H+", "OH-"], ... state=["liq", "aq", "aq"], coeff=[-1, 1, 1], ... temperatures=[25, 50, 75], solve="P", ... minP=1, maxP=1000) >>> print(result.out[["T", "P", "logK"]]) Notes ----- This function uses scipy.optimize.brentq for root-finding, which provides: - Guaranteed convergence if root is bracketed - Typical convergence in 5-15 function evaluations - ~100x speedup compared to custom binary search (1600 → 15 evaluations) - Identical numerical results to original implementation The algorithm also implements "warm start" optimization: when solving for multiple pressures/temperatures, previous solutions are used to intelligently bracket subsequent searches, further improving performance. References ---------- Based on univariant.r from pyCHNOSZ by Grayson Boyer Optimized using Brent, R. P. (1973). Algorithms for Minimization without Derivatives. """ # Track whether input was a single value or list single_logK_input = not isinstance(logK, list) # Ensure logK is a list for processing if single_logK_input: logK_list = [logK] else: logK_list = logK # Ensure species, state, and coeff are lists if not isinstance(species, list): species = [species] if not isinstance(state, list): state = [state] if not isinstance(coeff, list): coeff = [coeff] # Process each logK value results = [] for this_logK in logK_list: result = UnivariantResult() # Set default tolerance based on logK precision if tol is None: # Count decimal places in logK logK_str = str(float(this_logK)) if '.' in logK_str: n_decimals = len(logK_str.split('.')[1].rstrip('0')) else: n_decimals = 0 this_tol = 10 ** (-(n_decimals + 2)) if this_tol > 1e-5: this_tol = 1e-5 else: this_tol = tol # Get reaction information from first subcrt call try: initial_calc = subcrt(species, coeff=coeff, state=state, T=25, P=1, exceed_Ttr=True, messages=False, show=False) result.reaction = initial_calc.reaction except Exception as e: if messages: warnings.warn(f"Error getting reaction information: {str(e)}") result.reaction = None if solve.upper() == "T": # Solve for temperature at given pressure(s) if not isinstance(pressures, list): pressures = [pressures] results_list = [] prev_T = None # For warm start optimization for i, pressure in enumerate(pressures): if messages: print(f"Solving for T at P = {pressure} bar (logK = {this_logK})...") # Warm start: use previous solution to narrow search range if available current_minT = minT current_maxT = maxT if prev_T is not None and minT < prev_T < maxT: # Center search around previous solution with a safety margin # logK typically changes by ~0.006 per °C, so ±50°C should be safe margin = 50 current_minT = max(minT, prev_T - margin) current_maxT = min(maxT, prev_T + margin) if messages: print(f" Using warm start: searching {current_minT:.1f} to {current_maxT:.1f}°C") result_dict = _solve_T_for_pressure(this_logK, species, state, coeff, pressure, IS, current_minT, current_maxT, this_tol, initial_guess=prev_T, messages=messages) # If warm start failed, try full range if result_dict['T'] is None and prev_T is not None: if messages: print(f" Warm start failed, searching full range...") result_dict = _solve_T_for_pressure(this_logK, species, state, coeff, pressure, IS, minT, maxT, this_tol, messages=messages) results_list.append(result_dict) # Update for next warm start if result_dict['T'] is not None: prev_T = result_dict['T'] result.out = pd.DataFrame(results_list) elif solve.upper() == "P": # Solve for pressure at given temperature(s) if not isinstance(temperatures, list): temperatures = [temperatures] results_list = [] prev_P = None # For warm start optimization for i, temperature in enumerate(temperatures): if messages: print(f"Solving for P at T = {temperature} °C (logK = {this_logK})...") # Warm start: use previous solution to narrow search range if available current_minP = minP current_maxP = maxP if prev_P is not None and minP < prev_P < maxP: # Center search around previous solution with a safety margin # Pressure effects vary, use a generous ±500 bar margin margin = 500 current_minP = max(minP, prev_P - margin) current_maxP = min(maxP, prev_P + margin) if messages: print(f" Using warm start: searching {current_minP:.0f} to {current_maxP:.0f} bar") result_dict = _solve_P_for_temperature(this_logK, species, state, coeff, temperature, IS, current_minP, current_maxP, this_tol, initial_guess=prev_P, messages=messages) # If warm start failed, try full range if result_dict['P'] is None and prev_P is not None: if messages: print(f" Warm start failed, searching full range...") result_dict = _solve_P_for_temperature(this_logK, species, state, coeff, temperature, IS, minP, maxP, this_tol, messages=messages) results_list.append(result_dict) # Update for next warm start if result_dict['P'] is not None: prev_P = result_dict['P'] result.out = pd.DataFrame(results_list) else: raise ValueError(f"solve must be 'T' or 'P', got '{solve}'") # Create interactive plot if requested if plot_it: if not PLOTLY_AVAILABLE: warnings.warn("plotly is not installed. Set plot_it=False to suppress this warning, " "or install plotly with: pip install plotly") else: result.fig = _create_unicurve_plot(this_logK, species, state, coeff, result, solve, minT, maxT, minP, maxP, IS, width, height, res, messages) # Display result if requested if show and result.out is not None: try: from IPython.display import display if result.reaction is not None: print("\nReaction:") display(result.reaction) print(f"\nResults (logK = {this_logK}):") display(result.out) except ImportError: # Not in Jupyter, just print if result.reaction is not None: print("\nReaction:") print(result.reaction) print(f"\nResults (logK = {this_logK}):") print(result.out) # Add this result to the list results.append(result) # Return single result or list based on input if single_logK_input: return results[0] else: return resultsSolve for temperatures or pressures of equilibration for a given logK value(s).
This function calculates univariant curves useful for aqueous geothermometry and geobarometry. Given a measured equilibrium constant (logK) for a reaction, it solves for the temperatures (at specified pressures) or pressures (at specified temperatures) where the reaction would produce that logK value.
The solver uses scipy.optimize.brentq (Brent's method), which combines bisection, secant, and inverse quadratic interpolation for efficient and robust convergence. This is ~100x faster than the original binary search algorithm while maintaining identical numerical accuracy.
Parameters
logK:float, int,orlistoffloatorint- Logarithm (base 10) of the equilibrium constant(s). When a list is provided, each logK value is processed separately and a list of results is returned.
species:str, int,orlistofstrorint- Name, formula, or database index of species involved in the reaction
coeff:int, float,orlist- Reaction stoichiometric coefficients (negative for reactants, positive for products)
state:strorlistofstr- Physical state(s) of species: "aq", "cr", "gas", "liq"
pressures:floatorlistoffloat, default1- Pressure(s) in bars (used when solving for temperature)
temperatures:floatorlistoffloat, default25- Temperature(s) in °C (used when solving for pressure)
IS:float, default0- Ionic strength for activity corrections (mol/kg)
minT:float, default0.1- Minimum temperature (°C) to search (ignored when solving for pressure)
maxT:float, default100- Maximum temperature (°C) to search (ignored when solving for pressure)
minP:float, default1- Minimum pressure (bar) to search (ignored when solving for temperature)
maxP:float, default500- Maximum pressure (bar) to search (ignored when solving for temperature)
tol:float, optional- Tolerance for convergence. Default: 1/(10^(n+2)) where n is number of decimal places in logK, with maximum default of 1e-5
solve:str, default"T"- What to solve for: "T" for temperature or "P" for pressure
messages:bool, defaultTrue- Print informational messages
show:bool, defaultTrue- Display result table
plot_it:bool, defaultTrue- Display interactive plotly plot showing logK vs T (or P) with target logK as horizontal line and intersection points marked
width:int, default600- Plot width in pixels (used if plot_it=True)
height:int, default400- Plot height in pixels (used if plot_it=True)
res:int, default200- Number of points to calculate for plotting the logK curve (used if plot_it=True)
Returns
UnivariantResultorlistofUnivariantResult- When logK is a single value: returns a UnivariantResult object. When logK is a list: returns a list of UnivariantResult objects. Each result contains: - reaction: DataFrame with reaction stoichiometry - out: DataFrame with solved T or P values and thermodynamic properties - warnings: List of warning messages
Examples
>>> from pychnosz import unicurve, reset >>> reset() >>> >>> # Solve for temperature: quartz dissolution >>> # SiO2(quartz) = SiO2(aq) >>> result = unicurve(logK=-2.71, species=["quartz", "SiO2"], ... state=["cr", "aq"], coeff=[-1, 1], ... pressures=200, minT=1, maxT=350) >>> print(result.out[["P", "T", "logK"]]) >>> >>> # Solve for pressure: water dissociation >>> result = unicurve(logK=-14, species=["H2O", "H+", "OH-"], ... state=["liq", "aq", "aq"], coeff=[-1, 1, 1], ... temperatures=[25, 50, 75], solve="P", ... minP=1, maxP=1000) >>> print(result.out[["T", "P", "logK"]])Notes
This function uses scipy.optimize.brentq for root-finding, which provides: - Guaranteed convergence if root is bracketed - Typical convergence in 5-15 function evaluations - ~100x speedup compared to custom binary search (1600 → 15 evaluations) - Identical numerical results to original implementation
The algorithm also implements "warm start" optimization: when solving for multiple pressures/temperatures, previous solutions are used to intelligently bracket subsequent searches, further improving performance.
References
Based on univariant.r from pyCHNOSZ by Grayson Boyer Optimized using Brent, R. P. (1973). Algorithms for Minimization without Derivatives.
def univariant_TP(logK: int | float | List[int | float],
species: str | List[str] | int | List[int],
coeff: int | float | List[int | float],
state: str | List[str],
Trange: List[float],
Prange: List[float],
IS: float = 0,
xlim: List[float] | None = None,
ylim: List[float] | None = None,
line_type: str = 'markers+lines',
tol: float | None = None,
title: str | None = None,
res: int = 10,
width: int = 500,
height: int = 400,
save_as: str | None = None,
save_format: str = 'png',
save_scale: float = 1,
show: bool = False,
messages: bool = False,
parallel: bool = True,
plot_it: bool = True) ‑> List[UnivariantResult]-
Expand source code
def univariant_TP(logK: Union[float, int, List[Union[float, int]]], species: Union[str, List[str], int, List[int]], coeff: Union[int, float, List[Union[int, float]]], state: Union[str, List[str]], Trange: List[float], Prange: List[float], IS: float = 0, xlim: Optional[List[float]] = None, ylim: Optional[List[float]] = None, line_type: str = "markers+lines", tol: Optional[float] = None, title: Optional[str] = None, res: int = 10, width: int = 500, height: int = 400, save_as: Optional[str] = None, save_format: str = "png", save_scale: float = 1, show: bool = False, messages: bool = False, parallel: bool = True, plot_it: bool = True) -> List[UnivariantResult]: """ Solve for temperatures and pressures of equilibration for given logK value(s) and produce an interactive T-P diagram. This function calculates univariant curves in temperature-pressure (T-P) space for one or more logK values. For each pressure in a range, it solves for the temperature where the reaction achieves the target logK. The resulting curves show phase boundaries or equilibrium conditions in T-P space. Parameters ---------- logK : float, int, or list Logarithm (base 10) of equilibrium constant(s). Multiple values produce multiple curves on the same plot. species : str, int, or list of str or int Name, formula, or database index of species involved in the reaction coeff : int, float, or list Reaction stoichiometric coefficients (negative for reactants, positive for products) state : str or list of str Physical state(s) of species: "aq", "cr", "gas", "liq" Trange : list of two floats [min, max] temperature range (°C) to search for solutions Prange : list of two floats [min, max] pressure range (bar) to calculate along IS : float, default 0 Ionic strength for activity corrections (mol/kg) xlim : list of two floats, optional [min, max] range for x-axis (temperature) in plot ylim : list of two floats, optional [min, max] range for y-axis (pressure) in plot line_type : str, default "markers+lines" Plotly line type: "markers+lines", "markers", or "lines" tol : float, optional Convergence tolerance. Default: 1/(10^(n+2)) where n is decimal places in logK title : str, optional Plot title. Default: auto-generated from reaction res : int, default 10 Number of pressure points to calculate along the curve width : int, default 500 Plot width in pixels height : int, default 400 Plot height in pixels save_as : str, optional Filename to save plot (without extension) save_format : str, default "png" Save format: "png", "jpg", "jpeg", "webp", "svg", "pdf", "html" save_scale : float, default 1 Scale factor for saved plot show : bool, default False Display subcrt result tables messages : bool, default False Print informational messages parallel : bool, default True Use parallel processing across multiple logK values for faster computation. Utilizes multiple CPU cores when processing multiple logK curves. plot_it : bool, default True Display the plot Returns ------- list of UnivariantResult List of UnivariantResult objects, one for each logK value. Each contains reaction information and T-P curve data. Examples -------- >>> from pychnosz import univariant_TP, reset >>> reset() >>> >>> # Calcite-aragonite phase boundary >>> result = univariant_TP( ... logK=0, ... species=["calcite", "aragonite"], ... state=["cr", "cr"], ... coeff=[-1, 1], ... Trange=[0, 700], ... Prange=[2000, 16000] ... ) >>> >>> # Multiple curves for K-feldspar stability >>> result = univariant_TP( ... logK=[-8, -6, -4, -2], ... species=["K-feldspar", "kaolinite", "H2O", "SiO2", "muscovite"], ... state=["cr", "cr", "liq", "aq", "cr"], ... coeff=[-1, -1, 1, 2, 1], ... Trange=[0, 350], ... Prange=[1, 5000], ... res=20 ... ) Notes ----- This function creates T-P diagrams by: 1. Generating a range of pressures from Prange[0] to Prange[1] 2. For each pressure, solving for T where logK matches the target 3. Plotting the resulting T-P points as a curve For multiple logK values, each curve represents a different equilibrium condition. This is useful for: - Phase diagrams (e.g., mineral stability fields) - Isopleths (lines of constant logK) - Reaction boundaries Requires plotly for interactive plotting. If plotly is not installed, set plot_it=False to just return the data without plotting. References ---------- Based on univariant_TP from pyCHNOSZ by Grayson Boyer """ # Check if plotly is available if plot_it and not PLOTLY_AVAILABLE: warnings.warn("plotly is not installed. Set plot_it=False to suppress this warning, " "or install plotly with: pip install plotly") plot_it = False # Ensure logK is a list if not isinstance(logK, list): logK = [logK] # Create plotly figure if plot_it: fig = go.Figure() output = [] # Generate pressure array pressures = np.linspace(Prange[0], Prange[1], res) # Process each logK value (in parallel if enabled) if parallel and len(logK) > 1: # Parallel processing max_workers = min(len(logK), multiprocessing.cpu_count()) # Prepare arguments for each logK value args_list = [ (this_logK, species, state, coeff, pressures, Trange, IS, tol, show, messages) for this_logK in logK ] # Process in parallel with ProcessPoolExecutor(max_workers=max_workers) as executor: # Submit all tasks future_to_logK = { executor.submit(_process_single_logK, args): args[0] for args in args_list } # Collect results as they complete (maintains order via logK list) results_dict = {} for future in as_completed(future_to_logK): this_logK = future_to_logK[future] try: out = future.result() results_dict[this_logK] = out except Exception as e: if messages: print(f"Error processing logK={this_logK}: {str(e)}") # Create empty result results_dict[this_logK] = None # Reorder results to match input logK order for this_logK in logK: out = results_dict.get(this_logK) if out is not None: output.append(out) # Add to plot if we have valid data if plot_it and not out.out['T'].isnull().all(): fig.add_trace(go.Scatter( x=out.out['T'], y=out.out['P'], mode=line_type, name=f"logK={this_logK}", text=[f"logK={this_logK}" for _ in range(len(out.out['T']))], hovertemplate='%{text}<br>T, °C=%{x:.2f}<br>P, bar=%{y:.2f}<extra></extra>', )) elif out.out['T'].isnull().all(): if messages: print(f"Could not find any T or P values in this range that correspond to a logK value of {this_logK}") else: # Sequential processing (original code) for this_logK in logK: # Set tolerance if not specified if tol is None: logK_str = str(float(this_logK)) if '.' in logK_str: n_decimals = len(logK_str.split('.')[1].rstrip('0')) else: n_decimals = 0 this_tol = 10 ** (-(n_decimals + 2)) if this_tol > 1e-5: this_tol = 1e-5 else: this_tol = tol # Solve for T at each pressure out = unicurve( solve="T", logK=this_logK, species=species, state=state, coeff=coeff, pressures=list(pressures), minT=Trange[0], maxT=Trange[1], IS=IS, tol=this_tol, show=show, messages=messages, plot_it=False # Don't plot individual curves - univariant_TP makes its own plot ) # Add to plot if we have valid data if plot_it and not out.out['T'].isnull().all(): fig.add_trace(go.Scatter( x=out.out['T'], y=out.out['P'], mode=line_type, name=f"logK={this_logK}", text=[f"logK={this_logK}" for _ in range(len(out.out['T']))], hovertemplate='%{text}<br>T, °C=%{x:.2f}<br>P, bar=%{y:.2f}<extra></extra>', )) elif out.out['T'].isnull().all(): if messages: print(f"Could not find any T or P values in this range that correspond to a logK value of {this_logK}") output.append(out) # Generate plot title if not specified if plot_it: if title is None and len(output) > 0 and output[0].reaction is not None: react_grid = output[0].reaction # Build reaction string reactants = [] products = [] for i, row in react_grid.iterrows(): coeff_val = row['coeff'] name = row['name'] if row['name'] != 'water' else 'H2O' if coeff_val < 0: coeff_str = str(int(-coeff_val)) if -coeff_val != 1 else "" reactants.append(f"{coeff_str} {name}".strip()) elif coeff_val > 0: coeff_str = str(int(coeff_val)) if coeff_val != 1 else "" products.append(f"{coeff_str} {name}".strip()) title = " + ".join(reactants) + " = " + " + ".join(products) # Update layout fig.update_layout( template="simple_white", title=str(title) if title else "", xaxis_title="T, °C", yaxis_title="P, bar", width=width, height=height, hoverlabel=dict(bgcolor="white"), ) # Set axis limits if specified if xlim is not None: fig.update_xaxes(range=xlim) if ylim is not None: fig.update_yaxes(range=ylim) # Configure plot controls config = { 'displaylogo': False, 'modeBarButtonsToRemove': ['resetScale2d', 'toggleSpikelines'], 'toImageButtonOptions': { 'format': save_format, 'filename': save_as if save_as else 'univariant_TP', 'height': height, 'width': width, 'scale': save_scale, }, } # Save plot if requested if save_as is not None: full_filename = f"{save_as}.{save_format}" if save_format == 'html': fig.write_html(full_filename) else: fig.write_image(full_filename, format=save_format, width=width, height=height, scale=save_scale) if messages: print(f"Plot saved to {full_filename}") # Display plot fig.show(config=config) # Store figure in all result objects for out in output: out.fig = fig return outputSolve for temperatures and pressures of equilibration for given logK value(s) and produce an interactive T-P diagram.
This function calculates univariant curves in temperature-pressure (T-P) space for one or more logK values. For each pressure in a range, it solves for the temperature where the reaction achieves the target logK. The resulting curves show phase boundaries or equilibrium conditions in T-P space.
Parameters
logK:float, int,orlist- Logarithm (base 10) of equilibrium constant(s). Multiple values produce multiple curves on the same plot.
species:str, int,orlistofstrorint- Name, formula, or database index of species involved in the reaction
coeff:int, float,orlist- Reaction stoichiometric coefficients (negative for reactants, positive for products)
state:strorlistofstr- Physical state(s) of species: "aq", "cr", "gas", "liq"
Trange:listoftwo floats- [min, max] temperature range (°C) to search for solutions
Prange:listoftwo floats- [min, max] pressure range (bar) to calculate along
IS:float, default0- Ionic strength for activity corrections (mol/kg)
xlim:listoftwo floats, optional- [min, max] range for x-axis (temperature) in plot
ylim:listoftwo floats, optional- [min, max] range for y-axis (pressure) in plot
line_type:str, default"markers+lines"- Plotly line type: "markers+lines", "markers", or "lines"
tol:float, optional- Convergence tolerance. Default: 1/(10^(n+2)) where n is decimal places in logK
title:str, optional- Plot title. Default: auto-generated from reaction
res:int, default10- Number of pressure points to calculate along the curve
width:int, default500- Plot width in pixels
height:int, default400- Plot height in pixels
save_as:str, optional- Filename to save plot (without extension)
save_format:str, default"png"- Save format: "png", "jpg", "jpeg", "webp", "svg", "pdf", "html"
save_scale:float, default1- Scale factor for saved plot
show:bool, defaultFalse- Display subcrt result tables
messages:bool, defaultFalse- Print informational messages
parallel:bool, defaultTrue- Use parallel processing across multiple logK values for faster computation. Utilizes multiple CPU cores when processing multiple logK curves.
plot_it:bool, defaultTrue- Display the plot
Returns
listofUnivariantResult- List of UnivariantResult objects, one for each logK value. Each contains reaction information and T-P curve data.
Examples
>>> from pychnosz import univariant_TP, reset >>> reset() >>> >>> # Calcite-aragonite phase boundary >>> result = univariant_TP( ... logK=0, ... species=["calcite", "aragonite"], ... state=["cr", "cr"], ... coeff=[-1, 1], ... Trange=[0, 700], ... Prange=[2000, 16000] ... ) >>> >>> # Multiple curves for K-feldspar stability >>> result = univariant_TP( ... logK=[-8, -6, -4, -2], ... species=["K-feldspar", "kaolinite", "H2O", "SiO2", "muscovite"], ... state=["cr", "cr", "liq", "aq", "cr"], ... coeff=[-1, -1, 1, 2, 1], ... Trange=[0, 350], ... Prange=[1, 5000], ... res=20 ... )Notes
This function creates T-P diagrams by: 1. Generating a range of pressures from Prange[0] to Prange[1] 2. For each pressure, solving for T where logK matches the target 3. Plotting the resulting T-P points as a curve
For multiple logK values, each curve represents a different equilibrium condition. This is useful for: - Phase diagrams (e.g., mineral stability fields) - Isopleths (lines of constant logK) - Reaction boundaries
Requires plotly for interactive plotting. If plotly is not installed, set plot_it=False to just return the data without plotting.
References
Based on univariant_TP from pyCHNOSZ by Grayson Boyer
Classes
class UnivariantResult-
Expand source code
class UnivariantResult: """Result structure for univariant curve calculations.""" def __init__(self): self.reaction = None # Reaction summary DataFrame self.out = None # Results DataFrame with T/P and thermodynamic properties self.warnings = [] # Warning messages self.fig = None # Plotly figure object (if plot_it=True) def __repr__(self): if self.out is not None: return f"UnivariantResult with {len(self.out)} points" return "UnivariantResult (no calculations performed)" def __getitem__(self, key): """Allow dictionary-style access to attributes.""" return getattr(self, key)Result structure for univariant curve calculations.