From 2587e791e9a86a0fb7d13817be93138ef38e7cc6 Mon Sep 17 00:00:00 2001 From: Andreas Copan Date: Thu, 13 Feb 2025 15:07:08 -0500 Subject: [PATCH 1/2] Refactor: Stick with functional rate interface Internally, a strongly object-oriented approach is necessary for managing rate constants, but this isn't necessary for the final `Rate` object. Instead, users can interact with the final rate object through a friendly functional API. --- autochem/rate/_01const.py | 8 ++--- autochem/rate/_02rate.py | 69 +++++++++++++++++++----------------- autochem/rate/__init__.py | 2 ++ autochem/tests/test__rate.py | 2 +- 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/autochem/rate/_01const.py b/autochem/rate/_01const.py index 00eb5268..a9f86e7b 100644 --- a/autochem/rate/_01const.py +++ b/autochem/rate/_01const.py @@ -91,7 +91,7 @@ def process_output(self, ktp: ArrayLike) -> NDArray[numpy.float64]: def display( self, - others: "Mapping[str, RateConstant] | None" = None, + comp_rates: "Mapping[str, RateConstant] | None" = None, t_range: tuple[Number, Number] = (400, 1250), p: Number = 1, units: UnitsData | None = None, @@ -119,8 +119,8 @@ def display( y_label = f"{y_label} ({y_unit})" # Gather functions - others = {} if others is None else others - funcs = {label: self, **others} + comp_rates = {} if comp_rates is None else comp_rates + funcs = {label: self, **comp_rates} # Gether data from functons t = numpy.linspace(*t_range, num=500) @@ -144,7 +144,7 @@ def display( .scale(type="log") .axis(format=".1e", values=y_vals) ) - color = "key:N" if others else altair.Undefined + color = "key:N" if comp_rates else altair.Undefined # Create chart return ( diff --git a/autochem/rate/_02rate.py b/autochem/rate/_02rate.py index 35bda011..3b631f0f 100644 --- a/autochem/rate/_02rate.py +++ b/autochem/rate/_02rate.py @@ -78,39 +78,6 @@ def __call__( """ return self.rate_constant(t=t, p=p, units=units) - def display( - self, - others: "Mapping[str, Rate] | None" = None, - t_range: tuple[Number, Number] = (400, 1250), - p: Number = 1, - units: UnitsData | None = None, - label: str = "This work", - x_label: str = "1000/T", - y_label: str = "k", - ) -> altair.Chart: - """Display as an Arrhenius plot. - - :param others: Other rate constants by label - :param t_range: Temperature range - :param p: Pressure - :param units: Units - :param x_label: X-axis label - :param y_label: Y-axis label - :return: Chart - """ - if others is not None: - others = {lab: obj.rate_constant for lab, obj in others.items()} - - return self.rate_constant.display( - others=others, - t_range=t_range, - p=p, - units=units, - label=label, - x_label=x_label, - y_label=y_label, - ) - # Constructors def from_chemkin_string( @@ -252,3 +219,39 @@ def chemkin_string(rate: Rate, eq_width: int = 55, dup: bool = False) -> str: rate_str = rate_constant_chemkin_string(rate.rate_constant, eq_width=eq_width) reac_str = f"{eq:<{eq_width}} {rate_str}" return chemkin.write_with_dup(reac_str, dup=dup) + + +# Display +def display( + rate: Rate, + comp_rates: "Mapping[str, Rate] | None" = None, + t_range: tuple[Number, Number] = (400, 1250), + p: Number = 1, + units: UnitsData | None = None, + label: str = "This work", + x_label: str = "1000/T", + y_label: str = "k", +) -> altair.Chart: + """Display as an Arrhenius plot. + + :param rate: Rate + :param others: Other rate constants by label + :param t_range: Temperature range + :param p: Pressure + :param units: Units + :param x_label: X-axis label + :param y_label: Y-axis label + :return: Chart + """ + if comp_rates is not None: + comp_rates = {lab: obj.rate_constant for lab, obj in comp_rates.items()} + + return rate.rate_constant.display( + comp_rates=comp_rates, + t_range=t_range, + p=p, + units=units, + label=label, + x_label=x_label, + y_label=y_label, + ) diff --git a/autochem/rate/__init__.py b/autochem/rate/__init__.py index ee3f4d9c..aea704a4 100644 --- a/autochem/rate/__init__.py +++ b/autochem/rate/__init__.py @@ -21,6 +21,7 @@ Rate, chemkin_equation, chemkin_string, + display, expand_lumped, from_chemkin_string, ) @@ -42,6 +43,7 @@ "TroeBlendingFunction", "chemkin_equation", "chemkin_string", + "display", "expand_lumped", "from_chemkin_string", ] diff --git a/autochem/tests/test__rate.py b/autochem/tests/test__rate.py index 8a91484c..624c5740 100644 --- a/autochem/tests/test__rate.py +++ b/autochem/tests/test__rate.py @@ -126,7 +126,7 @@ def test__from_chemkin_string(name, data, check_roundtrip: bool): other_units = SIMPLE.get("units") other_chem_str = SIMPLE.get("chemkin") other_k = rate.from_chemkin_string(other_chem_str, units=other_units) - k.display({"Other": other_k}) + rate.display(k, {"Other": other_k}) if __name__ == "__main__": From 37564b375bcdb3dbb1c895e8b9bc5cbdae43ad8e Mon Sep 17 00:00:00 2001 From: Andreas Copan Date: Thu, 13 Feb 2025 15:58:14 -0500 Subject: [PATCH 2/2] Refactor: Pass in labels as a separate argument Passing labels and objects in as a dictionary turns out to be less convenient than one would think. --- autochem/rate/_01const.py | 19 +++++++++++-------- autochem/rate/_02rate.py | 14 +++++++------- autochem/tests/test__rate.py | 8 ++++---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/autochem/rate/_01const.py b/autochem/rate/_01const.py index a9f86e7b..7fc966ad 100644 --- a/autochem/rate/_01const.py +++ b/autochem/rate/_01const.py @@ -1,7 +1,7 @@ """Rate constant models.""" import abc -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from typing import Annotated, ClassVar import altair @@ -91,7 +91,8 @@ def process_output(self, ktp: ArrayLike) -> NDArray[numpy.float64]: def display( self, - comp_rates: "Mapping[str, RateConstant] | None" = None, + others: "Sequence[RateConstant]" = (), + labels: Sequence[str] = (), t_range: tuple[Number, Number] = (400, 1250), p: Number = 1, units: UnitsData | None = None, @@ -101,7 +102,8 @@ def display( ) -> altair.Chart: """Display as an Arrhenius plot. - :param others: Other rate constants by label + :param others: Other rate constants + :param others_labels: Labels for other rate constants :param t_range: Temperature range :param p: Pressure :param units: Units @@ -118,13 +120,14 @@ def display( x_label = f"{x_label} ({x_unit})" y_label = f"{y_label} ({y_unit})" - # Gather functions - comp_rates = {} if comp_rates is None else comp_rates - funcs = {label: self, **comp_rates} + # Gather rate constants and labels + assert len(others) == len(labels), f"{labels} !~ {others}" + all_ks = [self, *others] + all_labels = [label, *labels] # Gether data from functons t = numpy.linspace(*t_range, num=500) - data_dct = {lab: func(t, p) for lab, func in funcs.items()} + data_dct = {lab: k(t, p) for lab, k in zip(all_labels, all_ks, strict=True)} data = pandas.DataFrame({"x": numpy.divide(1000, t), **data_dct}) # Determine exponent range @@ -144,7 +147,7 @@ def display( .scale(type="log") .axis(format=".1e", values=y_vals) ) - color = "key:N" if comp_rates else altair.Undefined + color = "key:N" if others else altair.Undefined # Create chart return ( diff --git a/autochem/rate/_02rate.py b/autochem/rate/_02rate.py index 3b631f0f..c5d2fb56 100644 --- a/autochem/rate/_02rate.py +++ b/autochem/rate/_02rate.py @@ -224,7 +224,8 @@ def chemkin_string(rate: Rate, eq_width: int = 55, dup: bool = False) -> str: # Display def display( rate: Rate, - comp_rates: "Mapping[str, Rate] | None" = None, + comp_rates: Sequence[Rate] = (), + comp_labels: Sequence[str] = (), t_range: tuple[Number, Number] = (400, 1250), p: Number = 1, units: UnitsData | None = None, @@ -232,10 +233,11 @@ def display( x_label: str = "1000/T", y_label: str = "k", ) -> altair.Chart: - """Display as an Arrhenius plot. + """Display as an Arrhenius plot, optionally comparing to other rates. :param rate: Rate - :param others: Other rate constants by label + :param comp_rates: Rates for comparison + :param comp_labels: Labels for comparison :param t_range: Temperature range :param p: Pressure :param units: Units @@ -243,11 +245,9 @@ def display( :param y_label: Y-axis label :return: Chart """ - if comp_rates is not None: - comp_rates = {lab: obj.rate_constant for lab, obj in comp_rates.items()} - return rate.rate_constant.display( - comp_rates=comp_rates, + others=[r.rate_constant for r in comp_rates], + labels=comp_labels, t_range=t_range, p=p, units=units, diff --git a/autochem/tests/test__rate.py b/autochem/tests/test__rate.py index 624c5740..3b6c7364 100644 --- a/autochem/tests/test__rate.py +++ b/autochem/tests/test__rate.py @@ -123,10 +123,10 @@ def test__from_chemkin_string(name, data, check_roundtrip: bool): assert k == k_, f"\n {k}\n!= {k_}" # Plot against another rate - other_units = SIMPLE.get("units") - other_chem_str = SIMPLE.get("chemkin") - other_k = rate.from_chemkin_string(other_chem_str, units=other_units) - rate.display(k, {"Other": other_k}) + copm_units = SIMPLE.get("units") + copm_chem_str = SIMPLE.get("chemkin") + comp_k = rate.from_chemkin_string(copm_chem_str, units=copm_units) + rate.display(k, comp_rates=[comp_k], comp_labels=["comp"]) if __name__ == "__main__":