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 .
diff --git a/README.md b/README.md
index a824a6b..b55af17 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://encomp.readthedocs.io)
[](https://github.com/wlaur/encomp/blob/main/LICENSE)
-
+
> General-purpose library for *en*gineering *comp*utations.
@@ -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.
@@ -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.
diff --git a/encomp/constants.py b/encomp/constants.py
index 70b066f..196c603 100644
--- a/encomp/constants.py
+++ b/encomp/constants.py
@@ -55,3 +55,5 @@ def standard_conditions_temperature(self) -> Quantity[Temperature, float]:
CONSTANTS = Constants()
+"""Singleton :class:`Constants` instance; every attribute access returns a fresh
+:class:`~encomp.units.Quantity`."""
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/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.
diff --git a/encomp/coolprop/__init__.py b/encomp/coolprop/__init__.py
index ec28e12..5c2606b 100644
--- a/encomp/coolprop/__init__.py
+++ b/encomp/coolprop/__init__.py
@@ -92,6 +92,7 @@
# CoolProp AbstractState backends (HEOS is the general-purpose mixture EOS; IF97 is water/steam).
Backend = Literal["HEOS", "IF97", "REFPROP", "SRK", "PR", "PCSAFT", "VTPR", "INCOMP", "BICUBIC&HEOS", "TTSE&HEOS"]
BACKENDS: frozenset[Backend] = frozenset(get_args(Backend))
+"""Runtime set of the CoolProp ``AbstractState`` backend names in :data:`Backend`."""
# A few common CoolProp fluid names surfaced as Literals so editors can suggest them;
# ANY CoolProp name string is still accepted (the union widens to ``str``). The name may
@@ -131,6 +132,7 @@
"phase_critical_point",
]
PHASES: frozenset[Phase] = frozenset(get_args(Phase))
+"""Runtime set of the low-level CoolProp ``phase_*`` strings in :data:`Phase`."""
# User-facing assumed-phase names (matches encomp.fluids.Fluid.assume_phase). Each maps
# to the CoolProp ``phase_*`` string (the ``Phase`` literal) via ``_phase_from_assumed``;
@@ -144,13 +146,15 @@
"twophase",
]
ASSUMED_PHASES: frozenset[AssumedPhase] = frozenset(get_args(AssumedPhase))
+"""Runtime set of the user-facing assumed-phase names in :data:`AssumedPhase`."""
-# backends that ignore AbstractState.specify_phase because they are region-explicit: assuming a
-# phase has no effect there. Mirrors encomp.fluids._PHASE_IGNORING_BACKENDS.
PHASE_IGNORING_BACKENDS: frozenset[str] = frozenset({"IF97"})
+"""Region-explicit backends that ignore ``AbstractState.specify_phase``: assuming a phase has
+no effect there, so :func:`fluid` logs a warning when ``assume_phase`` names one of these.
+Mirrors the set :class:`encomp.fluids.Fluid` uses."""
-# the fluid name behind encomp.fluids.Water, and the one `water()` evaluates
WATER_NAME: CommonFluidName = "IF97::Water"
+"""The fluid name behind :class:`encomp.fluids.Water`, and the one :func:`water` evaluates."""
# CoolProp fluid parameters (outputs / the two state-input names): canonical names
# plus the common single-letter aliases.
@@ -296,6 +300,7 @@
"viscosity",
]
FLUID_PARAMS: frozenset[FluidParam] = frozenset(get_args(FluidParam))
+"""Runtime set of the CoolProp fluid parameter names in :data:`FluidParam`."""
# CoolProp fluid STATE-INPUT properties: the subset of FluidParam valid as the two
# inputs that fix a state -- pressure/temperature/quality + density/enthalpy/entropy/
@@ -309,6 +314,8 @@
"U", "UMASS", "UMOLAR", "Umass", "Umolar",
] # fmt: skip
FLUID_INPUTS: frozenset[FluidInput] = frozenset(get_args(FluidInput))
+"""Runtime set of the fluid state-input names in :data:`FluidInput` -- the subset of
+:data:`FluidParam` that can fix a state."""
# Common HumidAir (HAPropsSI) parameters.
HumidAirParam = Literal[
@@ -360,6 +367,7 @@
"psi_w",
]
HUMID_AIR_PARAMS: frozenset[HumidAirParam] = frozenset(get_args(HumidAirParam))
+"""Runtime set of the ``HAPropsSI`` parameter names in :data:`HumidAirParam`."""
# HumidAir (HAPropsSI) STATE-INPUT properties: the subset valid as the three inputs.
HumidAirInput = Literal[
@@ -369,6 +377,8 @@
"S", "Sda", "Sha", "Entropy", "V", "Vda", "Vha", "P", "P_w", "Y",
] # fmt: skip
HUMID_AIR_INPUTS: frozenset[HumidAirInput] = frozenset(get_args(HumidAirInput))
+"""Runtime set of the ``HAPropsSI`` state-input names in :data:`HumidAirInput` -- the subset
+of :data:`HumidAirParam` that can fix a state."""
_HUMID_AIR_INPUT_ALIASES: tuple[tuple[HumidAirInput, ...], ...] = (
("T", "Tdb", "T_db"),
diff --git a/encomp/fluids.py b/encomp/fluids.py
index 06ced2c..c403b0e 100644
--- a/encomp/fluids.py
+++ b/encomp/fluids.py
@@ -203,13 +203,13 @@ class HumidAirState(TypedDict, Generic[MT], total=False): # noqa: UP046
# phase has no effect and should not switch evaluation to the slower low-level loop
_PHASE_IGNORING_BACKENDS = frozenset({"IF97"})
-# Eager numpy / pl.Series inputs with at least this many elements are evaluated
-# through the GIL-free rust plugin (faster and lower peak memory than CoolProp's
-# vectorized PropsSI, and much faster than the low-level per-row Python loop for an
-# assumed phase / composition). Scalars and smaller arrays stay on the Python
-# CoolProp path, where the plugin's fixed dispatch overhead would dominate. The two
-# paths are verified to agree on dtype, value, and NaN/inf handling (test_fluids).
EAGER_PLUGIN_MIN_SIZE = 1000
+"""Eager numpy / ``pl.Series`` inputs with at least this many elements are evaluated
+through the GIL-free rust plugin (faster and lower peak memory than CoolProp's
+vectorized ``PropsSI``, and much faster than the low-level per-row Python loop for an
+assumed phase / composition). Scalars and smaller arrays stay on the Python CoolProp
+path, where the plugin's fixed dispatch overhead would dominate. The two paths are
+verified to agree on dtype, value, and NaN/inf handling (``test_fluids``)."""
# Caches the CONSTRUCTED pl.Expr per (fluid config, output, input-expr digests), so
# repeated evaluations of the same property return the IDENTICAL expression object.
diff --git a/encomp/settings.py b/encomp/settings.py
index c8b1fc3..2de7538 100644
--- a/encomp/settings.py
+++ b/encomp/settings.py
@@ -84,7 +84,7 @@ class Settings(BaseSettings):
)
-# The settings object is initialized the first time the library loads. Some
-# consumers read values during their own import/initialization, so do not treat
-# attribute assignment on this instance as a general runtime configuration API.
SETTINGS = Settings()
+"""Singleton :class:`Settings` instance, initialized the first time the library loads.
+Some consumers read values during their own import/initialization, so attribute
+assignment on this instance is not a general runtime-configuration API."""
diff --git a/encomp/tests/test_comparisons.py b/encomp/tests/test_comparisons.py
index 3db1335..b3642c8 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),
]
@@ -258,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
@@ -284,6 +316,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/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)
diff --git a/encomp/units.py b/encomp/units.py
index e95ffe3..096e57c 100644
--- a/encomp/units.py
+++ b/encomp/units.py
@@ -256,12 +256,14 @@ class DimensionalityRedefinitionError(ValueError):
"""Raised when defining a dimensionality that already exists."""
-# keep track of user-created dimensions
# NOTE: make sure to list all that are defined in defs/units.txt ("# custom dimensions")
CUSTOM_DIMENSIONS: list[str] = [
"currency",
"normal",
]
+"""Names of the registered custom base dimensions. The entries defined out of the box
+match the custom-dimension section of ``defs/units.txt``;
+:func:`define_dimensionality` appends every dimensionality it defines."""
_REGISTRY_STATIC_OPTIONS = {
@@ -312,6 +314,8 @@ def __init(self) -> None: # pyright: ignore[reportUnusedFunction] # invoked by
self._after_init()
+# NOTE: no attribute docstring here: autodoc's signature formatter fails on the
+# UnitRegistry[Any] value annotation, which would break the -W docs build
UNIT_REGISTRY = cast(UnitRegistry[Any], _LazyRegistry())
for _option_name, _option_value in _REGISTRY_STATIC_OPTIONS.items():
@@ -467,9 +471,12 @@ class Quantity(
Unit("delta_degRe")._units,
)
- # Tolerance for == / != and the ordering operators, applied identically to every magnitude
- # type: two values are equal when |a - b| <= max(rtol * max(|a|, |b|), atol). This is the
- # math.isclose / polars is_close predicate; see the module-level _is_close.
+ # Tolerance for == / != and the ordering operators, applied identically to every
+ # magnitude type: two values are equal when |a - b| <= max(rtol * max(|a|, |b|), atol).
+ # This is the math.isclose / polars is_close predicate; see the module-level _is_close.
+ # NOTE: no comment line here may begin with "# type:" -- sphinx's source analyzer
+ # parses that as a PEP 484 type comment, and a failure there drops the attribute
+ # docstrings of the WHOLE module from the rendered API reference.
rtol: float = 1e-9
atol: float = 1e-12
@@ -510,9 +517,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
@@ -620,25 +627,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:
@@ -2507,20 +2523,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))
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": [
- "