From fb5218ac0210ef8843c93def8aa9e67e203e9392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:41:16 +0300 Subject: [PATCH 01/13] Raise the typeguard floor to 4.1.3 typeguard 4.1.0-4.1.2 do `from ast import Str` at import time; ast.Str was removed in Python 3.14 (still present in 3.13), so on 3.14 those releases -- and therefore `import encomp` -- fail with an opaque ImportError. All three ship py3-none-any wheels whose Requires-Python allows 3.14 and none is yanked, so a lowest-bounds resolution selects 4.1.0 on 3.14. Bisected empirically: 4.1.3 is the first release that imports on 3.14, and encomp works end-to-end with it (isinstance_types, the registered Quantity typeguard checker, self_check). Co-Authored-By: Claude Fable 5 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a0c4bc6..81eef9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "pint>=0.25.3", "pydantic>=2.4", "pydantic-settings>=2", - "typeguard>=4.1", + "typeguard>=4.1.3", "typing-extensions>=4.13", "numpy>=2.1", # The bundled Rust plugin is loaded over polars' plugin ABI (polars-ffi, major 0), diff --git a/uv.lock b/uv.lock index e76488f..925cdcf 100644 --- a/uv.lock +++ b/uv.lock @@ -567,7 +567,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.4" }, { name = "pydantic-settings", specifier = ">=2" }, { name = "sympy", specifier = ">=1.13" }, - { name = "typeguard", specifier = ">=4.1" }, + { name = "typeguard", specifier = ">=4.1.3" }, { name = "typing-extensions", specifier = ">=4.13" }, { name = "uncertainties", specifier = ">=3.2" }, ] From 618a13047e2f9538c6d833f0ab6ebaa53769b3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:46:42 +0300 Subject: [PATCH 02/13] Derive the non-strict ordering operators from the strict ones The fold previously combined pint's RAW >= / <= with the tolerant equality (`raw_ge | equal`). polars' total order calls NaN equal to NaN, so its raw `nan >= nan` is True while encomp's __eq__ says False -- on Series/Expr magnitudes `nan >= nan` and `nan <= nan` answered True with ==, > and < all False, breaking the documented `ge == (gt or eq)` decomposition within one container (the float path asserts the opposite result explicitly). Evaluate only the strict pint operator and derive the non-strict result as `(strict & ~equal) | equal`. For every non-NaN input this is equivalent to the previous fold (ties, +-inf and null propagation included, verified by fuzzing ~172k pairs per operator across all four containers); for nan-vs-nan it now answers False everywhere. The deliberate NaN-vs-number ordering divergence between the numpy and polars worlds is unchanged. nan-vs-nan joins _COMPARISON_PAIRS, which asserts container-independence for every operator. Co-Authored-By: Claude Fable 5 --- encomp/tests/test_comparisons.py | 17 ++++++++++++++++- encomp/units.py | 22 ++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/encomp/tests/test_comparisons.py b/encomp/tests/test_comparisons.py index 3db1335..1ec3ee9 100644 --- a/encomp/tests/test_comparisons.py +++ b/encomp/tests/test_comparisons.py @@ -161,8 +161,12 @@ def test_values_within_tolerance_are_ties_and_sort_stably() -> None: _INF = float("inf") +_NAN = float("nan") -# pairs that straddle the tolerance band, plus the non-finite corners +# pairs that straddle the tolerance band, plus the non-finite corners. NaN appears only +# paired with itself: numpy and polars deliberately disagree on ordering NaN against a +# NUMBER (see test_numpy_and_polars_disagree_on_nan_ordering), but nan-vs-nan answers +# identically everywhere (never equal, never ordered) _COMPARISON_PAIRS = [ (1.0, 1.0), (0.0, 0.0), @@ -178,6 +182,7 @@ def test_values_within_tolerance_are_ties_and_sort_stably() -> None: (-_INF, -_INF), (1.0, _INF), (-_INF, _INF), + (_NAN, _NAN), ] @@ -284,6 +289,16 @@ def test_numpy_and_polars_disagree_on_nan_ordering() -> None: assert (series > one).to_list() == [True] assert (series <= one).to_list() == [False] + # nan-vs-nan is the one NaN case the worlds AGREE on: polars' raw >= calls NaN equal + # to NaN (total order), but the non-strict operators are derived as `strict or equal` + # with encomp's tolerant __eq__ (NaN is never equal), so every container answers + # False for all four orderings -- `ge == (gt or eq)` holds even here + other_nan = Q(pl.Series([nan]), "m") + assert (series >= other_nan).to_list() == [False] + assert (series <= other_nan).to_list() == [False] + assert (series > other_nan).to_list() == [False] + assert (series == other_nan).to_list() == [False] + # a null magnitude propagates as null in both directions with_null = Q(pl.Series([1.0, None]), "m") assert (with_null > one).to_list() == [False, None] diff --git a/encomp/units.py b/encomp/units.py index e95ffe3..1765cd4 100644 --- a/encomp/units.py +++ b/encomp/units.py @@ -2507,20 +2507,30 @@ def _ordering_comparison( if isinstance(other, Quantity): self._check_comparable_magnitudes(self.m, other.m) + # only the STRICT pint operator is ever evaluated; a non-strict result is derived + # from it below. Using pint's raw >= / <= would poison the derivation for NaN: + # polars' total order calls NaN equal to NaN, so its raw `nan >= nan` is True while + # encomp's tolerant __eq__ says False -- and `ge == (gt or eq)` would not hold. + strict_op: str = {"__ge__": "__gt__", "__le__": "__lt__"}.get(op, op) + try: - ret = getattr(self._pint_super, op)(other) + ret = getattr(self._pint_super, strict_op)(other) except (ValueError, DimensionalityError) as e: raise DimensionalityComparisonError(str(e)) from e # __eq__ is tolerant (rtol, atol), so every ordering operator must agree with it or the - # five relations do not form an ordering. pint compares exactly, so fold the tolerance in - # here: the non-strict operators absorb equality, the strict ones exclude it. Without the - # strict side, `a > b` and `a <= b` were both True for operands equal within tolerance, - # which breaks sorted()/bisect and any code assuming `a > b` implies `not (a <= b)`. + # five relations do not form an ordering. pint compares exactly, so fold the tolerance + # in here: the strict operators exclude equality, and the non-strict ones are exactly + # `strict or equal`. This keeps `a > b` implies `not (a <= b)` (which sorted()/bisect + # assume), and makes `ge == (gt or eq)` / `le == (lt or eq)` hold for EVERY input -- + # including NaN operands on polars magnitudes -- in all four magnitude containers. equal = cast(Any, self).__eq__(other) not_equal = (not equal) if isinstance(equal, bool) else ~equal - ret = (ret | equal) if op in ("__ge__", "__le__") else (ret & not_equal) + ret = ret & not_equal + + if op in ("__ge__", "__le__"): + ret = ret | equal if isinstance(ret, np.bool): return bool(cast(Any, ret)) From e834b63a1418e242ca03df03b97ea3168ddf4e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:51:10 +0300 Subject: [PATCH 03/13] Run the dependency-floors job on 3.13 and 3.14 The job resolved and ran the suite on 3.13 only. A pure-Python wheel installs on any Python, so --only-binary cannot stop a floor that resolves on both supported Pythons but is broken on one: typeguard 4.1.0 (the previous floor) imports ast.Str, which 3.14 removed -- the job passed on 3.13 while `import encomp` failed on 3.14. Cover the oldest and newest supported Python so a version-dependent floor fails in CI. Verified locally for the 3.14 leg: the lowest resolution picks typeguard==4.1.3 / pydantic==2.12.0 / pydantic-settings==2.0.0, and the installed suite passes (770 passed, 10 skipped). Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01d84ce..e37285f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,13 +101,22 @@ jobs: # Python: a bound may legitimately name a release that predates 3.13 and ships no wheel # for it (pydantic 2.4 is the floor for the core-schema API, but its pydantic-core has # no cp313 wheel). Resolving to a version nobody can install would test nothing. - name: dependency floors + # + # The matrix covers the OLDEST and NEWEST supported Python: a pure-Python wheel + # installs on any Python, so --only-binary cannot stop a floor that resolves on + # every supported Python but only works on some of them (e.g. one that uses a + # stdlib API the newest Python removed). + name: dependency floors (${{ matrix.python-version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.13", "3.14"] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: - python-version: "3.13" + python-version: ${{ matrix.python-version }} - name: Build the bundled CoolProp library run: | python -m pip install cmake @@ -126,9 +135,9 @@ jobs: # and then run doc snippets against the source tree, which has no compiled plugin. # Outside it, they skip -- exactly as they do for someone who pip-installed a wheel. run: | - uv pip compile pyproject.toml --python-version 3.13 --resolution lowest-direct --only-binary=:all: -o floors.txt + uv pip compile pyproject.toml --python-version ${{ matrix.python-version }} --resolution lowest-direct --only-binary=:all: -o floors.txt cat floors.txt - uv venv "$RUNNER_TEMP/floors-venv" --python 3.13 + uv venv "$RUNNER_TEMP/floors-venv" --python ${{ matrix.python-version }} PY=$RUNNER_TEMP/floors-venv/bin/python uv pip install --python "$PY" -r floors.txt uv pip install --python "$PY" --no-deps . From 0b303de22f2fed379d3916dd9574489c40ed9587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:51:10 +0300 Subject: [PATCH 04/13] Reject Series-vs-Expr magnitude comparisons like the other mixed containers The mixed-container guard covered only numpy-vs-polars. A pl.Series quantity compared against a pl.Expr quantity fell through to polars' raw operator, which lifts the Series into an Expr literal and compares EXACTLY -- silently skipping the (rtol, atol) tolerance every sanctioned path applies (within-tolerance operands answered False), with a length mismatch surfacing only at collect() time as a ShapeError pointing into the plan. The pair is outside the typed API exactly like numpy-vs-polars, and the guard's stated principle is to refuse instead of picking silently. Treat any two DIFFERENT magnitude containers as mixed, and point the error at the right escape hatch: .astype() conversion for eager containers, or evaluating / explicitly pl.lit()-lifting when a pl.Expr is involved (an Expr has no data for .astype to convert). Co-Authored-By: Claude Fable 5 --- encomp/tests/test_comparisons.py | 29 +++++++++++++++++++++++++- encomp/units.py | 35 ++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/encomp/tests/test_comparisons.py b/encomp/tests/test_comparisons.py index 1ec3ee9..b3642c8 100644 --- a/encomp/tests/test_comparisons.py +++ b/encomp/tests/test_comparisons.py @@ -263,15 +263,42 @@ def test_comparing_numpy_and_polars_magnitudes_raises() -> None: series = Q(pl.Series([1.0]), "m") expr = Q(pl.col("x"), "m") - for lhs, rhs in ((array, series), (series, array), (array, expr), (expr, array)): + for lhs, rhs in ((array, series), (series, array)): for op in _OPERATORS.values(): with pytest.raises(TypeError, match="convert one side first"): op(lhs, rhs) + # when a pl.Expr is involved there is no data to convert on that side, so the + # message points at evaluating the expression or lifting the other operand instead + for lhs, rhs in ((array, expr), (expr, array)): + for op in _OPERATORS.values(): + with pytest.raises(TypeError, match="lift the other side"): + op(lhs, rhs) + # the escape hatch is explicit conversion assert (array.astype("pl.Series") == series).to_list() == [True] +def test_comparing_series_and_expr_magnitudes_raises() -> None: + # Series-vs-Expr is equally outside the typed API: polars' raw operator would lift the + # Series into an Expr literal and compare EXACTLY, skipping the (rtol, atol) tolerance + # every sanctioned path applies, with a length mismatch surfacing only at collect() + # time as a ShapeError pointing into the plan -- so the runtime refuses this pair too + series = Q(pl.Series([1.0]), "m") + expr = Q(pl.col("x"), "m") + + for lhs, rhs in ((series, expr), (expr, series)): + for op in _OPERATORS.values(): + with pytest.raises(TypeError, match="lift the other side"): + op(lhs, rhs) + + # the escape hatch is an explicit literal lift, which states the intent and lands on + # the sanctioned (tolerant) Expr-vs-Expr path + df = pl.DataFrame({"x": [1.0 + 5e-10, 2.0]}) + lifted = Q(pl.lit(pl.Series([1.0, 2.0])), "m") + assert df.select((expr == lifted).alias("r"))["r"].to_list() == [True, True] + + def test_numpy_and_polars_disagree_on_nan_ordering() -> None: # numpy follows IEEE: every comparison against NaN is False. polars orders NaN as the largest # value. Neither is wrong, but they are not interchangeable -- which is why the two worlds are diff --git a/encomp/units.py b/encomp/units.py index 1765cd4..920552d 100644 --- a/encomp/units.py +++ b/encomp/units.py @@ -620,25 +620,34 @@ def _get_magnitude_type_name(mt: object) -> MagnitudeTypeName: @classmethod def _check_comparable_magnitudes(cls, m: object, other_m: object) -> None: - # The numpy world and the polars world do not compare elementwise against each other: - # numpy raises an opaque ufunc-loop TypeError, polars an equally opaque dtype error, and - # pl.Expr silently lifts an ndarray into a literal. None of these combinations is in the - # typed API (the __eq__/__gt__/... overloads pair a magnitude type with itself or with a - # scalar), so reject them here with an actionable message instead + # Mixed containers do not compare elementwise: numpy-vs-polars raises an opaque + # ufunc-loop TypeError or dtype error, and pl.Expr silently lifts an ndarray OR a + # pl.Series into a literal and compares EXACTLY, skipping the (rtol, atol) tolerance + # every sanctioned path applies -- with a length mismatch surfacing only at collect() + # time as a ShapeError pointing into the plan. None of these combinations is in the + # typed API (the __eq__/__gt__/... overloads pair a magnitude type with itself or with + # a scalar), so reject them here with an actionable message instead if cls._is_mixed_container(m, other_m): + if isinstance(m, pl.Expr) or isinstance(other_m, pl.Expr): + hint = ( + "evaluate the pl.Expr side first, or lift the other side into the " + "expression explicitly (e.g. with pl.lit(...)) if literal semantics are intended" + ) + else: + hint = 'convert one side first, e.g. with .astype("pl.Series") or .astype("ndarray")' + raise TypeError( f"Cannot compare a Quantity with {cls._describe_magnitude(m)} magnitude to one with " - f"{cls._describe_magnitude(other_m)} magnitude; convert one side first, " - 'e.g. with .astype("pl.Series") or .astype("ndarray")' + f"{cls._describe_magnitude(other_m)} magnitude; {hint}" ) - @staticmethod - def _is_mixed_container(m: object, other_m: object) -> bool: - polars_types = (pl.Series, pl.Expr) + @classmethod + def _is_mixed_container(cls, m: object, other_m: object) -> bool: + container_kinds = ("ndarray", "pl.Series", "pl.Expr") + m_kind = cls._describe_magnitude(m) + other_kind = cls._describe_magnitude(other_m) - return (isinstance(m, np.ndarray) and isinstance(other_m, polars_types)) or ( - isinstance(m, polars_types) and isinstance(other_m, np.ndarray) - ) + return m_kind in container_kinds and other_kind in container_kinds and m_kind != other_kind @staticmethod def _describe_magnitude(value: object) -> str: From 8e9c1fb4af1741e77de1949038991f98ee6a290b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:52:35 +0300 Subject: [PATCH 05/13] Reject a NaN density when convert_volume_mass gets a Polars input A missing density yields a missing result at that position: NaN for float/numpy magnitudes, null for a Polars Series. A scalar NaN density (or an ndarray density with NaN rows) paired with a pl.Series input slipped between the container-specific guards and produced a Series result carrying NaN -- the sentinel a polars magnitude never uses, and the exact leak the Series-density branch rejects with 'use null for a missing density'. A float/ndarray density has no null spelling, so with a Polars input a NaN density cannot express 'missing': reject it with the same pointer at a null-carrying pl.Series. NaN densities remain the valid missing sentinel for float and numpy inputs. Co-Authored-By: Claude Fable 5 --- encomp/conversion.py | 20 ++++++++++++++++++-- encomp/tests/test_conversion.py | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/encomp/conversion.py b/encomp/conversion.py index 9db3a63..e40390e 100644 --- a/encomp/conversion.py +++ b/encomp/conversion.py @@ -49,7 +49,9 @@ def convert_volume_mass( rho : Quantity[Density, Any] Density of the substance. A *missing* density is allowed and yields a missing result at that position, exactly as a missing ``inp`` does: ``NaN`` for float and - numpy magnitudes, ``null`` for a Polars Series. Every *present* value must be + numpy magnitudes, ``null`` for a Polars Series. A Polars result carries null + (never NaN), so with a Polars ``inp`` a NaN density is rejected -- spell the + missing density as a null in a ``pl.Series``. Every *present* value must be finite and strictly positive. Polars Expr inputs are deferred and cannot be data-validated here. @@ -62,15 +64,29 @@ def convert_volume_mass( if not isinstance_types(rho, Quantity[Density, Any]): raise ExpectedDimensionalityError(f"rho must be a Quantity[Density], passed {rho!r} ({type(rho).__name__})") + # a NaN density means "missing" for float/numpy data, but a polars result carries null, + # never NaN -- and a float/ndarray density has no null spelling. A NaN there would flow + # into the Series/Expr result as NaN, so it is rejected when the input is polars + inp_is_polars = isinstance(inp.m, (pl.Series, pl.Expr)) + nan_rho_message = ( + "rho must not be NaN when inp has a Polars magnitude; " + "pass rho as a pl.Series with null for a missing density, got {rho!r}" + ) + rho_m = rho.to("kg/m³").m if isinstance(rho_m, float): - if not np.isnan(rho_m) and (not np.isfinite(rho_m) or rho_m <= 0.0): + if np.isnan(rho_m): + if inp_is_polars: + raise ValueError(nan_rho_message.format(rho=rho)) + elif not np.isfinite(rho_m) or rho_m <= 0.0: raise ValueError(f"rho must be finite and positive, got {rho!r}") elif isinstance(rho_m, np.ndarray): rho_arr = cast("Numpy1DArray", rho_m) present = ~np.isnan(rho_arr) if bool(np.any(present & (~np.isfinite(rho_arr) | (rho_arr <= 0.0)))): raise ValueError(f"rho must contain only finite positive values, got {rho!r}") + if inp_is_polars and not bool(present.all()): + raise ValueError(nan_rho_message.format(rho=rho)) elif isinstance(rho_m, pl.Series): # null is the missing sentinel in the polars world and propagates through the # arithmetic below; a NaN there would leak into the result, where polars magnitudes diff --git a/encomp/tests/test_conversion.py b/encomp/tests/test_conversion.py index ac2a70d..429cb55 100644 --- a/encomp/tests/test_conversion.py +++ b/encomp/tests/test_conversion.py @@ -101,3 +101,22 @@ def test_convert_volume_mass_polars_series_density() -> None: # a null density is missing, not invalid: it propagates nulled = convert_volume_mass(mass, rho=Q(pl.Series([1000.0, None]), "kg/m³")) assert nulled.to("m³").m.to_list() == [0.002, None] + + +def test_nan_density_is_rejected_for_polars_inputs() -> None: + # a polars result carries null, never NaN, and a float/ndarray density has no null + # spelling -- so a NaN density cannot express "missing" when the input is polars + mass_series = Q(pl.Series([2.0, 4.0]), "kg") + mass_expr = Q(pl.col("m"), "kg") + + with pytest.raises(ValueError, match="null for a missing density"): + convert_volume_mass(mass_series, rho=Q(float("nan"), "kg/m³")) + + with pytest.raises(ValueError, match="null for a missing density"): + convert_volume_mass(mass_expr, rho=Q(float("nan"), "kg/m³")) + + with pytest.raises(ValueError, match="null for a missing density"): + convert_volume_mass(mass_series, rho=Q(np.array([1000.0, np.nan]), "kg/m³")) + + # the same missing density is fine for float/numpy inputs, where NaN IS the sentinel + assert np.isnan(convert_volume_mass(Q(2.0, "kg"), rho=Q(float("nan"), "kg/m³")).m) From 0634f1bf107b34bc9e6e814bf1b3def0db981026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:53:11 +0300 Subject: [PATCH 06/13] Point the __hash__ tolerance note at the (rtol, atol) predicate The note cited np.isclose, whose asymmetric predicate is precisely what the module-level _is_close exists to avoid; name the tolerance pair instead of an implementation the equality operator does not use. Co-Authored-By: Claude Fable 5 --- encomp/units.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/encomp/units.py b/encomp/units.py index 920552d..5109acb 100644 --- a/encomp/units.py +++ b/encomp/units.py @@ -510,9 +510,9 @@ def __hash__(self) -> int: # hash on the canonical root-unit representation so the eq/hash contract holds: # __eq__ compares across units (Q(1, "m") == Q(100, "cm")), so two quantities that # are equal must hash equal -- hashing the raw (m, u) would break that. - # NOTE: __eq__ is tolerant (np.isclose), so quantities that are only *approximately* + # NOTE: __eq__ is tolerant (rtol, atol), so quantities that are only *approximately* # equal may still hash differently -- an inherent limit of tolerant equality, the same - # way hash(0.1 + 0.2) != hash(0.3); exact equality (the common case) is now consistent. + # way hash(0.1 + 0.2) != hash(0.3); exact equality (the common case) is consistent. root = self.to_root_units() # __eq__ also answers True against plain numbers (Q(0.5, "") == 0.5), so a From 387dcbd49494717e54a09f572c53e6206537863c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:54:26 +0300 Subject: [PATCH 07/13] Attribute the arithmetic inference to the operator overloads in the README The overview bullet credited Quantity.__new__ for the * / ` / ** result dimensionalities; __new__ overloads encode the unit literals, and the combinations are __mul__/__truediv__/__pow__ overloads, as the type-system section further down already states. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a824a6b..44edabe 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Every physical quantity in `encomp` carries a magnitude, a unit, and a dimension - **`Quantity`** (`encomp.units`, `encomp.utypes`) extends [pint](https://pypi.org/project/Pint): each dimensionality (pressure, mass flow, density, ...) is a distinct subclass of `Quantity`. Dividing a `Mass` by a `Volume` gives a `Density`, inferred by static type checkers and verified at runtime. Magnitudes can be scalars, NumPy arrays, Polars `Series`, or Polars `Expr`. -- **Static unit checking.** Common dimensionalities and their `*` / `/` / `**` combinations are encoded as `__new__` overloads, so a type checker flags a `Temperature` passed where a `Power` is expected. `@typeguard.typechecked` extends the same checks to runtime. +- **Static unit checking.** Common dimensionalities are encoded as `Quantity.__new__` overloads, and their `*` / `/` / `**` combinations as arithmetic-operator overloads, so a type checker flags a `Temperature` passed where a `Power` is expected. `@typeguard.typechecked` extends the same checks to runtime. - **`Fluid` / `Water` / `HumidAir`** (`encomp.fluids`) wrap [CoolProp](http://www.coolprop.org): fix a state with two points (three for humid air) and read any property as an attribute. Inputs and outputs are `Quantity` objects, so units are converted and validated automatically. From 40eedca1cf729408fd8c0c84020d2a1293885dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:55:23 +0300 Subject: [PATCH 08/13] State the actual versioning policy instead of claiming semver The versioning sections promised semantic versioning for documented public APIs; the release practice keeps those APIs stable where possible and announces any breaking change in the GitHub release notes, without reserving such changes for major versions. Say that. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- docs/usage.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 44edabe..67d1b70 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The remaining modules (`encomp.gases`, `encomp.conversion`, `encomp.constants`, ## Versioning and stability -`encomp` uses semantic versioning for documented public APIs. Public APIs are the documented modules and objects in the API reference; private helpers, tests, notebooks, generated docs, and Rust internals may change in any release. The top-level `encomp` package intentionally exposes only `__version__`; import library APIs from their submodules. +`encomp` does not follow strict semantic versioning: releases keep the documented public APIs stable where possible, and any breaking change to them is called out in the [GitHub release notes](https://github.com/wlaur/encomp/releases). Public APIs are the documented modules and objects in the API reference; private helpers, tests, notebooks, generated docs, and Rust internals may change in any release. The top-level `encomp` package intentionally exposes only `__version__`; import library APIs from their submodules. `encomp.sympy` is legacy and soft-deprecated. It remains available for existing users, but new code should avoid depending on its `sympy.Symbol` monkey-patching and helper wrappers because the module is planned for removal in a future major release. diff --git a/docs/usage.md b/docs/usage.md index 51183f6..75f33d9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -4,7 +4,7 @@ This guide covers the main `encomp` modules: units, fluids, and symbolic math. ## Versioning and stability -`encomp` uses semantic versioning for documented public APIs. Public APIs are the documented modules and objects in the API reference; private helpers, tests, notebooks, generated docs, and Rust internals may change in any release. The top-level `encomp` package intentionally exposes only `__version__`; import library APIs from their submodules. +`encomp` does not follow strict semantic versioning: releases keep the documented public APIs stable where possible, and any breaking change to them is called out in the [GitHub release notes](https://github.com/wlaur/encomp/releases). Public APIs are the documented modules and objects in the API reference; private helpers, tests, notebooks, generated docs, and Rust internals may change in any release. The top-level `encomp` package intentionally exposes only `__version__`; import library APIs from their submodules. `encomp.sympy` is legacy and soft-deprecated. It remains available for existing users, but new code should avoid depending on its `sympy.Symbol` monkey-patching and helper wrappers because the module is planned for removal in a future major release. From dfced806e8478ae51f61a3bc1e0a162ea86da04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:55:40 +0300 Subject: [PATCH 09/13] List water in the coolprop README file overview The Files section named fluid and humid_air as the package's public API; water is documented in the same README's API list above. Co-Authored-By: Claude Fable 5 --- encomp/coolprop/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encomp/coolprop/README.md b/encomp/coolprop/README.md index 4c37531..2981d10 100644 --- a/encomp/coolprop/README.md +++ b/encomp/coolprop/README.md @@ -147,7 +147,7 @@ PYO3_PYTHON=$(pwd)/.venv/bin/python cargo test --manifest-path rust/Cargo.toml - ## Files -- `encomp/coolprop/__init__.py` — public Python API (`fluid`, `humid_air`). +- `encomp/coolprop/__init__.py` — public Python API (`fluid`, `water`, `humid_air`). - `rust/src/lib.rs` — the `cp_evaluate` / `ha_evaluate` plugin expressions (crate at repo root). - `rust/src/coolprop.rs` — the CoolProp C-API bindings + thread-safety model (all `unsafe`). - `scripts/build_libcoolprop.py` — builds + bundles CoolProp's shared library. From 650570af60073e8df543fb152d18bc6296aad4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Fri, 10 Jul 2026 15:56:44 +0300 Subject: [PATCH 10/13] Refresh the polars-expr-coolprop notebook from a clean top-to-bottom run The committed state was an out-of-order interactive-session snapshot: execution counts 19, 20, 39, 21, ..., a stale 'extension is already loaded' stream in the setup cell, and no outputs for the %%pyinstrument cell. Re-execute sequentially (counts 1-14) and drop the pyinstrument profile display from the committed outputs -- it embeds machine-local filesystem paths -- keeping the cell's DataFrame result. Co-Authored-By: Claude Fable 5 --- notebooks/polars-expr-coolprop.ipynb | 619 +++++++++++++++++++++++++-- 1 file changed, 576 insertions(+), 43 deletions(-) diff --git a/notebooks/polars-expr-coolprop.ipynb b/notebooks/polars-expr-coolprop.ipynb index d62cf18..d540ad2 100644 --- a/notebooks/polars-expr-coolprop.ipynb +++ b/notebooks/polars-expr-coolprop.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 19, + "execution_count": 1, "id": "188463ef", "metadata": {}, "outputs": [], @@ -12,21 +12,10 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "id": "db0ff790", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n", - "The pyinstrument extension is already loaded. To reload it, use:\n", - " %reload_ext pyinstrument\n" - ] - } - ], + "outputs": [], "source": [ "%load_ext autoreload\n", "%load_ext pyinstrument\n", @@ -35,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 3, "id": "fdd6ea17", "metadata": {}, "outputs": [ @@ -65,16 +54,522 @@ }, { "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", - "application/vnd.holoviews_load.v0+json": "" + "application/javascript": [ + "(function(root) {\n", + " function now() {\n", + " return new Date();\n", + " }\n", + "\n", + " const force = true;\n", + " const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", + " const reloading = false;\n", + " const Bokeh = root.Bokeh;\n", + " const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n", + " const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n", + "\n", + " // Set a timeout for this load but only if we are not already initializing\n", + " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_failed_load = false;\n", + " }\n", + "\n", + " function run_callbacks() {\n", + " try {\n", + " root._bokeh_onload_callbacks.forEach(function(callback) {\n", + " if (callback != null)\n", + " callback();\n", + " });\n", + " } finally {\n", + " delete root._bokeh_onload_callbacks;\n", + " }\n", + " console.debug(\"Bokeh: all callbacks have finished\");\n", + " }\n", + "\n", + " function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n", + " if (css_urls == null) css_urls = [];\n", + " if (js_urls == null) js_urls = [];\n", + " if (js_modules == null) js_modules = [];\n", + " if (js_exports == null) js_exports = {};\n", + "\n", + " root._bokeh_onload_callbacks.push(callback);\n", + "\n", + " if (root._bokeh_is_loading > 0) {\n", + " // Don't load bokeh if it is still initializing\n", + " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", + " return null;\n", + " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", + " // There is nothing to load\n", + " run_callbacks();\n", + " return null;\n", + " }\n", + "\n", + " function on_load() {\n", + " root._bokeh_is_loading--;\n", + " if (root._bokeh_is_loading === 0) {\n", + " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", + " run_callbacks()\n", + " }\n", + " }\n", + " window._bokeh_on_load = on_load\n", + "\n", + " function on_error(e) {\n", + " const src_el = e.srcElement\n", + " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", + " }\n", + "\n", + " const skip = [];\n", + " if (window.requirejs) {\n", + " window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n", + " root._bokeh_is_loading = css_urls.length + 0;\n", + " } else {\n", + " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", + " }\n", + "\n", + " const existing_stylesheets = []\n", + " const links = document.getElementsByTagName('link')\n", + " for (let i = 0; i < links.length; i++) {\n", + " const link = links[i]\n", + " if (link.href != null) {\n", + " existing_stylesheets.push(link.href)\n", + " }\n", + " }\n", + " for (let i = 0; i < css_urls.length; i++) {\n", + " const url = css_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", + " on_load()\n", + " continue;\n", + " }\n", + " const element = document.createElement(\"link\");\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.rel = \"stylesheet\";\n", + " element.type = \"text/css\";\n", + " element.href = url;\n", + " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", + " document.body.appendChild(element);\n", + " } var existing_scripts = []\n", + " const scripts = document.getElementsByTagName('script')\n", + " for (let i = 0; i < scripts.length; i++) {\n", + " var script = scripts[i]\n", + " if (script.src != null) {\n", + " existing_scripts.push(script.src)\n", + " }\n", + " }\n", + " for (let i = 0; i < js_urls.length; i++) {\n", + " const url = js_urls[i];\n", + " const escaped = encodeURI(url)\n", + " const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n", + " const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n", + " const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n", + " if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " const element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (let i = 0; i < js_modules.length; i++) {\n", + " const url = js_modules[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (const name in js_exports) {\n", + " const url = js_exports[name];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " element.textContent = `\n", + " import ${name} from \"${url}\"\n", + " window.${name} = ${name}\n", + " window._bokeh_on_load()\n", + " `\n", + " document.head.appendChild(element);\n", + " }\n", + " if (!js_urls.length && !js_modules.length) {\n", + " on_load()\n", + " }\n", + " };\n", + "\n", + " function inject_raw_css(css) {\n", + " const element = document.createElement(\"style\");\n", + " element.appendChild(document.createTextNode(css));\n", + " document.body.appendChild(element);\n", + " }\n", + "\n", + " const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n", + " const js_modules = [];\n", + " const js_exports = {};\n", + " const css_urls = [];\n", + " const inline_js = [ function(Bokeh) {\n", + " Bokeh.set_log_level(\"info\");\n", + " },\n", + "function(Bokeh) {} // ensure no trailing comma for IE\n", + " ];\n", + "\n", + " function run_inline_js() {\n", + " if ((root.Bokeh !== undefined) || (force === true)) {\n", + " for (let i = 0; i < inline_js.length; i++) {\n", + " try {\n", + " inline_js[i].call(root, root.Bokeh);\n", + " } catch(e) {\n", + " if (!reloading) {\n", + " throw e;\n", + " }\n", + " }\n", + " }\n", + " } else if (Date.now() < root._bokeh_timeout) {\n", + " setTimeout(run_inline_js, 100);\n", + " } else if (!root._bokeh_failed_load) {\n", + " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", + " root._bokeh_failed_load = true;\n", + " }\n", + " root._bokeh_is_initializing = false;\n", + " }\n", + "\n", + " function load_or_wait() {\n", + " // Implement a backoff loop that tries to ensure we do not load multiple\n", + " // versions of Bokeh and its dependencies at the same time.\n", + " // In recent versions we use the root._bokeh_is_initializing flag\n", + " // to determine whether there is an ongoing attempt to initialize\n", + " // bokeh, however for backward compatibility we also try to ensure\n", + " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", + " // before older versions are fully initialized.\n", + " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", + " // If the timeout and bokeh was not successfully loaded we reset\n", + " // everything and try loading again\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_is_initializing = false;\n", + " root._bokeh_onload_callbacks = undefined;\n", + " root._bokeh_is_loading = 0;\n", + " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", + " load_or_wait();\n", + " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", + " setTimeout(load_or_wait, 100);\n", + " } else {\n", + " root._bokeh_is_initializing = true;\n", + " root._bokeh_onload_callbacks = [];\n", + " const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n", + " if (!reloading && !bokeh_loaded) {\n", + " if (root.Bokeh) {\n", + " root.Bokeh = undefined;\n", + " }\n", + " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", + " }\n", + " load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n", + " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", + " run_inline_js();\n", + " if (Bokeh != undefined && !reloading) {\n", + " const NewBokeh = root.Bokeh;\n", + " if (Bokeh.versions === undefined) {\n", + " Bokeh.versions = new Map();\n", + " }\n", + " if (NewBokeh.version !== Bokeh.version) {\n", + " Bokeh[NewBokeh.version] = NewBokeh;\n", + " Bokeh.versions.set(NewBokeh.version, NewBokeh);\n", + " }\n", + " root.Bokeh = Bokeh;\n", + " }\n", + " });\n", + " }\n", + " }\n", + " // Give older versions of the autoload script a head-start to ensure\n", + " // they initialize before we start loading newer version.\n", + " setTimeout(load_or_wait, 100)\n", + "}(window));" + ], + "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" }, "metadata": {}, "output_type": "display_data" }, { "data": { - "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", - "application/vnd.holoviews_load.v0+json": "" + "application/javascript": [ + "\n", + "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", + " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", + "}\n", + "\n", + "\n", + " function JupyterCommManager() {\n", + " }\n", + "\n", + " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", + " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " comm_manager.register_target(comm_id, function(comm) {\n", + " comm.on_msg(msg_handler);\n", + " });\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", + " comm.onMsg = msg_handler;\n", + " });\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data, comm_id};\n", + " var buffers = []\n", + " for (var buffer of message.buffers || []) {\n", + " buffers.push(new DataView(buffer))\n", + " }\n", + " var metadata = message.metadata || {};\n", + " var msg = {content, buffers, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " })\n", + " }\n", + " }\n", + "\n", + " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", + " if (comm_id in window.PyViz.comms) {\n", + " return window.PyViz.comms[comm_id];\n", + " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", + " if (msg_handler) {\n", + " comm.on_msg(msg_handler);\n", + " }\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", + " let retries = 0;\n", + " const open = () => {\n", + " if (comm.active) {\n", + " comm.open();\n", + " } else if (retries > 3) {\n", + " console.warn('Comm target never activated')\n", + " } else {\n", + " retries += 1\n", + " setTimeout(open, 500)\n", + " }\n", + " }\n", + " if (comm.active) {\n", + " comm.open();\n", + " } else {\n", + " setTimeout(open, 500)\n", + " }\n", + " if (msg_handler) {\n", + " comm.onMsg = msg_handler;\n", + " }\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", + " comm_promise.then((comm) => {\n", + " window.PyViz.comms[comm_id] = comm;\n", + " if (msg_handler) {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data};\n", + " var metadata = message.metadata || {comm_id};\n", + " var msg = {content, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " })\n", + " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", + " return comm_promise.then((comm) => {\n", + " comm.send(data, metadata, buffers, disposeOnDone);\n", + " });\n", + " };\n", + " var comm = {\n", + " send: sendClosure\n", + " };\n", + " }\n", + " window.PyViz.comms[comm_id] = comm;\n", + " return comm;\n", + " }\n", + " window.PyViz.comm_manager = new JupyterCommManager();\n", + " \n", + "\n", + "\n", + "var JS_MIME_TYPE = 'application/javascript';\n", + "var HTML_MIME_TYPE = 'text/html';\n", + "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", + "var CLASS_NAME = 'output';\n", + "\n", + "/**\n", + " * Render data to the DOM node\n", + " */\n", + "function render(props, node) {\n", + " var div = document.createElement(\"div\");\n", + " var script = document.createElement(\"script\");\n", + " node.appendChild(div);\n", + " node.appendChild(script);\n", + "}\n", + "\n", + "/**\n", + " * Handle when a new output is added\n", + " */\n", + "function handle_add_output(event, handle) {\n", + " var output_area = handle.output_area;\n", + " var output = handle.output;\n", + " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", + " return\n", + " }\n", + " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", + " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", + " if (id !== undefined) {\n", + " var nchildren = toinsert.length;\n", + " var html_node = toinsert[nchildren-1].children[0];\n", + " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var scripts = [];\n", + " var nodelist = html_node.querySelectorAll(\"script\");\n", + " for (var i in nodelist) {\n", + " if (nodelist.hasOwnProperty(i)) {\n", + " scripts.push(nodelist[i])\n", + " }\n", + " }\n", + "\n", + " scripts.forEach( function (oldScript) {\n", + " var newScript = document.createElement(\"script\");\n", + " var attrs = [];\n", + " var nodemap = oldScript.attributes;\n", + " for (var j in nodemap) {\n", + " if (nodemap.hasOwnProperty(j)) {\n", + " attrs.push(nodemap[j])\n", + " }\n", + " }\n", + " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", + " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", + " oldScript.parentNode.replaceChild(newScript, oldScript);\n", + " });\n", + " if (JS_MIME_TYPE in output.data) {\n", + " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", + " }\n", + " output_area._hv_plot_id = id;\n", + " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", + " window.PyViz.plot_index[id] = Bokeh.index[id];\n", + " } else {\n", + " window.PyViz.plot_index[id] = null;\n", + " }\n", + " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", + " var bk_div = document.createElement(\"div\");\n", + " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var script_attrs = bk_div.children[0].attributes;\n", + " for (var i = 0; i < script_attrs.length; i++) {\n", + " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", + " }\n", + " // store reference to server id on output_area\n", + " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle when an output is cleared or removed\n", + " */\n", + "function handle_clear_output(event, handle) {\n", + " var id = handle.cell.output_area._hv_plot_id;\n", + " var server_id = handle.cell.output_area._bokeh_server_id;\n", + " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", + " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", + " if (server_id !== null) {\n", + " comm.send({event_type: 'server_delete', 'id': server_id});\n", + " return;\n", + " } else if (comm !== null) {\n", + " comm.send({event_type: 'delete', 'id': id});\n", + " }\n", + " delete PyViz.plot_index[id];\n", + " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", + " var doc = window.Bokeh.index[id].model.document\n", + " doc.clear();\n", + " const i = window.Bokeh.documents.indexOf(doc);\n", + " if (i > -1) {\n", + " window.Bokeh.documents.splice(i, 1);\n", + " }\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle kernel restart event\n", + " */\n", + "function handle_kernel_cleanup(event, handle) {\n", + " delete PyViz.comms[\"hv-extension-comm\"];\n", + " window.PyViz.plot_index = {}\n", + "}\n", + "\n", + "/**\n", + " * Handle update_display_data messages\n", + " */\n", + "function handle_update_output(event, handle) {\n", + " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", + " handle_add_output(event, handle)\n", + "}\n", + "\n", + "function register_renderer(events, OutputArea) {\n", + " function append_mime(data, metadata, element) {\n", + " // create a DOM node to render to\n", + " var toinsert = this.create_output_subarea(\n", + " metadata,\n", + " CLASS_NAME,\n", + " EXEC_MIME_TYPE\n", + " );\n", + " this.keyboard_manager.register_events(toinsert);\n", + " // Render to node\n", + " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", + " render(props, toinsert[0]);\n", + " element.append(toinsert);\n", + " return toinsert\n", + " }\n", + "\n", + " events.on('output_added.OutputArea', handle_add_output);\n", + " events.on('output_updated.OutputArea', handle_update_output);\n", + " events.on('clear_output.CodeCell', handle_clear_output);\n", + " events.on('delete.Cell', handle_clear_output);\n", + " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", + "\n", + " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", + " safe: true,\n", + " index: 0\n", + " });\n", + "}\n", + "\n", + "if (window.Jupyter !== undefined) {\n", + " try {\n", + " var events = require('base/js/events');\n", + " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", + " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", + " register_renderer(events, OutputArea);\n", + " }\n", + " } catch(err) {\n", + " }\n", + "}\n" + ], + "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" }, "metadata": {}, "output_type": "display_data" @@ -83,12 +578,12 @@ "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ - "
\n", - "
\n", + "
\n", + "
\n", "
\n", "