diff --git a/src/autochem/rate/data.py b/src/autochem/rate/data.py index 1d954024..0b316ed5 100644 --- a/src/autochem/rate/data.py +++ b/src/autochem/rate/data.py @@ -648,7 +648,10 @@ def fit( if not validate or bad_fit == "fill": try: with np.errstate(all="raise"): - obj(T=T) + vals = obj(T=T) + if not np.all(np.isfinite(vals)): + msg = "Fitted rate gives non-finite values over the input temperature range." + raise FloatingPointError(msg) except FloatingPointError as e: if A_fill is not None: return cls(order=order, A=A_fill, b=0, E=0) @@ -917,22 +920,25 @@ def fit( # noqa: PLR0913 (Options: "fill", replace with the fill value, or numpy.seterr options) :return: Rate fit """ - bad_fits = [ - "fill" if np.any(np.isclose(p, bad_fit_fill_pressures)) else bad_fit - for p in Ps - ] - k_data_fits = [ - ArrheniusRateFit.fit( + k_fits = [] + for k_data, P in zip(np.transpose(k_data), Ps, strict=True): + if validate: + print(f"Fitting Arrhenius rate for {P = }") + + bad_fit = ( + "fill" if np.any(np.isclose(P, bad_fit_fill_pressures)) else bad_fit + ) + k_fit = ArrheniusRateFit.fit( Ts=Ts, - ks=ks, + ks=k_data, A_fill=A_fill, - bad_fit=f, # pyright: ignore[reportArgumentType] + bad_fit=bad_fit, validate=validate, order=order, units=units, ) - for ks, f in zip(np.transpose(k_data), bad_fits, strict=True) - ] + k_fits.append(k_fit) + k_high_fit = None if k_high is not None: k_high_fit = ArrheniusRateFit.fit( @@ -943,9 +949,9 @@ def fit( # noqa: PLR0913 return cls( order=order, - As=[f.A for f in k_data_fits], - bs=[f.b for f in k_data_fits], - Es=[f.E for f in k_data_fits], + As=[f.A for f in k_fits], + bs=[f.b for f in k_fits], + Es=[f.E for f in k_fits], Ps=Ps, # pyright: ignore[reportArgumentType] ) diff --git a/src/autochem/tests/test__therm.py b/src/autochem/tests/test__therm.py index 7f6616bf..b111b006 100644 --- a/src/autochem/tests/test__therm.py +++ b/src/autochem/tests/test__therm.py @@ -81,10 +81,8 @@ def test__from_chemkin_string(name, spc_str0): # Plot therm.display(spc) therm.display( - spc, - label="original", - others=[spc_times_2, spc_divided_by_2], - others_labels=["doubled", "halved"], + [spc, spc_times_2, spc_divided_by_2], + label=["original","doubled", "halved"] ) @@ -105,7 +103,7 @@ def test__fit(name, spc_str0): units={"energy": "kcal"}, ) spc_fit = therm.fit(spc) - therm.display(spc, label="data", others=[spc_fit], others_labels=["fit"]) + therm.display([spc, spc_fit], label=["data", "fit"]) if __name__ == "__main__": diff --git a/src/autochem/therm/_species.py b/src/autochem/therm/_species.py index 0264a2b6..11906b13 100644 --- a/src/autochem/therm/_species.py +++ b/src/autochem/therm/_species.py @@ -1,8 +1,9 @@ """Thermodynamic data.""" import datetime +import re from collections.abc import Sequence -from typing import ClassVar, Literal +from typing import ClassVar, Literal, Self import altair as alt import numpy as np @@ -23,6 +24,17 @@ class Species(Scalable): name: str therm: Therm_ + def racemize( + self, enant_suffixes: tuple[str, str] = ("0", "1"), rac_suffix: str = "R" + ) -> Self: + """Racemize the thermodynamic functions for a chiral species.""" + suffix0, suffix1 = enant_suffixes + pattern = re.compile(rf"(?:{re.escape(suffix0)}|{re.escape(suffix1)})$") + if pattern.search(self.name): + self.name = pattern.sub(rac_suffix, self.name) + self.therm = self.therm.racemize() + return self + # Private attributes _scalers: ClassVar[Scalers] = {"therm": (lambda c, x: c * x)} @@ -259,16 +271,17 @@ def fit( # Display def display( # noqa: PLR0913 - spc: Species, + spc: Species | Sequence[Species], *, props: Sequence[Literal["Cv", "Cp", "S", "H", "dH"]] = ("Cp", "S", "H"), - others: Sequence[Species] = (), - others_labels: Sequence[str] = (), T_range: tuple[float, float] = (200, 3000), # noqa: N803 units: UnitsData | None = None, - label: str = "This work", - x_label: str = "𝑇", # noqa: RUF001 + label: str | Sequence[str] | None = None, + color: str | Sequence[str] | None = None, + x_label: str = "temperature", y_labels: Sequence[str | None] | None = None, + x_unit: str | None = "K", + y_unit: str | Sequence[str | None] | None = None, horizontal: bool = False, ) -> alt.Chart: """Display as an Arrhenius plot, optionally comparing to other rates. @@ -284,15 +297,19 @@ def display( # noqa: PLR0913 :param y_label: Y-axis label :return: Chart """ - return spc.therm.display( + spcs = [spc] if isinstance(spc, Species) else spc + therms = [s.therm for s in spcs] + return data.display( + therm_=therms, props=props, - others=[o.therm for o in others], - others_labels=others_labels, T_range=T_range, units=units, label=label, + color=color, x_label=x_label, y_labels=y_labels, + x_unit=x_unit, + y_unit=y_unit, horizontal=horizontal, ) diff --git a/src/autochem/therm/data.py b/src/autochem/therm/data.py index e12f6710..5df6674d 100644 --- a/src/autochem/therm/data.py +++ b/src/autochem/therm/data.py @@ -45,15 +45,50 @@ class BaseTherm(ThermCalculator, UnitManager, Frozen, Scalable, SubclassTyped, a formula: Formula_ charge: int = 0 + @abc.abstractmethod + def racemize(self) -> Self: + """Racemize the thermodynamic functions for a chiral species.""" + return self + + @property + def plot_mark(self) -> str: + """Plot mark to use in altair.""" + return plot.Mark.line + + def plot_data( + self, + *, + T_range: tuple[float, float] = (200, 3000), # noqa: N803 + units: UnitsData | None = None, + ) -> tuple[NDArray[np.float64], dict[str, NDArray[np.float64]]]: + """Get data for plotting. + + :return: Tuple of (x values, dict of y values by property) + """ + units = UNITS if units is None else Units.model_validate(units) + + prop_func_dct = { + Key.Cv: heat_capacity_constant_volume, + Key.Cp: heat_capacity_constant_pressure, + Key.S: entropy, + Key.H: enthalpy, + Key.dH: delta_enthalpy, + } + + x_data = np.linspace(*T_range, num=1000) + y_data_dct = { + k: f_(self, x_data, units=units) for k, f_ in prop_func_dct.items() + } + return x_data, y_data_dct + def display( # noqa: PLR0913 self, props: Sequence[Literal["Cv", "Cp", "S", "H", "dH"]] = ("Cp", "S", "H"), *, - others: "Sequence[BaseTherm]" = (), - others_labels: Sequence[str] = (), T_range: tuple[float, float] = (200, 3000), # noqa: N803 units: UnitsData | None = None, - label: str = "This work", + label: str | None = None, + color: str | None = None, x_label: str = "𝑇", # noqa: RUF001 y_labels: Sequence[str | None] | None = None, horizontal: bool = False, @@ -70,112 +105,34 @@ def display( # noqa: PLR0913 :param horizontal: Whether to display horizontally :return: Chart """ - y_labels = y_labels or [None] * len(props) - charts = [ - self._display( - prop=prop, - others=others, - others_labels=others_labels, - T_range=T_range, - units=units, - label=label, + prop_label_dct = ( + dict(zip(props, y_labels, strict=True)) + if y_labels is not None + else { + Key.Cv: "𝐢α΅₯", + Key.Cp: "πΆβ‚š", + Key.S: "𝑆", # noqa: RUF001 + Key.H: "𝐻", # noqa: RUF001 + Key.dH: "Δ𝐻", + } + ) + x_data, y_data_dct = self.plot_data(T_range=T_range, units=units) + charts = [] + for key in props: + data = y_data_dct[key] + chart = plot.general( + y_data=[data], + x_data=x_data, + labels=[label] if label is not None else None, + colors=[color] if color is not None else None, x_label=x_label, - y_label=y_label, + y_label=prop_label_dct[key], + mark=self.plot_mark, ) - for prop, y_label in zip(props, y_labels, strict=True) - ] + charts.append(chart) concat_ = alt.hconcat if horizontal else alt.vconcat return concat_(*charts) - def _display( # noqa: PLR0913 - self, - prop: Literal["Cv", "Cp", "S", "H", "dH"], - others: "Sequence[BaseTherm]" = (), - others_labels: Sequence[str] = (), - T_range: tuple[float, float] = (200, 3000), # noqa: N803 - units: UnitsData | None = None, - label: str = "This work", - x_label: str = "𝑇", # noqa: RUF001 - y_label: str | None = None, - ) -> alt.Chart: - """Display as a thermodynamic function plot. - - :param prop: Thermodynamic properties to display - :param others: Other thermodynamic data to compare to - :param others_labels: Labels for other thermodynamic data - :param T_range: Temperature range - :param units: Units - :param x_label: X-axis label - :param y_labels: Y-axis labels, by property - :return: Chart - """ - units = UNITS if units is None else Units.model_validate(units) - - # Property units - prop_unit_dct = { - Key.Cv: units.energy_per_substance / units.temperature, - Key.Cp: units.energy_per_substance / units.temperature, - Key.S: units.energy_per_substance / units.temperature, - Key.H: units.energy_per_substance, - Key.dH: units.energy_per_substance, - } - prop_func_dct = { - Key.Cv: heat_capacity_constant_volume, - Key.Cp: heat_capacity_constant_pressure, - Key.S: entropy, - Key.H: enthalpy, - Key.dH: delta_enthalpy, - } - prop_label_dct = { - Key.Cv: "𝐢α΅₯", - Key.Cp: "πΆβ‚š", - Key.S: "𝑆", # noqa: RUF001 - Key.H: "𝐻", # noqa: RUF001 - Key.dH: "Δ𝐻", - } - - # Process units - x_unit = unit_.pretty_string(units.temperature) - y_unit = unit_.pretty_string(prop_unit_dct.get(prop)) - - # Add units to labels - x_label = f"{x_label} ({x_unit})" - y_label = f"{y_label or prop_label_dct.get(prop)} ({y_unit})" - - # Get property function - func_ = prop_func_dct[prop] - - # Gather objects and labels - assert len(others) == len(others_labels), f"{others_labels} !~ {others}" - all_objs = [self, *others] - all_labels = [label, *others_labels] - all_colors = plot.LINE_COLOR_CYCLE[: len(all_labels)] - - # Gather data from functons - T = np.linspace(*T_range, num=500) # noqa: N806 - data_dct = {L: func_(o, T) for L, o in zip(all_labels, all_objs, strict=True)} - data = pd.DataFrame({"x": T, **data_dct}) - - # Prepare encoding parameters - x = alt.X("x", title=x_label) - y = alt.Y("value:Q", title=y_label) - color = ( - alt.Color( - "key:N", - scale=alt.Scale(domain=all_labels, range=all_colors), - ) - if others - else alt.value(all_colors[0]) - ) - - # Create chart - return ( - alt.Chart(data) - .mark_line() - .transform_fold(fold=list(data_dct.keys())) - .encode(x=x, y=y, color=color) - ) - class Therm(BaseTherm): """Raw thermodynamic data. @@ -202,6 +159,40 @@ class Therm(BaseTherm): "Z2": D.temperature**-2, } + @property + def plot_mark(self) -> str: + """Plot mark to use in altair.""" + return plot.Mark.point + + def plot_data( + self, + *, + T_range: tuple[float, float] = (200, 3000), # noqa: N803 + units: UnitsData | None = None, + ) -> tuple[NDArray[np.float64], dict[str, NDArray[np.float64]]]: + """Get data for plotting. + + :return: Tuple of (x values, dict of y values by property) + """ + units = UNITS if units is None else Units.model_validate(units) + + prop_func_dct = { + Key.Cv: heat_capacity_constant_volume, + Key.Cp: heat_capacity_constant_pressure, + Key.S: entropy, + Key.H: enthalpy, + Key.dH: delta_enthalpy, + } + + (i_,) = np.where( + np.greater_equal(self.T, T_range[0]) & np.less_equal(self.T, T_range[1]) + ) + x_data = np.take(self.T, i_) + y_data_dct = { + k: f_(self, x_data, units=units) for k, f_ in prop_func_dct.items() + } + return x_data, y_data_dct + @property def T_min(self) -> float: # noqa: N802 """Get minimum temperature.""" @@ -227,6 +218,11 @@ def sort_by_temperature(self) -> Self: self.model_config["frozen"] = frozen return self + def racemize(self) -> Self: + """Racemize the thermodynamic functions for a chiral species.""" + self.Z0 = np.add(self.Z0, np.log(2)).tolist() + return self + # Replace this with a model validator (before or after), allowing extra arguments def __init__( self, @@ -530,6 +526,12 @@ def piecewise_conditions( calc_high.in_bounds(T, include_max=True), ] + def racemize(self) -> Self: + """Racemize the thermodynamic functions for a chiral species.""" + self.coeffs_low[-1] += np.log(2).item() + self.coeffs_high[-1] += np.log(2).item() + return self + def heat_capacity( self, T: ArrayLike, # noqa: N803 @@ -595,6 +597,36 @@ def delta_enthalpy( funcs = [calc.delta_enthalpy for calc in self.piecewise_calculators()] return np.piecewise(T, conds, funcs) + @classmethod + def from_therm( + cls, + therm: Therm, + *, + T_min: float | None = None, # noqa: N803 + T_mid: float = 1000, # noqa: N803 + T_max: float | None = None, # noqa: N803 + ) -> "Nasa7ThermFit": + """Fit data to Nasa-7 therm fit object.""" + T = therm.temperature_data() # noqa: N806 + Cp = therm.heat_capacity_data(const="P") # noqa: N806 + S = therm.entropy_data(P=1, units={"pressure": "bar"}) # noqa: N806 + H = therm.enthalpy_data() # noqa: N806 + + T_min = T_min or np.min(T) # noqa: N806 + T_max = T_max or np.max(T) # noqa: N806 + + return Nasa7ThermFit.fit( + T=T, + Cp=Cp, + S=S, + H=H, + formula=therm.formula, + charge=therm.charge, + T_min=T_min, + T_mid=T_mid, + T_max=T_max, + ) + @classmethod def fit( # noqa: PLR0913 cls, @@ -895,6 +927,91 @@ def from_pac99_output_parse_results( ) +# Display +def display( # noqa: PLR0913 + therm_: BaseTherm | Sequence[BaseTherm], + *, + props: Sequence[Literal["Cv", "Cp", "S", "H", "dH"]] = ("Cp", "S", "H"), + T_range: tuple[float, float] = (200, 3000), # noqa: N803 + units: UnitsData | None = None, + label: str | Sequence[str] | None = None, + color: str | Sequence[str] | None = None, + x_label: str = "temperature", + y_labels: Sequence[str | None] | None = None, + x_unit: str | None = "K", + y_unit: str | Sequence[str | None] | None = None, + horizontal: bool = False, +) -> alt.Chart: + """Display as a thermodynamic function plot. + + :param props: Thermodynamic properties to display + :param others: Other thermodynamic data to compare to + :param others_labels: Labels for other thermodynamic data + :param T_range: Temperature range + :param units: Units + :param x_label: X-axis label + :param y_labels: Y-axis labels, by property + :param horizontal: Whether to display horizontally + :return: Chart + """ + therms = [therm_] if isinstance(therm_, BaseTherm) else therm_ + labels = [label] if isinstance(label, str) else label + colors = [color] if isinstance(color, str) else color + y_units = [y_unit] if isinstance(y_unit, str) else y_unit + prop_unit_dct = ( + dict(zip(props, y_units, strict=True)) if y_units is not None else {} + ) + + prop_label_dct = ( + dict(zip(props, y_labels, strict=True)) + if y_labels is not None + else { + Key.Cv: "constant volume heat capacity", + Key.Cp: "constant pressure heat capacity", + Key.S: "entropy", + Key.H: "enthalpy", + Key.dH: "thermal enthalpy increment", + } + ) + x_datas, y_data_dcts = zip( + *[therm_.plot_data(T_range=T_range, units=units) for therm_ in therms], + strict=True, + ) + x_range = T_range + charts = [] + for key in props: + y_vals = np.concatenate([d[key] for d in y_data_dcts]) + y_range = (np.min(y_vals), np.max(y_vals)) + mark_charts = [] + for mark in (plot.Mark.line, plot.Mark.point): + ixs = [i for i, t in enumerate(therms) if t.plot_mark == mark] + if ixs: + x_data, *x_datas_ = [x_datas[i] for i in ixs] + for x_data_ in x_datas_: + assert np.allclose(x_data, x_data_), f"{x_data=} != {x_data_=}" + + y_datas = [y_data_dcts[i][key] for i in ixs] + y_label = prop_label_dct[key] + y_unit = prop_unit_dct.get(key) + chart = plot.general( + y_data=y_datas, + x_data=x_data, + labels=None if labels is None else [labels[i] for i in ixs], + colors=None if colors is None else [colors[i] for i in ixs], + x_label=x_label if x_unit is None else f"{x_label} ({x_unit})", + y_label=y_label if y_unit is None else f"{y_label} ({y_unit})", + x_scale=plot.regular_scale(x_range), + y_scale=plot.regular_scale(y_range), + x_axis=plot.regular_scale_axis(x_range), + y_axis=plot.regular_scale_axis(y_range), + mark=mark, + ) + mark_charts.append(chart) + charts.append(alt.layer(*mark_charts).resolve_scale(color="independent")) + concat_ = alt.hconcat if horizontal else alt.vconcat + return concat_(*charts) + + # Helpers KJ_TO_CAL = pint.Quantity(1, "kJ").m_as("cal") ENTHALPY_CHANGE_0K_TO_298K: dict[str, float] = { diff --git a/src/autochem/util/plot.py b/src/autochem/util/plot.py index 93f94dc8..f4064bf5 100644 --- a/src/autochem/util/plot.py +++ b/src/autochem/util/plot.py @@ -166,7 +166,7 @@ def regular_scale(val_range: tuple[float, float]) -> alt.Scale: :param val_range: Range :return: Scale """ - return alt.Scale(domain=val_range) + return alt.Scale(domain=val_range, domainMax=val_range[-1], nice=True) def regular_scale_axis(val_range: tuple[float, float]) -> alt.Axis: @@ -179,7 +179,7 @@ def regular_scale_axis(val_range: tuple[float, float]) -> alt.Axis: val_scale = val_max - val_min if val_scale < 1: fmt = ".2f" - elif val_scale < 3: + elif val_scale < 5: fmt = ".1f" else: fmt = ".0f" @@ -329,8 +329,8 @@ def interp_(x: Any) -> np.ndarray: def general( y_data: Sequence[Sequence[float]], x_data: Sequence[float], # noqa: N803 - labels: Sequence[str], *, + labels: Sequence[str] | None = None, colors: Sequence[str] | None = None, x_label: str | None = None, # noqa: RUF001 y_label: str | None = None, # noqa: RUF001 @@ -360,6 +360,10 @@ def general( else [*POINT_COLOR_CYCLE, *LINE_COLOR_CYCLE] ) + nseries = len(y_data) + keep_legend = (labels is not None) and legend + labels = labels or [f"{i + 1}" for i in range(nseries)] + ny, nx = np.shape(y_data) # noqa: N806 colors = colors or list(itertools.islice(itertools.cycle(color_cycle), ny)) assert len(x_data) == nx, f"{x_data} !~ {y_data}" @@ -375,14 +379,15 @@ def general( color = alt.Color( "key:N", scale=alt.Scale(domain=labels, range=colors), - legend=alt.Undefined if legend else None, + legend=alt.Undefined if keep_legend else None, ) chart = alt.Chart(data) - kwargs = {} if mark_kwargs is None else mark_kwargs if mark == Mark.point: + kwargs = {"filled": True, "opacity": 1} if mark_kwargs is None else mark_kwargs chart = chart.mark_point(**kwargs) else: + kwargs = {} if mark_kwargs is None else mark_kwargs chart = chart.mark_line(**kwargs) # Create chart diff --git a/src/automol/extern/molfile.py b/src/automol/extern/molfile.py index 09784703..26d3c534 100644 --- a/src/automol/extern/molfile.py +++ b/src/automol/extern/molfile.py @@ -24,11 +24,12 @@ class FMT(): COUNTS_KEY = 'counts' ATOM_KEY = 'atom' BOND_KEY = 'bond' - STRING = (_HEAD + _BEGIN(_CTAB) + - _ENTRY(key=COUNTS_KEY, fmt='s') + - _BEGIN(_ATOM) + _ENTRY(key=ATOM_KEY, fmt='s') + _END(_ATOM) + - _BEGIN(_BOND) + _ENTRY(key=BOND_KEY, fmt='s') + _END(_BOND) + - _END(_CTAB) + _FOOT).format + + HEADER_ = ( + _HEAD + _BEGIN(_CTAB) + _ENTRY(key=COUNTS_KEY, fmt='s')).format + ATOM_BLOCK_ = (_BEGIN(_ATOM) + _ENTRY(key=ATOM_KEY, fmt='s') + _END(_ATOM)).format + BOND_BLOCK_ = (_BEGIN(_BOND) + _ENTRY(key=BOND_KEY, fmt='s') + _END(_BOND)).format + FOOTER = _END(_CTAB) + _FOOT class COUNTS(): """ _ """ @@ -90,15 +91,23 @@ def from_data(atm_keys, bnd_keys, atm_syms, atm_bnd_vlcs, atm_rad_vlcs, **{FMT.COUNTS.NA_KEY: natms, FMT.COUNTS.NB_KEY: nbnds}) - atom_block = _atom_block(atm_keys, key_map, atm_syms, atm_bnd_vlcs, + inner_atom_block = _atom_block(atm_keys, key_map, atm_syms, atm_bnd_vlcs, atm_rad_vlcs, atm_xyzs=atm_xyzs) - bond_block = _bond_block(bnd_keys, key_map, bnd_ords, + inner_bond_block = _bond_block(bnd_keys, key_map, bnd_ords, with_stereo=(atm_xyzs is not None)) - mlf = FMT.STRING(**{FMT.COUNTS_KEY: counts_line, - FMT.ATOM_KEY: atom_block, - FMT.BOND_KEY: bond_block}) + parts = [ + FMT.HEADER_(**{FMT.COUNTS_KEY: counts_line}), + FMT.ATOM_BLOCK_(**{FMT.ATOM_KEY: inner_atom_block}), + ] + + if nbnds > 0: + parts.append(FMT.BOND_BLOCK_(**{FMT.BOND_KEY: inner_bond_block})) + + parts.append(FMT.FOOTER) + + mlf = ''.join(parts) # for recovering the original keys from those used in the molfile key_map_inv = dict(map(reversed, key_map.items())) diff --git a/src/automol/geom/_1conv.py b/src/automol/geom/_1conv.py index 711ec6ab..3ae11c4b 100644 --- a/src/automol/geom/_1conv.py +++ b/src/automol/geom/_1conv.py @@ -9,6 +9,7 @@ import pyparsing as pp from numpy.typing import ArrayLike from pyparsing import pyparsing_common as ppc +from rdkit.Chem.inchi import MolBlockToInchiAndAuxInfo from phydat import phycon from .. import vmat @@ -340,38 +341,10 @@ def inchi_with_numbers(geo, stereo=True, gra=None): gra = graph_base.set_stereo_from_geometry(gra, geo) mlf, key_map_inv = molfile_with_atom_mapping(gra, geo=geo) - rdm = rdkit_.from_molfile(mlf) - ich, aux_info = rdkit_.to_inchi(rdm, with_aux_info=True) + ich, aux_info = MolBlockToInchiAndAuxInfo(mlf) nums_lst = _parse_sort_order_from_aux_info(aux_info) nums_lst = tuple(tuple(map(key_map_inv.__getitem__, nums)) for nums in nums_lst) - - # Assuming the MolFile InChI works, the above code is all we need. What - # follows is to correct cases where it fails. - # This only appears to work sometimes, so when it doesn't, we fall back on - # the original inchi output. - if geo is not None: - gra = graph_base.set_stereo_from_geometry(gra, geo) - gra = graph_base.implicit(gra) - sub_ichs = inchi_base.split(ich) - - failed = False - - new_sub_ichs = [] - for sub_ich, nums in zip(sub_ichs, nums_lst): - sub_gra = graph_base.subgraph(gra, nums, stereo=True) - sub_ich = _connected_inchi_with_graph_stereo(sub_ich, sub_gra, nums) - if sub_ich is None: - failed = True - break - - new_sub_ichs.append(sub_ich) - - # If it worked, replace the InChI with our forced-stereo InChI. - if not failed: - ich = inchi_base.join(new_sub_ichs) - ich = inchi_base.standard_form(ich) - return ich, nums_lst