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 @@ [![Documentation](https://readthedocs.org/projects/encomp/badge/?version=latest)](https://encomp.readthedocs.io) [![License](https://img.shields.io/pypi/l/encomp.svg)](https://github.com/wlaur/encomp/blob/main/LICENSE) -encomp logo +encomp logo > 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": [ - "
\n", - "
\n", + "
\n", + "
\n", "
\n", "