diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml index b8edabe4..2c97a4ef 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -69,10 +69,6 @@ jobs: working-directory: easyscience run: pixi run python -m pytest ../tests/unit/ --color=yes -v - - name: Run functional tests to verify the installation - working-directory: easyscience - run: pixi run python -m pytest ../tests/functional/ --color=yes -v - - name: Run integration tests to verify the installation working-directory: easyscience run: pixi run python -m pytest ../tests/integration/ --color=yes -n auto diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 776ed94f..b04d87bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -102,21 +102,6 @@ jobs: pixi run --environment $env unit-tests done - - name: Run functional tests - shell: bash - run: | - set -euo pipefail - - for py_ver in $PY_VERSIONS; do - echo - echo "πŸ”ΉπŸ”ΈπŸ”ΉπŸ”ΈπŸ”Ή Python: $py_ver πŸ”ΉπŸ”ΈπŸ”ΉπŸ”ΈπŸ”Ή" - - env="py-$(echo $py_ver | tr -d .)-env" # Converts 3.XX -> py-3XX-env - - echo "Running tests in environment: $env" - pixi run --environment $env functional-tests - done - - name: Run integration tests ${{ needs.env-prepare.outputs.pytest-marks }} shell: bash run: | @@ -258,25 +243,6 @@ jobs: cd .. done - - name: Run functional tests - shell: bash - run: | - set -euo pipefail - - for py_ver in $PY_VERSIONS; do - echo - echo "πŸ”ΉπŸ”ΈπŸ”ΉπŸ”ΈπŸ”Ή Python: $py_ver πŸ”ΉπŸ”ΈπŸ”ΉπŸ”ΈπŸ”Ή" - - echo "Entering pixi project directory easyscience_py$py_ver" - cd easyscience_py$py_ver - - echo "Running tests" - pixi run python -m pytest ../tests/functional/ --color=yes -v - - echo "Exiting pixi project directory" - cd .. - done - - name: Run integration tests ${{ needs.env-prepare.outputs.pytest-marks }} shell: bash run: | diff --git a/docs/docs/api-reference/index.md b/docs/docs/api-reference/index.md index 4bd6f561..b6767ccf 100644 --- a/docs/docs/api-reference/index.md +++ b/docs/docs/api-reference/index.md @@ -8,19 +8,16 @@ This section contains the reference detailing the functions and modules available in EasyScience. - [base_classes](base_classes.md) – Core abstract and helper base - classes used to build EasyScience objects (e.g. `ObjBase`, - `ModelBase`). -- [fitting](fitting.md) – Fitting utilities and interfaces, including - `Fitter` and available minimizers. + classes used to build EasyScience objects (e.g. `EasyBase`, + `ModelBase`, `EasyList`, `SerializerBase`). +- [fitting](fitting.md) – Fitting and sampling, including `Fitter`, + `Sampler` and the available minimizers. - [global_object](global_object.md) – Global singleton providing shared - services (logger, map, undo/redo stack, script manager). -- [io](io.md) – Serialization and deserialization framework for - persisting EasyScience objects (serializers and components). + services (logger and object registry). - [job](job.md) – Job and experiment abstractions for running and organizing analyses. -- [models](models.md) – Predefined model implementations (e.g. - polynomial models) used in fitting workflows. - [utils](utils.md) – Miscellaneous utility functions and helpers (class tools, decorators, type helpers). - [variable](variable.md) – Descriptor types and variable abstractions - (e.g. `DescriptorNumber`, `Parameter`, `DescriptorArray`). + (e.g. `DescriptorNumber`, `Parameter`, `DescriptorArray`). All of them + are `EasyBase` objects, serialized with `to_dict`/`from_dict`. diff --git a/docs/docs/api-reference/io.md b/docs/docs/api-reference/io.md deleted file mode 100644 index f06a9356..00000000 --- a/docs/docs/api-reference/io.md +++ /dev/null @@ -1 +0,0 @@ -::: easyscience.io diff --git a/docs/docs/api-reference/models.md b/docs/docs/api-reference/models.md deleted file mode 100644 index 2471f604..00000000 --- a/docs/docs/api-reference/models.md +++ /dev/null @@ -1 +0,0 @@ -::: easyscience.models diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb index 302800c2..a2931634 100644 --- a/docs/docs/tutorials/fitting-bayesian.ipynb +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -28,7 +28,7 @@ "\n", "where $\\theta$ are the model parameters, $d$ is the observed data, $p(d \\mid \\theta)$ is the likelihood, and $p(\\theta)$ is the prior. In `easyscience`, the `min`/`max` bounds of a `Parameter` are interpreted as a **uniform prior**, and a Gaussian likelihood is constructed from the data and supplied weights.\n", "\n", - "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Sampler` class. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", + "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Sampler` class.\n", "\n", "```{note}\n", "This tutorial focuses on Bayesian analysis with a simple QENS model for illustration. For dedicated QENS fitting with more sophisticated models, consider using [`EasyDynamics`](https://github.com/easyscience/easydynamics).\n", @@ -138,12 +138,12 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "7", "metadata": {}, "source": [ "## Defining parameters with priors\n", "\n", - "Create four `Parameter` objects, for the area $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n", + "Create four `Parameter` objects, for the area $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*. The parameters are then collected in an `EasyList` container: this is the model object that both `Fitter` and `Sampler` take, so it is defined here, before the optional fit.\n", "\n", "| Parameter | Initial Value | Min | Max |\n", "| --- | --- | --- | --- |\n", @@ -156,21 +156,24 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "8", "metadata": {}, "outputs": [], "source": [ + "from easyscience import EasyList\n", "from easyscience import Parameter\n", "\n", - "area = Parameter(name='area', value=10, fixed=False, min=1, max=100)\n", - "gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n", - "omega_0 = Parameter(name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n", - "sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)" + "area = Parameter(display_name='area', value=10, fixed=False, min=1, max=100)\n", + "gamma = Parameter(display_name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n", + "omega_0 = Parameter(display_name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n", + "sigma = Parameter(display_name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)\n", + "\n", + "parameter_container = EasyList(area, gamma, omega_0, sigma)" ] }, { "cell_type": "markdown", - "id": "77ce3f63", + "id": "9", "metadata": {}, "source": [ "## The model\n", @@ -193,7 +196,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36a0c4f4", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -217,9 +220,7 @@ "source": [ "## Maximum-likelihood fit (optional, but recommended)\n", "\n", - "Perform a quick maximum-likelihood fit. This is **not** a prerequisite for sampling: `Sampler`\n", - "needs a *configured* `Fitter`, not a *fitted* one, and you can sample straight from the initial\n", - "parameter values.\n", + "Perform a quick maximum-likelihood fit.\n", "\n", "It is worth doing anyway, for two reasons:\n", "\n", @@ -239,9 +240,6 @@ "outputs": [], "source": [ "from easyscience import Fitter\n", - "from easyscience import ObjBase\n", - "\n", - "parameter_container = ObjBase(name='params', A=area, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", "\n", "mle_fitter = Fitter(parameter_container, intensity_model)\n", "mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / intensity_error)\n", @@ -261,7 +259,7 @@ "\n", "We now draw samples from the posterior distribution $p(\\theta \\mid d)$ using the BUMPS DREAM (DiffeRential Evolution Adaptive Metropolis) algorithm. DREAM is an ensemble MCMC method that runs multiple chains in parallel and automatically tunes the proposal distribution.\n", "\n", - "DREAM only works with the BUMPS minimizer. We reuse the ``mle_fitter`` created above β€” any configured `Fitter` would do, and it does not have to have been fitted β€” switch it to BUMPS, and create a `Sampler` instance bound to the fitter and data. Calling `sampler.sample()` returns a `SamplingResults` object with the following attributes:\n", + "Create a `Sampler` from the same `parameter_container` and `intensity_model` we gave the `Fitter`, bound to the data. Calling `sampler.sample()` returns a `SamplingResults` object with the following attributes:\n", "\n", "- `draws`: a `(n_samples, n_parameters)` array of posterior samples: each **row** is one complete draw from the joint posterior (one value for every parameter simultaneously), and each **column** holds all sampled values for a single parameter. Note this is a *trimmed* view of the chain rather than the raw buffer, so `n_samples` is smaller than `samples / thin` — see the note under [Extend the chain](#extend-the-chain-and-check-convergence);\n", "- `param_names`: the unique names of the parameters, in the same column order as `draws`;\n", @@ -274,7 +272,9 @@ "- `burn` (500): the number of initial *burn-in* generations to discard — the sampler needs time to find the typical set of the posterior, and early samples are not representative. Note this counts generations, not raw samples, so `burn=500` discards `500 Γ— n_chains` raw samples;\n", "- `thin` (2): the *thinning* interval — only every second generation is kept, which reduces autocorrelation between consecutive draws;\n", "\n", - "First, we switch to the BUMPS minimizer:" + "```{note}\n", + "If you already have a `Fitter`, `Sampler.from_fitter(mle_fitter, omega, intensity_obs, weights=1 / intensity_error)` builds the same sampler from the model bound to it. The fitter is neither needed afterwards nor modified.\n", + "```" ] }, { @@ -283,22 +283,12 @@ "id": "14", "metadata": {}, "outputs": [], - "source": [ - "from easyscience import AvailableMinimizers\n", - "\n", - "mle_fitter.switch_minimizer(AvailableMinimizers.Bumps)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5a219fcd", - "metadata": {}, - "outputs": [], "source": [ "from easyscience.fitting import Sampler\n", "\n", - "sampler = Sampler(mle_fitter, omega, intensity_obs, weights=1 / intensity_error)\n", + "sampler = Sampler(\n", + " parameter_container, intensity_model, omega, intensity_obs, weights=1 / intensity_error\n", + ")\n", "results = sampler.sample(samples=10000, burn=500, thin=2)\n", "\n", "print(f'Drew {results.draws.shape[0]} samples for {results.draws.shape[1]} parameters.')\n", @@ -307,7 +297,7 @@ }, { "cell_type": "markdown", - "id": "8766b170", + "id": "15", "metadata": {}, "source": [ "## Convergence diagnostics\n", @@ -323,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3c49ab6f", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -359,7 +349,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "17", "metadata": {}, "source": [ "## Posterior summaries\n", @@ -377,7 +367,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ce3e38a8", + "id": "18", "metadata": {}, "outputs": [], "source": [] @@ -385,7 +375,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -407,7 +397,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "20", "metadata": {}, "source": [ "## Visualise the joint posterior\n", @@ -418,7 +408,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -452,7 +442,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "22", "metadata": {}, "source": [ "## Posterior-predictive band\n", @@ -463,7 +453,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -499,7 +489,7 @@ }, { "cell_type": "markdown", - "id": "03339658", + "id": "24", "metadata": {}, "source": [ "## Extend the chain and check convergence\n", @@ -543,7 +533,7 @@ { "cell_type": "code", "execution_count": null, - "id": "293b140b", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -573,7 +563,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9ec4302c", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -614,7 +604,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b0f30be6", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -641,7 +631,7 @@ }, { "cell_type": "markdown", - "id": "50b7213a", + "id": "28", "metadata": {}, "source": [ "### What is Gelman-Rubin R-hat?\n", @@ -675,7 +665,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3449e0a7", + "id": "29", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/fitting-qens.ipynb b/docs/docs/tutorials/fitting-qens.ipynb index a4929a5c..3809461e 100644 --- a/docs/docs/tutorials/fitting-qens.ipynb +++ b/docs/docs/tutorials/fitting-qens.ipynb @@ -246,10 +246,10 @@ "source": [ "from easyscience import Parameter\n", "\n", - "A = Parameter(name='A', value=10, fixed=False, min=0.01, max=100)\n", - "gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=10e-3)\n", - "omega_0 = Parameter(name='omega_0', value=0.001, fixed=False, min=0, max=0.002)\n", - "sigma = Parameter(name='sigma', value=0.001, fixed=False, min=1e-5, max=0.1)" + "A = Parameter(display_name='A', value=10, fixed=False, min=0.01, max=100)\n", + "gamma = Parameter(display_name='gamma', value=8e-3, fixed=False, min=1e-4, max=10e-3)\n", + "omega_0 = Parameter(display_name='omega_0', value=0.001, fixed=False, min=0, max=0.002)\n", + "sigma = Parameter(display_name='sigma', value=0.001, fixed=False, min=1e-5, max=0.1)" ] }, { @@ -293,10 +293,10 @@ "metadata": {}, "outputs": [], "source": [ + "from easyscience import EasyList\n", "from easyscience import Fitter\n", - "from easyscience import ObjBase\n", "\n", - "params = ObjBase(name='params', A=A, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", + "params = EasyList(A, gamma, omega_0, sigma)\n", "f = Fitter(params, intensity)\n", "\n", "res = f.fit(x=q, y=i, weights=1 / di)" diff --git a/docs/docs/tutorials/fitting-sans.ipynb b/docs/docs/tutorials/fitting-sans.ipynb index 06b0cc36..84c3a1cd 100644 --- a/docs/docs/tutorials/fitting-sans.ipynb +++ b/docs/docs/tutorials/fitting-sans.ipynb @@ -236,10 +236,10 @@ "source": [ "from easyscience import Parameter\n", "\n", - "scale = Parameter(name='scale', value=1.4e-7, fixed=True)\n", - "delta_rho = Parameter(name='delta_rho', value=3, fixed=False, min=0, max=10)\n", - "r = Parameter(name='r', value=80, fixed=False, min=0, max=1000)\n", - "bkg = Parameter(name='bkg', value=0.01, fixed=False, min=0.001, max=0.1)" + "scale = Parameter(display_name='scale', value=1.4e-7, fixed=True)\n", + "delta_rho = Parameter(display_name='delta_rho', value=3, fixed=False, min=0, max=10)\n", + "r = Parameter(display_name='r', value=80, fixed=False, min=0, max=1000)\n", + "bkg = Parameter(display_name='bkg', value=0.01, fixed=False, min=0.001, max=0.1)" ] }, { @@ -283,10 +283,10 @@ "metadata": {}, "outputs": [], "source": [ + "from easyscience import EasyList\n", "from easyscience import Fitter\n", - "from easyscience import ObjBase\n", "\n", - "params = ObjBase(name='params', delta_rho=delta_rho, r=r, bkg=bkg)\n", + "params = EasyList(delta_rho, r, bkg)\n", "f = Fitter(params, sphere)\n", "\n", "res = f.fit(x=q, y=i, weights=1 / di)" diff --git a/docs/docs/tutorials/progress-callback.ipynb b/docs/docs/tutorials/progress-callback.ipynb index a5625a5a..f782f1b9 100644 --- a/docs/docs/tutorials/progress-callback.ipynb +++ b/docs/docs/tutorials/progress-callback.ipynb @@ -47,9 +47,10 @@ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", - "from easyscience import AvailableMinimizers\n", + "from easyscience import EasyList\n", "from easyscience import Fitter\n", - "from easyscience import ObjBase\n", + "from easyscience import Minimizers\n", + "from easyscience import ModelBase\n", "from easyscience import Parameter" ] }, @@ -82,32 +83,23 @@ }, "outputs": [], "source": [ - "class MultiPeak(ObjBase):\n", - " a0: Parameter\n", - " a1: Parameter\n", - " a2: Parameter\n", - " A1: Parameter\n", - " mu1: Parameter\n", - " sigma1: Parameter\n", - " A2: Parameter\n", - " mu2: Parameter\n", - " sigma2: Parameter\n", - " A3: Parameter\n", - " mu3: Parameter\n", - " sigma3: Parameter\n", - "\n", + "class MultiPeak(ModelBase):\n", " def __init__(self, **values):\n", - " params = {name: Parameter(name, value) for name, value in values.items()}\n", - " super().__init__('multipeak', **params)\n", + " super().__init__(display_name='multipeak')\n", + " self._parameters = EasyList(\n", + " *(Parameter(value, display_name=name) for name, value in values.items())\n", + " )\n", + "\n", + " @property\n", + " def parameters(self) -> EasyList:\n", + " return self._parameters\n", "\n", " def __call__(self, x):\n", - " bg = self.a0.value + self.a1.value * x + self.a2.value * x**2\n", + " p = {par.display_name: par.value for par in self.parameters}\n", + " bg = p['a0'] + p['a1'] * x + p['a2'] * x**2\n", " peaks = 0.0\n", - " for A, mu, sigma in (\n", - " (self.A1.value, self.mu1.value, self.sigma1.value),\n", - " (self.A2.value, self.mu2.value, self.sigma2.value),\n", - " (self.A3.value, self.mu3.value, self.sigma3.value),\n", - " ):\n", + " for i in (1, 2, 3):\n", + " A, mu, sigma = p[f'A{i}'], p[f'mu{i}'], p[f'sigma{i}']\n", " peaks = peaks + A * np.exp(-0.5 * ((x - mu) / sigma) ** 2)\n", " return bg + peaks" ] @@ -205,7 +197,7 @@ " mu3=3.0,\n", " sigma3=1.2,\n", " )\n", - " for p in model.get_parameters():\n", + " for p in model.get_all_parameters():\n", " p.fixed = False\n", " return model" ] @@ -272,7 +264,7 @@ }, "outputs": [], "source": [ - "def fit_with(backend: AvailableMinimizers):\n", + "def fit_with(backend: Minimizers):\n", " model = make_initial_model()\n", " fitter = Fitter(model, model)\n", " fitter.switch_minimizer(backend)\n", @@ -285,7 +277,7 @@ "\n", "results = {}\n", "histories = {}\n", - "for backend in (AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO):\n", + "for backend in (Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO):\n", " res, hist = fit_with(backend)\n", " results[backend.name] = res\n", " histories[backend.name] = hist\n", @@ -378,7 +370,7 @@ "else:\n", " model = make_initial_model()\n", " fitter = Fitter(model, model)\n", - " fitter.switch_minimizer(AvailableMinimizers.LMFit)\n", + " fitter.switch_minimizer(Minimizers.LMFit)\n", " fitter.max_evaluations = 108\n", "\n", " pbar = tqdm(total=fitter.max_evaluations, desc='LMFit', unit='eval')\n", diff --git a/docs/docs/user-guide/controlling-log-output.md b/docs/docs/user-guide/controlling-log-output.md index 0403590a..12899e1e 100644 --- a/docs/docs/user-guide/controlling-log-output.md +++ b/docs/docs/user-guide/controlling-log-output.md @@ -35,14 +35,13 @@ individual subsystems: ``` easyscience # root β€” controls everything β”œβ”€β”€ easyscience.base_classes # EasyList runtime warnings -β”œβ”€β”€ easyscience.legacy # ObjBase / CollectionBase deprecations β”œβ”€β”€ easyscience.deprecated # @deprecated decorator messages β”œβ”€β”€ easyscience.fitting # import-availability warnings β”‚ β”œβ”€β”€ easyscience.fitting.bumps # Bumps fitting runtime messages β”‚ β”œβ”€β”€ easyscience.fitting.lmfit # LMFit fitting runtime messages β”‚ └── easyscience.fitting.dfo # DFO fitting runtime messages β”œβ”€β”€ easyscience.variable # parameter warnings -└── easyscience.global_object # undo/redo, debugging diagnostics +└── easyscience.global_object # debugging diagnostics ``` Setting the level on a parent logger affects all its children, because @@ -56,9 +55,6 @@ logging.getLogger('easyscience.fitting').setLevel(logging.ERROR) # Suppress only Bumps runtime messages logging.getLogger('easyscience.fitting.bumps').setLevel(logging.ERROR) - -# Suppress only legacy deprecation notices -logging.getLogger('easyscience.legacy').setLevel(logging.ERROR) ``` ## Environment Variable diff --git a/docs/docs/user-guide/external-calculators.md b/docs/docs/user-guide/external-calculators.md new file mode 100644 index 00000000..9b3c4f9f --- /dev/null +++ b/docs/docs/user-guide/external-calculators.md @@ -0,0 +1,332 @@ +# Connecting an External Calculator + +Technique libraries built on EasyScience usually compute their model +with an external package (for example refnx or refl1d for +reflectometry). This page describes how such a calculator connects to +the EasyScience model and to `Fitter`. + +EasyScience does not provide a calculator base class, a factory or a +registry. A calculator is any object your library defines; the only +thing EasyScience sees is the fit function passed to `Fitter`. + +## General overview + +```mermaid +flowchart LR + subgraph lib["Technique library"] + calc["Calculator
backend choice and configuration"] + func["Fit function
calculator.fit_func_for(model)"] + backend["Backend package
e.g. refnx, refl1d"] + end + subgraph core["EasyScience core"] + fitter["Fitter / MultiFitter"] + minimizer["Minimizer
LMFit, Bumps, DFO"] + model["Model (ModelBase)
Parameters: the only state"] + end + + calc -- "creates" --> func + func -. "handed over once:
Fitter(model, fit_function)" .-> fitter + fitter -- "get_free_parameters()" --> model + minimizer -- "trial values" --> fitter + fitter -- "1: sets Parameter.value" --> model + fitter -- "2: calls fit_function(x)" --> func + func -- "3: reads current values" --> model + func -- "4: builds and evaluates
with plain numbers" --> backend + backend -- "5: y" --> func +``` + +The arrows between the library and the model only point one way: the fit +function reads parameters, it never writes them. `Fitter` is the only +common point between core and the library, and the only thing passed +across is a callable. + +## The Contract + +- **EasyScience is the only store of parameter state.** Values, + uncertainties, bounds, fixed flags and dependencies live in the + `Parameter` objects of the model. The calculator does not keep its own + copy. +- **The calculator reads the model when asked to calculate.** Each + evaluation uses the current values of the independent parameters and + of the dependent parameters derived from them. +- **Reading a parameter has no side effects.** `Parameter.value` returns + the stored value; it never calls into a calculator. +- **Evaluation does not change the model.** Calculating leaves values, + uncertainties, bounds, fixed flags and dependencies as they were. Only + the fitting engine writes parameter values during a fit, through the + ordinary `value` setter. +- **Structural edits are seen by the next evaluation.** Adding or + removing a part of the model needs no extra step, because the + calculator reads the model fresh. + +## A Minimal Example + +The calculator below evaluates a straight line. It holds no parameter +values; it reads them from the model on every call. + +```python +import numpy as np + +from easyscience import Parameter +from easyscience.base_classes import ModelBase +from easyscience.fitting import Fitter + + +class Line(ModelBase): + def __init__(self, slope: float, intercept: float): + super().__init__() + self._slope = Parameter(slope, display_name='slope') + self._intercept = Parameter(intercept, display_name='intercept') + + @property + def slope(self) -> Parameter: + return self._slope + + @property + def intercept(self) -> Parameter: + return self._intercept + + +class LineCalculator: + """Owned by the technique library, unknown to EasyScience.""" + + def calculate(self, model: Line, x: np.ndarray) -> np.ndarray: + # Hand plain numbers to the backend + return model.slope.value * x + model.intercept.value + + def fit_func_for(self, model: Line): + return lambda x: self.calculate(model, x) + + +model = Line(slope=1.0, intercept=0.0) +calculator = LineCalculator() + +x = np.linspace(0, 10, 50) +y = 3.0 * x + 1.5 + np.random.default_rng(0).normal(0.0, 0.01, x.size) + +fitter = Fitter(model, calculator.fit_func_for(model)) +result = fitter.fit(x=x, y=y, weights=np.ones_like(x)) + +print(model.slope.value, model.intercept.value) # close to 3.0 and 1.5 +``` + +During the fit, the engine sets the parameter values and then calls the +fit function, so the calculator always uses the values being tried. + +Dependent parameters need no special handling. If the intercept is made +dependent on the slope, the engine fits only the slope and the +calculator reads the recomputed intercept: + +```python +model.intercept.make_dependent_on( + dependency_expression='0.5 * s', + dependency_map={'s': model.slope}, +) +``` + +## Several Datasets + +With `MultiFitter`, pass one fit function per dataset. A `Parameter` +object used by several models is fitted once and every calculator call +will read the same value: + +```python +from easyscience.fitting.multi_fitter import MultiFitter + +fitter = MultiFitter( + [model_1, model_2], + [calculator.fit_func_for(model_1), calculator.fit_func_for(model_2)], +) +results = fitter.fit(x=[x_1, x_2], y=[y_1, y_2], weights=[w_1, w_2]) +``` + +## Performance + +Rebuilding the backend object on every evaluation is simple and, for +typical models, cheap. If profiling shows that construction dominates, +build the backend object once when a fit starts, refresh only the +numbers on each call, and discard it when the fit ends. Such a +fit-scoped session lives inside the fit function in your library. The +parameter set does not change during a fit, so the session does not have +to track structural edits. + +## Writing Values Back + +EasyScience has no API for a backend to write values into the model, and +ordinary evaluation should never need one. If your library has an +operation that genuinely changes the model based on a backend result, +perform it in your own code through the public `Parameter.value` setter, +outside the fit, as an explicit step the user can see. + +## Following a Fit Iteration by Iteration + +Because the calculator only reads the model, it is not the place to +watch a fit's progress. To see the parameter values at each iteration, +pass a `progress_callback` to `fit()`. Starting again from a fresh, +independent line (the dependent intercept from the previous section +would not be in the payload, see below): + +```python +model = Line(slope=1.0, intercept=0.0) +history = [] + + +def on_iteration(payload: dict) -> None: + # Called by the minimizer as the fit progresses. Values are keyed + # by unique_name and describe the point the minimizer reports. + values = payload['parameter_values'] + slope = values[model.slope.unique_name] + intercept = values[model.intercept.unique_name] + history.append((payload['iteration'], slope, intercept)) + print( + f'iteration {payload["iteration"]:3d}: ' + f'slope={slope:.6f} intercept={intercept:.6f} chi2={payload["chi2"]:.4g}' + ) + + +fitter = Fitter(model, calculator.fit_func_for(model)) +result = fitter.fit(x=x, y=y, weights=np.ones_like(x), progress_callback=on_iteration) +``` + +The callback receives a dictionary with these keys: + +| Key | Meaning | +| --------------------------- | ------------------------------------------------------------------------------- | +| `iteration` | Iteration or evaluation number, as counted by the minimizer | +| `chi2` | Chi-squared at the reported point | +| `reduced_chi2` | Chi-squared divided by the degrees of freedom | +| `parameter_values` | Values of the fitted parameters at the reported point, indexed by `unique_name` | +| `refresh_plots`, `finished` | Hints for user interfaces | + +`parameter_values` holds exactly the parameters returned by +`model.get_free_parameters()`: the free, independent ones. Fixed and +dependent parameters are not in it. To report a dependent value, read it +from the model or recompute it from the free values in the payload. + +Its return value is ignored. To stop a fit from the callback, see +[Stopping a Fit](#stopping-a-fit). + +When the callback is called, and which point it reports, depends on the +minimizer: + +| Minimizer | Called | Reported point | +| --------- | ---------------------------- | ------------------------------------------- | +| LMFit | on every function evaluation | the point just evaluated | +| Bumps | once per optimizer step | the optimizer's current point for that step | +| DFO | on every function evaluation | the best point found so far | + +With LMFit `iteration` is an evaluation counter, not a step index: it +starts at `-1`, the first few callbacks all describe the starting point, +the small steps of about 1e-5 are LMFit estimating the gradient +numerically, and the last number is reported twice. Compare the values +rather than the counter if you only want real steps. With DFO the +reported values change only when an evaluation improves on the best +point. + +If you need a hook on every evaluation rather than every iteration, put +it in the fit function your calculator returns: it is called each time +and reads the current values from the model. + +## Stopping a Fit + +EasyScience has no single cancel switch that works the same way on every +minimizer. The options and how each minimizer responds: + +| How | LMFit | Bumps | DFO | +| --------------------------------- | ------------------------- | ------------------------------------------------ | ----------------------------------- | +| Raise from `progress_callback` | stops, raises `FitError` | stops, raises `FitError` | stops, raises `FitError` | +| Raise from the fit function | stops, raises `FitError` | stops only after logging every failed evaluation | stops, raises `FitError` | +| `abort_test=` passed to `fit()` | ignored with a warning | stops, returns `success=False` | raises `FitError` (unknown keyword) | +| `fitter.max_evaluations` property | stops after N evaluations | stops after N optimizer steps | stops after N evaluations | + +### Raising from the progress callback + +This is the one approach that works on all three minimizers. Each +minimizer catches the exception, puts the fitted parameters back to the +values they had before the fit, and raises `FitError` with your +exception as its argument. + +A user interface can set a flag from its "Stop" button and let the +callback check it: + +```python +from easyscience.fitting.minimizers import FitError + + +class FitCancelled(Exception): + pass + + +stop_requested = False # set to True from the user interface + + +def on_iteration(payload: dict) -> None: + if stop_requested: + raise FitCancelled() + + +try: + result = fitter.fit(x=x, y=y, weights=np.ones_like(x), progress_callback=on_iteration) +except FitError: + # The fit was cancelled; the model is back at its pre-fit values. + ... +``` + +With LMFit and DFO the callback runs on every function evaluation, so +the fit stops at the next evaluation. With Bumps it runs once per +optimizer step, so the fit stops at the end of the current step. + +### Raising from the fit function + +Avoid using the fit function as a stop signal. LMFit and DFO stop as +they would for an exception from the callback, but Bumps catches the +error on every evaluation, logs a traceback, and carries on for many +failing evaluations before it gives up. + +### `abort_test` with Bumps + +Bumps, and the DREAM sampler, accept an `abort_test` argument: a +function without arguments that returns `True` when the fit should stop. +It is polled during the optimizer loop and once more after it finishes, +so it must have no side effects: + +```python +from easyscience import Minimizers + +fitter.switch_minimizer(Minimizers.Bumps) +result = fitter.fit( + x=x, y=y, weights=np.ones_like(x), abort_test=lambda: stop_requested +) +if not result.success: + print(result.message) # 'Fit aborted before convergence' +``` + +Unlike raising an exception, an aborted Bumps fit returns normally and +keeps the best values found so far in the model. Do not pass +`abort_test` to the other minimizers: LMFit ignores it and runs the fit +to the end, and DFO rejects it with a `FitError`. + +### Limiting the number of evaluations + +Set the budget on the `Fitter` before calling `fit()`. Do not pass +`max_evaluations=` (or `tolerance=`) as a keyword to `fit()`: the +`Fitter` already forwards its own value, and a second one raises +`TypeError: got multiple values for keyword argument` on every +minimizer. + +```python +fitter.max_evaluations = 100 +result = fitter.fit(x=x, y=y, weights=np.ones_like(x)) +if not result.success: + print(result.message) +``` + +The budget means different things to the minimizers. For LMFit and DFO +it is a limit on function evaluations. For Bumps it is forwarded as the +optimizer's `steps` setting, and one step can contain several +evaluations, so the calculator may be called many more times than the +number you set. The `iterations` reported in the result and in the +progress payload count the same quantity as the budget for each +minimizer, so they are not comparable across minimizers. It is a budget, +not a cancel button, and it does not bound elapsed time when an +evaluation is slow. diff --git a/docs/docs/user-guide/index.md b/docs/docs/user-guide/index.md index 84fe5c09..4c31eb8c 100644 --- a/docs/docs/user-guide/index.md +++ b/docs/docs/user-guide/index.md @@ -8,3 +8,10 @@ icon: material/book-open-variant – How to suppress, filter, or redirect the messages that EasyScience produces. Covers the `EASYSCIENCE_LOG_LEVEL` environment variable, the logger hierarchy, context managers, and common recipes. +- [:material-link-variant: Serializing Dependent Parameters](parameter-dependency-serialization.md) + – How to save and load parameters that depend on other parameters, and + how dependencies are resolved after loading. +- [:material-calculator-variant: Connecting an External Calculator](external-calculators.md) + – How a technique library connects its own calculator to an + EasyScience model and to `Fitter`, and what to change when migrating + from `InterfaceFactoryTemplate`. diff --git a/docs/docs/user-guide/parameter-dependency-serialization.md b/docs/docs/user-guide/parameter-dependency-serialization.md new file mode 100644 index 00000000..2a8254e8 --- /dev/null +++ b/docs/docs/user-guide/parameter-dependency-serialization.md @@ -0,0 +1,179 @@ +# Serializing Dependent Parameters + +A `Parameter` can depend on other parameters through an expression (see +`Parameter.make_dependent_on` and `Parameter.from_dependency`). This +page explains how such parameters are written to a dictionary or JSON +and how their dependencies are rebuilt when they are loaded. + +## Quick Start + +Serialize every parameter involved with `to_dict`: + +```python +import json + +from easyscience import Parameter + +a = Parameter(value=2.0, unit='m', min=0, max=10, display_name='a') +b = Parameter.from_dependency( + dependency_expression='2 * a', + dependency_map={'a': a}, + display_name='b', +) + +with open('parameters.json', 'w') as f: + json.dump({'a': a.to_dict(), 'b': b.to_dict()}, f, indent=2) +``` + +To load them, deserialize all parameters first. Then resolve the +dependencies in a single step: + +```python +import json + +from easyscience import Parameter +from easyscience.variable.parameter_dependency_resolver import ( + resolve_all_parameter_dependencies, +) + +with open('parameters.json') as f: + params_dict = json.load(f) + +new_a = Parameter.from_dict(params_dict['a']) +new_b = Parameter.from_dict(params_dict['b']) + +resolve_all_parameter_dependencies({'a': new_a, 'b': new_b}) + +new_a.value = 5.0 +print(new_b.value) # 10.0 +``` + +!!! warning "Always resolve after loading" + + `Parameter.from_dict` does **not** restore the dependency on its + own. Until `resolve_all_parameter_dependencies` (or + `Parameter.resolve_pending_dependencies`) is called, a loaded + dependent parameter behaves as an independent parameter holding the + value it had when it was saved. + +Alternatively, if all you have is a dictionary of serialized parameters, +the convenience helper `deserialize_and_resolve_parameters` combines +both steps: + +```python +from easyscience.variable.parameter_dependency_resolver import ( + deserialize_and_resolve_parameters, +) + +params = deserialize_and_resolve_parameters(params_dict) +params['a'].value = 3.0 +print(params['b'].value) # 6.0 +``` + +## What Is Written + +An independent parameter is written with its constructor arguments +(`value`, `unit`, `variance`, `min`, `max`, `fixed`, `description`, +`url`, and `display_name`/`unique_name` when they were set explicitly). +A dependent parameter gets the following additional fields: + +| Field | Meaning | +| -------------------------------- | ----------------------------------------------------------------------------------- | +| `_dependency_string` | The dependency expression, e.g. `"2 * a"`. | +| `_dependency_map_serializer_ids` | Maps each symbol in the expression to the serializer id of the object it refers to. | +| `_independent` | `false`, marking the parameter as dependent. | +| `_desired_unit` | Only present if a `desired_unit` was given when making the dependency. | + +Any parameter that something else depends on also writes its own +`__serializer_id`. This is a UUID assigned the first time another +parameter depends on it. It is what `_dependency_map_serializer_ids` +refers to: + +```json +{ + "@module": "easyscience.variable.parameter", + "@class": "Parameter", + "value": 4.0, + "unit": "m", + "display_name": "b", + "_dependency_string": "2 * a", + "_independent": false, + "_dependency_map_serializer_ids": { + "a": "a48db37b-519b-4bea-9873-aa3cc5b2ec76" + } +} +``` + +Serializer ids are independent of `unique_name`. A loaded parameter can +therefore receive a fresh unique name, avoiding a clash with objects +that already exist in the session, and its dependencies still find each +other. + +## How Loading Works + +1. `Parameter.from_dict` creates each parameter as an ordinary + independent parameter and restores its `__serializer_id`. +2. For dependent parameters, the expression, the serializer-id map and + the desired unit are kept on the object as pending dependency + information. +3. `resolve_all_parameter_dependencies(obj)` walks `obj` and finds every + parameter with pending information. `obj` can be a single + `Parameter`, a list, tuple or dict of them, or an object exposing + them as public attributes or properties. For each parameter it looks + up the referenced objects by serializer id among all objects alive in + the session and calls `make_dependent_on`. It then removes the + pending information. + +Because resolution happens only after everything is loaded, parameters +can be deserialized in **any order**. A dependent parameter may come +before the parameters it depends on. + +To check what still needs resolving, use +`get_parameters_with_pending_dependencies`: + +```python +from easyscience.variable.parameter_dependency_resolver import ( + get_parameters_with_pending_dependencies, +) + +pending = get_parameters_with_pending_dependencies({'a': new_a, 'b': new_b}) +print(f'{len(pending)} parameter(s) still need resolving') +``` + +## Errors + +`resolve_all_parameter_dependencies` tries every pending parameter and +then raises a single `ValueError` listing all failures. The typical +cause is a referenced parameter that was not loaded: + +```text +ValueError: Failed to resolve dependencies for 1 parameter(s): +Failed to resolve dependencies for parameter 'Parameter_0' (display_name: 'b', serializer_id: 'unknown'): Cannot find parameter with serializer_id 'f9040c2a-...' +``` + +An invalid expression, or one that would create a cyclic dependency, is +reported the same way, with the underlying error message appended. + +## Best Practices + +- **Serialize the whole dependency graph.** Every parameter referenced + by a dependency expression must be saved and loaded together with the + parameters that depend on it. +- **Load everything, then resolve once.** Deserialize all parameters + first and call `resolve_all_parameter_dependencies` on the collection + as a whole. +- **Keep the referenced objects alive.** Lookup goes through the global + object registry, so a parameter that has been garbage-collected cannot + be found. +- **Do not load the same data twice in one session.** Serializer ids are + restored verbatim. If the original parameters, or an earlier load of + the same file, are still alive, a dependency can be bound to that + older object instead of the one just loaded. + +!!! note "Parameters nested in models" + + Dependency information is written by `Parameter.to_dict`. Calling + `to_dict` on a `ModelBase` that *contains* dependent parameters + does not currently go through `Parameter.to_dict`, so the + dependency fields are not included. Until this is addressed, + serialize dependent parameters directly as shown above. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 91165ebc..905fa409 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -192,6 +192,9 @@ nav: - User Guide: - User Guide: user-guide/index.md - Controlling Log Output: user-guide/controlling-log-output.md + - Serializing Dependent Parameters: + user-guide/parameter-dependency-serialization.md + - Connecting an External Calculator: user-guide/external-calculators.md - Tutorials: - Tutorials: tutorials/index.md - Bayesian analysis: tutorials/fitting-bayesian.ipynb @@ -204,8 +207,6 @@ nav: - base_classes: api-reference/base_classes.md - fitting: api-reference/fitting.md - global_object: api-reference/global_object.md - - io: api-reference/io.md - job: api-reference/job.md - - models: api-reference/models.md - utils: api-reference/utils.md - variable: api-reference/variable.md diff --git a/legacy/Examples/README.rst b/legacy/Examples/README.rst deleted file mode 100644 index e2d0e47e..00000000 --- a/legacy/Examples/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -Example Galleries -================= - -These are a collection of examples covering aspects of the *EasyScience* project. \ No newline at end of file diff --git a/legacy/Examples/base/README.rst b/legacy/Examples/base/README.rst deleted file mode 100644 index 46da573a..00000000 --- a/legacy/Examples/base/README.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _base_examples: - -Subclassing Examples ------------------------- - -This section gathers examples which correspond to subclassing the :class:`easyscience.base_classes.ObjBase` class. diff --git a/legacy/Examples/base/plot_baseclass1.py b/legacy/Examples/base/plot_baseclass1.py deleted file mode 100644 index b87c559e..00000000 --- a/legacy/Examples/base/plot_baseclass1.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Subclassing ObjBase - Simple Pendulum -===================================== -This example shows how to subclass :class:`easyscience.base_classes.ObjBase` with parameters from -:class:`EasyScience.variable.Parameter`. For this example a simple pendulum will be modeled. - -.. math:: - y = A \sin (2 \pi f t + \phi ) - -Imports -******* - -Firstly the necessary imports. Notice that we import numpy from easyscience. This is not done for any reason other than -saving time from multiple imports. -""" - -import matplotlib.pyplot as plt -import numpy as np - -from easyscience.base_classes import ObjBase -from easyscience.variable import Parameter - -# %% -# Subclassing -# *********** -# To include embedded rST, use a line of >= 20 ``#``'s or ``#%%`` between your -# rST and your code. This separates your example -# into distinct text and code blocks. You can continue writing code below the -# embedded rST text block: - - -class Pendulum(ObjBase): - def __init__(self, A: Parameter, f: Parameter, p: Parameter): - super(Pendulum, self).__init__('SimplePendulum', A=A, f=f, p=p) - - @classmethod - def from_pars(cls, A: float = 1, f: float = 1, p: float = 0): - A = Parameter('Amplitude', A) - f = Parameter('Frequency', f) - p = Parameter('Phase', p) - return cls(A, f, p) - - def __call__(self, t): - return self.A.value * np.sin(2 * np.pi * self.f.value * t + self.p.value) - - def plot(self, time, axis=None, **kwargs): - if axis is None: - axis = plt - else: - axis.set_title(f'A={self.A.value}, F={self.f.value}, P={self.p.value}') - p = axis.plot(time, self(time), **kwargs) - return p - - -# %% -# Single Example -# ************** -# To include embedded rST, use a line of >= 20 ``#``'s or ``#%%`` between your -# rST and your code. This separates your example -# into distinct text and code blocks. You can continue writing code below the -# embedded rST text block: - -p1 = Pendulum.from_pars() -# Another pendulum with Amplitude = 5 -p2 = Pendulum.from_pars(A=5) -# Another pendulum with Frequency = 4 -p3 = Pendulum.from_pars(A=5, f=4) -# Another pendulum with Phase = pi/2 -p4 = Pendulum.from_pars(A=5, f=4, p=np.pi / 2) - -# %% -# Plotting - -t = np.linspace(0, 3, 601) -fig = plt.figure() -gs = fig.add_gridspec(2, 2) -(ax1, ax2), (ax3, ax4) = gs.subplots(sharex='col', sharey='row') -p1.plot(t, axis=ax1) -p2.plot(t, axis=ax2) -p3.plot(t, axis=ax3) -p4.plot(t, axis=ax4) -fig.show() - -# %% -# Multiple Examples -# ***************** -# To include embedded rST, use a line of >= 20 ``#``'s or ``#%%`` between your -# rST and your code. This separates your example -# into distinct text and code blocks. You can continue writing code below the -# embedded rST text block: - -pendulum_array = [Pendulum.from_pars(p=phase) for phase in np.linspace(0, 1, 3)] -fig = plt.figure() -for pendulum in pendulum_array: - pendulum.plot(t, label=f'Phase = {pendulum.p}') -plt.legend(loc='lower right') -fig.show() diff --git a/legacy/Examples/fitting/README.rst b/legacy/Examples/fitting/README.rst deleted file mode 100644 index 614960cb..00000000 --- a/legacy/Examples/fitting/README.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _fitting_examples: - -Fitting Examples ----------------- - -This section gathers examples which demonstrate fitting functionality using EasyScience's fitting capabilities. diff --git a/legacy/PARAMETER_DEPENDENCY_SERIALIZATION.md b/legacy/PARAMETER_DEPENDENCY_SERIALIZATION.md deleted file mode 100644 index 66a6659b..00000000 --- a/legacy/PARAMETER_DEPENDENCY_SERIALIZATION.md +++ /dev/null @@ -1,259 +0,0 @@ -# Parameter Dependency Serialization - -This document explains how to serialize and deserialize `Parameter` -objects that have dependencies. - -## Overview - -Parameters with dependencies can now be serialized to dictionaries (and -JSON) while preserving their dependency relationships. After -deserialization, the dependencies are automatically reconstructed using -the `serializer_id` attribute to match parameters, with `unique_name` -attribute being used as a fallback. - -## Key Features - -- **Automatic dependency serialization**: Dependency expressions and - maps are automatically saved during serialization -- **Reliable dependency resolution**: Dependencies are resolved using - stable `serializer_id` attributes with `unique_name` as fallback after - deserialization -- **Order-independent loading**: Parameters can be loaded in any order - thanks to the reliable ID system -- **Bulk dependency resolution**: Utility functions help resolve all - dependencies at once -- **JSON compatibility**: Full support for JSON - serialization/deserialization -- **Backward compatibility**: Existing code using `unique_name` - continues to work as fallback - -## Usage - -### Basic Serialization/Deserialization - -```python -import json -from easyscience import Parameter, global_object -from easyscience.variable.parameter_dependency_resolver import resolve_all_parameter_dependencies - -# Create parameters with dependencies -a = Parameter(name="a", value=2.0, unit="m", min=0, max=10) -b = Parameter.from_dependency( - name="b", - dependency_expression="2 * a", - dependency_map={"a": a}, - unit="m" -) - -# Serialize to dictionary and save to file -params_dict = {"a": a.as_dict(), "b": b.as_dict()} -with open("parameters.json", "w") as f: - json.dump(params_dict, f, indent=2, default=str) - -print("Parameters saved to parameters.json") -``` - -In a new Python session: - -```python -import json -from easyscience import Parameter, global_object -from easyscience.variable.parameter_dependency_resolver import resolve_all_parameter_dependencies - -# Load parameters from file -with open("parameters.json", "r") as f: - params_dict = json.load(f) - -# Clear global map (simulate new environment) -global_object.map._clear() - -# Deserialize parameters -new_a = Parameter.from_dict(params_dict["a"]) -new_b = Parameter.from_dict(params_dict["b"]) - -# Resolve dependencies -resolve_all_parameter_dependencies({"a": new_a, "b": new_b}) - -# Dependencies are now working -new_a.value = 5.0 -print(new_b.value) # Will be 10.0 (2 * 5.0) -``` - -### JSON Serialization - -```python -import json - -# Serialize to JSON -param_dict = parameter.as_dict() -json_str = json.dumps(param_dict, default=str) - -# Deserialize from JSON -loaded_dict = json.loads(json_str) -new_param = Parameter.from_dict(loaded_dict) - -# Resolve dependencies -resolve_all_parameter_dependencies(new_param) -``` - -### Bulk Operations - -```python -from easyscience.variable.parameter_dependency_resolver import get_parameters_with_pending_dependencies - -# Create multiple parameters with dependencies -params = create_parameter_hierarchy() # Your function - -# Serialize all -serialized = {name: param.as_dict() for name, param in params.items()} - -# Clear and deserialize -global_object.map._clear() -new_params = {name: Parameter.from_dict(d) for name, d in serialized.items()} - -# Check which parameters have pending dependencies -pending = get_parameters_with_pending_dependencies(new_params) -print(f"Found {len(pending)} parameters with pending dependencies") - -# Resolve all at once -resolve_all_parameter_dependencies(new_params) -``` - -## Implementation Details - -### Serialization - -During serialization, the following additional fields are added to -dependent parameters: - -- `_dependency_string`: The original dependency expression -- `_dependency_map_serializer_ids`: A mapping of dependency keys to - stable dependency IDs (preferred) -- `_dependency_map_unique_names`: A mapping of dependency keys to unique - names (fallback) -- `__serializer_id`: The parameter's own unique dependency ID -- `_independent`: Boolean flag indicating if the parameter is dependent - -### Deserialization - -During deserialization: - -1. Parameters are created normally but marked as independent temporarily -2. Dependency information is stored in `_pending_dependency_string`, - `_pending_dependency_map_serializer_ids`, and - `_pending_dependency_map_unique_names` attributes -3. The parameter's own `__serializer_id` is restored from serialized - data -4. After all parameters are loaded, - `resolve_all_parameter_dependencies()` establishes the dependency - relationships using dependency IDs first, then unique names as - fallback - -### Dependency Resolution - -The dependency resolution process: - -1. Scans for parameters with pending dependencies -2. First attempts to look up dependency objects by their stable - `serializer_id` -3. Falls back to `unique_name` lookup in the global map if serializer_id - is not available -4. Calls `make_dependent_on()` to establish the dependency relationship -5. Cleans up temporary attributes - -This dual-strategy approach ensures reliable dependency resolution -regardless of parameter loading order while maintaining backward -compatibility. - -## Error Handling - -The system provides detailed error messages for common issues: - -- Missing dependencies (parameter with required unique_name not found) -- Invalid dependency expressions -- Circular dependency detection - -## Utility Functions - -### `resolve_all_parameter_dependencies(obj)` - -Recursively finds all Parameter objects with pending dependencies and -resolves them. - -**Parameters:** - -- `obj`: Object to search for Parameters (can be Parameter, list, dict, - or complex object) - -**Returns:** - -- None (modifies parameters in place) - -**Raises:** - -- `ValueError`: If dependency resolution fails - -### `get_parameters_with_pending_dependencies(obj)` - -Finds all Parameter objects that have pending dependencies. - -**Parameters:** - -- `obj`: Object to search for Parameters - -**Returns:** - -- `List[Parameter]`: List of parameters with pending dependencies - -## Best Practices - -1. **Always resolve dependencies after deserialization**: Use - `resolve_all_parameter_dependencies()` after loading serialized - parameters - -2. **Handle the global map carefully**: The global map must contain all - referenced parameters for dependency resolution to work - -3. **Use unique names for cross-references**: When creating dependency - expressions that reference other parameters, consider using unique - names with quotes: `'Parameter_0'` - -4. **Error handling**: Wrap dependency resolution in try-catch blocks - for robust error handling - -5. **Bulk operations**: For complex object hierarchies, use the utility - functions to handle all parameters at once - -6. **Reliable ordering**: With the new dependency ID system, parameters - can be loaded in any order without affecting dependency resolution - -7. **Access dependency ID**: Use `parameter.serializer_id` to access the - stable ID for debugging or manual cross-referencing - -## Example: Complex Hierarchy - -```python -def save_model(model): - \"\"\"Save a model with parameter dependencies to JSON.\"\"\" - model_dict = model.as_dict() - with open('model.json', 'w') as f: - json.dump(model_dict, f, indent=2, default=str) - -def load_model(filename): - \"\"\"Load a model from JSON and resolve dependencies.\"\"\" - global_object.map._clear() # Start fresh - - with open(filename) as f: - model_dict = json.load(f) - - model = Model.from_dict(model_dict) - - # Resolve all parameter dependencies - resolve_all_parameter_dependencies(model) - - return model -``` - -This system ensures that complex parameter hierarchies with dependencies -can be reliably serialized and reconstructed while maintaining their -behavioral relationships. diff --git a/legacy/examples_old/dataset_examples.ipynb b/legacy/examples_old/dataset_examples.ipynb deleted file mode 100644 index 48f206c2..00000000 --- a/legacy/examples_old/dataset_examples.ipynb +++ /dev/null @@ -1,1254 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from easyscience.Datasets.xarray import xr\n", - "from easyscience.Objects.Base import Parameter, BaseObj\n", - "from easyscience.fitting import Fitter" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "try:\n", - " import hvplot.xarray\n", - "except:\n", - " import sys\n", - " !{sys.executable} -m pip install hvplot\n", - " import hvplot.xarray" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1D Data" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a dataset\n", - "d = xr.Dataset()\n", - "\n", - "# Create a coreObject\n", - "b = BaseObj('line',\n", - " m=Parameter('m', 1),\n", - " c=Parameter('c', 1))\n", - "\n", - "# Create a fit function to use with b\n", - "def fit_fun(x, *args, **kwargs):\n", - " return b.c.raw_value + b.m.raw_value * x\n", - "\n", - "\n", - "# Create and initialize the fitter\n", - "f = Fitter()\n", - "f.initialize(b, fit_fun)\n", - "\n", - "# Generate some `fake` data to fit to\n", - "nx = 1E3\n", - "x_min = 0\n", - "x_max = 100\n", - "\n", - "x = np.linspace(x_min, x_max, num=int(nx))\n", - "y = 2*x - 1 + 5*(np.random.random(size=x.shape) - 0.5)\n", - "\n", - "# Add the data to the dataset\n", - "d.easyscience.add_coordinate('x', x)\n", - "d.easyscience.add_variable('y', ['x'], y, auto_sigma=False)\n", - "\n", - "# Perform the fit\n", - "f_res = d['y'].easyscience.fit(f)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":NdOverlay [Variable]\n", - " :Curve [x] (value)" - ] - }, - "execution_count": 14, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1608" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "# Plot the results\n", - "d['fit'] = f_res.y_calc.unstack()\n", - "d.hvplot(y=['y','fit'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2D Data" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "# Variables to be used for `fake` data generation\n", - "nx = 5E2\n", - "x_min = 0\n", - "x_max = 3\n", - "\n", - "sin_offest = - 0.35\n", - "cos_offset = 0.14\n", - "s_off_start_point = 0\n", - "c_off_start_point = 0" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "d = xr.Dataset()\n", - "\n", - "# Generate some `x`, `y`, `z` data\n", - "x = np.linspace(x_min, x_max, num=int(nx))\n", - "y = x + 1\n", - "z = np.sin(2*np.pi*(x + sin_offest)).reshape((-1, 1))* np.cos(2*np.pi*(x + cos_offset)).reshape((1, -1)) + \\\n", - " 2*(0.5 - np.random.random(size=(int(nx), int(nx))))\n", - "\n", - "# Add it to the dataset\n", - "d.easyscience.add_coordinate('x', x)\n", - "d.easyscience.add_coordinate('y', y)\n", - "d.easyscience.add_variable('z', ['x', 'y'], z)\n", - "\n", - "\n", - "# easyscience Object\n", - "b = BaseObj('line',\n", - " s_off=Parameter('s_off', s_off_start_point),\n", - " c_off=Parameter('c_off', c_off_start_point))\n", - "\n", - "# The fitting function\n", - "def fit_fun(x, *args, **kwargs):\n", - " return np.sin(2*np.pi*(x[:, 0] + b.s_off.raw_value)) * np.cos(2*np.pi*(x[:, 1] + b.c_off.raw_value))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset>\n",
-       "Dimensions:  (x: 500, y: 500)\n",
-       "Coordinates:\n",
-       "  * x        (x) float64 0.0 0.006012 0.01202 0.01804 ... 2.982 2.988 2.994 3.0\n",
-       "  * y        (y) float64 1.0 1.006 1.012 1.018 1.024 ... 3.982 3.988 3.994 4.0\n",
-       "Data variables:\n",
-       "    z        (x, y) float64 0.3882 -1.286 -0.8712 ... -0.3584 -1.213 -0.3778\n",
-       "Attributes:\n",
-       "    name:         \n",
-       "    description:  \n",
-       "    url:          \n",
-       "    units:        {'x': <Unit('dimensionless')>, 'y': <Unit('dimensionless')>...
" - ], - "text/plain": [ - "\n", - "Dimensions: (x: 500, y: 500)\n", - "Coordinates:\n", - " * x (x) float64 0.0 0.006012 0.01202 0.01804 ... 2.982 2.988 2.994 3.0\n", - " * y (y) float64 1.0 1.006 1.012 1.018 1.024 ... 3.982 3.988 3.994 4.0\n", - "Data variables:\n", - " z (x, y) float64 0.3882 -1.286 -0.8712 ... -0.3584 -1.213 -0.3778\n", - "Attributes:\n", - " name: \n", - " description: \n", - " url: \n", - " units: {'x': , 'y': ..." - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "d" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Image [y,x] (z)" - ] - }, - "execution_count": 19, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1827" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "d['z'].hvplot()" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Image [y,x] (computed_)" - ] - }, - "execution_count": 20, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1942" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "\n", - "f = Fitter()\n", - "f.initialize(b, fit_fun)\n", - "minimizer = f.current_engine\n", - "\n", - "f_res = d['z'].easyscience.fit(f)\n", - "d[f'computed_{minimizer}'] = f_res.y_calc.unstack()\n", - "d[f'dz_{minimizer}'] = d['z'] - d[f'computed_{minimizer}']\n", - "\n", - "\n", - "d[f'computed_{minimizer}'].hvplot()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Image [y,x] (dz_)" - ] - }, - "execution_count": 10, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1231" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "d[f'dz_{minimizer}'].hvplot()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray 'axes_broadcast' (x: 500, y: 500)>\n",
-       "array([[0.        , 0.        , 0.        , ..., 0.        , 0.        ,\n",
-       "        0.        ],\n",
-       "       [0.00601202, 0.00601202, 0.00601202, ..., 0.00601202, 0.00601202,\n",
-       "        0.00601202],\n",
-       "       [0.01202405, 0.01202405, 0.01202405, ..., 0.01202405, 0.01202405,\n",
-       "        0.01202405],\n",
-       "       ...,\n",
-       "       [2.98797595, 2.98797595, 2.98797595, ..., 2.98797595, 2.98797595,\n",
-       "        2.98797595],\n",
-       "       [2.99398798, 2.99398798, 2.99398798, ..., 2.99398798, 2.99398798,\n",
-       "        2.99398798],\n",
-       "       [3.        , 3.        , 3.        , ..., 3.        , 3.        ,\n",
-       "        3.        ]])\n",
-       "Coordinates:\n",
-       "  * x        (x) float64 0.0 0.006012 0.01202 0.01804 ... 2.982 2.988 2.994 3.0\n",
-       "  * y        (y) float64 1.0 1.006 1.012 1.018 1.024 ... 3.982 3.988 3.994 4.0
" - ], - "text/plain": [ - "\n", - "array([[0. , 0. , 0. , ..., 0. , 0. ,\n", - " 0. ],\n", - " [0.00601202, 0.00601202, 0.00601202, ..., 0.00601202, 0.00601202,\n", - " 0.00601202],\n", - " [0.01202405, 0.01202405, 0.01202405, ..., 0.01202405, 0.01202405,\n", - " 0.01202405],\n", - " ...,\n", - " [2.98797595, 2.98797595, 2.98797595, ..., 2.98797595, 2.98797595,\n", - " 2.98797595],\n", - " [2.99398798, 2.99398798, 2.99398798, ..., 2.99398798, 2.99398798,\n", - " 2.99398798],\n", - " [3. , 3. , 3. , ..., 3. , 3. ,\n", - " 3. ]])\n", - "Coordinates:\n", - " * x (x) float64 0.0 0.006012 0.01202 0.01804 ... 2.982 2.988 2.994 3.0\n", - " * y (y) float64 1.0 1.006 1.012 1.018 1.024 ... 3.982 3.988 3.994 4.0" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x_array = f_res.x.unstack()\n", - "x_array[0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.8" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/legacy/examples_old/dimer_example.ipynb b/legacy/examples_old/dimer_example.ipynb deleted file mode 100644 index fc18f014..00000000 --- a/legacy/examples_old/dimer_example.ipynb +++ /dev/null @@ -1,1144 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "# Weakly connected 1D spin dimers (Spin ladder)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from easyscience.Datasets.xarray import xr\n", - "from easyscience.Objects.Base import Parameter, BaseObj\n", - "from easyscience.fitting import Fitter" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": "\n(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\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, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\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\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\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 }\n\n var skip = [];\n if (window.requirejs) {\n require([], function() {\n })\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) >= 0) { on_load(); continue; }\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 console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n\tif (!js_urls.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 var js_urls = [];\n var css_urls = [];\n\n var inline_js = [\n function(Bokeh) {\n inject_raw_css(\".bk.panel-widget-box {\\n\\tmin-height: 20px;\\n\\tbackground-color: #f5f5f5;\\n\\tborder: 1px solid #e3e3e3;\\n\\tborder-radius: 4px;\\n\\t-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05);\\n\\tbox-shadow: inset 0 1px 1px rgba(0,0,0,.05);\\n\\toverflow-x: hidden;\\n\\toverflow-y: hidden;\\n}\\n\\n.scrollable {\\n overflow: scroll;\\n}\\n\\nprogress {\\n\\tappearance: none;\\n\\t-moz-appearance: none;\\n\\t-webkit-appearance: none;\\n\\n\\tborder: none;\\n\\theight: 20px;\\n\\tbackground-color: whiteSmoke;\\n\\tborder-radius: 3px;\\n\\tbox-shadow: 0 2px 3px rgba(0,0,0,.5) inset;\\n\\tcolor: royalblue;\\n\\tposition: relative;\\n\\tmargin: 0 0 1.5em;\\n}\\n\\nprogress[value]::-webkit-progress-bar {\\n\\tbackground-color: whiteSmoke;\\n\\tborder-radius: 3px;\\n\\tbox-shadow: 0 2px 3px rgba(0,0,0,.5) inset;\\n}\\n\\nprogress[value]::-webkit-progress-value {\\n\\tposition: relative;\\n\\n\\tbackground-size: 35px 20px, 100% 100%, 100% 100%;\\n\\tborder-radius:3px;\\n}\\n\\nprogress.active:not([value])::before {\\n\\tbackground-position: 10%;\\n\\tanimation-name: stripes;\\n\\tanimation-duration: 3s;\\n\\tanimation-timing-function: linear;\\n\\tanimation-iteration-count: infinite;\\n}\\n\\nprogress[value]::-moz-progress-bar {\\n\\tbackground-size: 35px 20px, 100% 100%, 100% 100%;\\n\\tborder-radius:3px;\\n}\\n\\nprogress:not([value])::-moz-progress-bar {\\n\\tborder-radius:3px;\\n\\tbackground:\\n\\tlinear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, 0.2) 33%, rgba(0, 0, 0, 0.2) 66%, transparent 66%) left/2.5em 1.5em;\\n\\n}\\n\\nprogress.active:not([value])::-moz-progress-bar {\\n\\tbackground-position: 10%;\\n\\tanimation-name: stripes;\\n\\tanimation-duration: 3s;\\n\\tanimation-timing-function: linear;\\n\\tanimation-iteration-count: infinite;\\n}\\n\\nprogress.active:not([value])::-webkit-progress-bar {\\n\\tbackground-position: 10%;\\n\\tanimation-name: stripes;\\n\\tanimation-duration: 3s;\\n\\tanimation-timing-function: linear;\\n\\tanimation-iteration-count: infinite;\\n}\\n\\nprogress.primary[value]::-webkit-progress-value { background-color: #007bff; }\\nprogress.primary:not([value])::before { background-color: #007bff; }\\nprogress.primary:not([value])::-webkit-progress-bar { background-color: #007bff; }\\nprogress.primary::-moz-progress-bar { background-color: #007bff; }\\n\\nprogress.secondary[value]::-webkit-progress-value { background-color: #6c757d; }\\nprogress.secondary:not([value])::before { background-color: #6c757d; }\\nprogress.secondary:not([value])::-webkit-progress-bar { background-color: #6c757d; }\\nprogress.secondary::-moz-progress-bar { background-color: #6c757d; }\\n\\nprogress.success[value]::-webkit-progress-value { background-color: #28a745; }\\nprogress.success:not([value])::before { background-color: #28a745; }\\nprogress.success:not([value])::-webkit-progress-bar { background-color: #28a745; }\\nprogress.success::-moz-progress-bar { background-color: #28a745; }\\n\\nprogress.danger[value]::-webkit-progress-value { background-color: #dc3545; }\\nprogress.danger:not([value])::before { background-color: #dc3545; }\\nprogress.danger:not([value])::-webkit-progress-bar { background-color: #dc3545; }\\nprogress.danger::-moz-progress-bar { background-color: #dc3545; }\\n\\nprogress.warning[value]::-webkit-progress-value { background-color: #ffc107; }\\nprogress.warning:not([value])::before { background-color: #ffc107; }\\nprogress.warning:not([value])::-webkit-progress-bar { background-color: #ffc107; }\\nprogress.warning::-moz-progress-bar { background-color: #ffc107; }\\n\\nprogress.info[value]::-webkit-progress-value { background-color: #17a2b8; }\\nprogress.info:not([value])::before { background-color: #17a2b8; }\\nprogress.info:not([value])::-webkit-progress-bar { background-color: #17a2b8; }\\nprogress.info::-moz-progress-bar { background-color: #17a2b8; }\\n\\nprogress.light[value]::-webkit-progress-value { background-color: #f8f9fa; }\\nprogress.light:not([value])::before { background-color: #f8f9fa; }\\nprogress.light:not([value])::-webkit-progress-bar { background-color: #f8f9fa; }\\nprogress.light::-moz-progress-bar { background-color: #f8f9fa; }\\n\\nprogress.dark[value]::-webkit-progress-value { background-color: #343a40; }\\nprogress.dark:not([value])::-webkit-progress-bar { background-color: #343a40; }\\nprogress.dark:not([value])::before { background-color: #343a40; }\\nprogress.dark::-moz-progress-bar { background-color: #343a40; }\\n\\nprogress:not([value])::-webkit-progress-bar {\\n\\tborder-radius: 3px;\\n\\tbackground:\\n\\tlinear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, 0.2) 33%, rgba(0, 0, 0, 0.2) 66%, transparent 66%) left/2.5em 1.5em;\\n}\\nprogress:not([value])::before {\\n\\tcontent:\\\" \\\";\\n\\tposition:absolute;\\n\\theight: 20px;\\n\\ttop:0;\\n\\tleft:0;\\n\\tright:0;\\n\\tbottom:0;\\n\\tborder-radius: 3px;\\n\\tbackground:\\n\\tlinear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, 0.2) 33%, rgba(0, 0, 0, 0.2) 66%, transparent 66%) left/2.5em 1.5em;\\n}\\n\\n@keyframes stripes {\\n from {background-position: 0%}\\n to {background-position: 100%}\\n}\\n\\n.bk.loader::after {\\n content: \\\"\\\";\\n border-radius: 50%;\\n -webkit-mask-image: radial-gradient(transparent 50%, rgba(0, 0, 0, 1) 54%);\\n width: 100%;\\n height: 100%;\\n left: 0;\\n top: 0;\\n position: absolute;\\n}\\n\\n.bk-root .bk.loader.dark::after {\\n background: #0f0f0f;\\n}\\n\\n.bk-root .bk.loader.light::after {\\n background: #f0f0f0;\\n}\\n\\n.bk-root .bk.loader.spin::after {\\n animation: spin 2s linear infinite;\\n}\\n\\n.bk-root div.bk.loader.spin.primary-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #007bff 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.secondary-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #6c757d 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.success-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #28a745 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.danger-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #dc3545 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.warning-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #ffc107 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.info-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #17a2b8 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.light-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #f8f9fa 50%);\\n}\\n\\n.bk-root div.bk.loader.dark-light::after {\\n background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #343a40 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.primary-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #007bff 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.secondary-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #6c757d 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.success-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #28a745 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.danger-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #dc3545 50%)\\n}\\n\\n.bk-root div.bk.loader.spin.warning-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #ffc107 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.info-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #17a2b8 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.light-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #f8f9fa 50%);\\n}\\n\\n.bk-root div.bk.loader.spin.dark-dark::after {\\n background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #343a40 50%);\\n}\\n\\n/* Safari */\\n@-webkit-keyframes spin {\\n 0% { -webkit-transform: rotate(0deg); }\\n 100% { -webkit-transform: rotate(360deg); }\\n}\\n\\n@keyframes spin {\\n 0% { transform: rotate(0deg); }\\n 100% { transform: rotate(360deg); }\\n}\\n\\n.dot div {\\n height: 100%;\\n width: 100%;\\n border: 1px solid #000 !important;\\n background-color: #fff;\\n border-radius: 50%;\\n display: inline-block;\\n}\\n\\n.dot-filled div {\\n height: 100%;\\n width: 100%;\\n border: 1px solid #000 !important;\\n border-radius: 50%;\\n display: inline-block;\\n}\\n\\n.dot-filled.primary div {\\n background-color: #007bff;\\n}\\n\\n.dot-filled.secondary div {\\n background-color: #6c757d;\\n}\\n\\n.dot-filled.success div {\\n background-color: #28a745;\\n}\\n\\n.dot-filled.danger div {\\n background-color: #dc3545;\\n}\\n\\n.dot-filled.warning div {\\n background-color: #ffc107;\\n}\\n\\n.dot-filled.info div {\\n background-color: #17a2b8;\\n}\\n\\n.dot-filled.dark div {\\n background-color: #343a40;\\n}\\n\\n.dot-filled.light div {\\n background-color: #f8f9fa;\\n}\");\n },\n function(Bokeh) {\n inject_raw_css(\"table.panel-df {\\n margin-left: auto;\\n margin-right: auto;\\n border: none;\\n border-collapse: collapse;\\n border-spacing: 0;\\n color: black;\\n font-size: 12px;\\n table-layout: fixed;\\n width: 100%;\\n}\\n\\n.panel-df tr, .panel-df th, .panel-df td {\\n text-align: right;\\n vertical-align: middle;\\n padding: 0.5em 0.5em !important;\\n line-height: normal;\\n white-space: normal;\\n max-width: none;\\n border: none;\\n}\\n\\n.panel-df tbody {\\n display: table-row-group;\\n vertical-align: middle;\\n border-color: inherit;\\n}\\n\\n.panel-df tbody tr:nth-child(odd) {\\n background: #f5f5f5;\\n}\\n\\n.panel-df thead {\\n border-bottom: 1px solid black;\\n vertical-align: bottom;\\n}\\n\\n.panel-df tr:hover {\\n background: lightblue !important;\\n cursor: pointer;\\n}\\n\");\n },\n function(Bokeh) {\n inject_raw_css(\".json-formatter-row {\\n font-family: monospace;\\n}\\n.json-formatter-row,\\n.json-formatter-row a,\\n.json-formatter-row a:hover {\\n color: black;\\n text-decoration: none;\\n}\\n.json-formatter-row .json-formatter-row {\\n margin-left: 1rem;\\n}\\n.json-formatter-row .json-formatter-children.json-formatter-empty {\\n opacity: 0.5;\\n margin-left: 1rem;\\n}\\n.json-formatter-row .json-formatter-children.json-formatter-empty:after {\\n display: none;\\n}\\n.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {\\n content: \\\"No properties\\\";\\n}\\n.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {\\n content: \\\"[]\\\";\\n}\\n.json-formatter-row .json-formatter-string,\\n.json-formatter-row .json-formatter-stringifiable {\\n color: green;\\n white-space: pre;\\n word-wrap: break-word;\\n}\\n.json-formatter-row .json-formatter-number {\\n color: blue;\\n}\\n.json-formatter-row .json-formatter-boolean {\\n color: red;\\n}\\n.json-formatter-row .json-formatter-null {\\n color: #855A00;\\n}\\n.json-formatter-row .json-formatter-undefined {\\n color: #ca0b69;\\n}\\n.json-formatter-row .json-formatter-function {\\n color: #FF20ED;\\n}\\n.json-formatter-row .json-formatter-date {\\n background-color: rgba(0, 0, 0, 0.05);\\n}\\n.json-formatter-row .json-formatter-url {\\n text-decoration: underline;\\n color: blue;\\n cursor: pointer;\\n}\\n.json-formatter-row .json-formatter-bracket {\\n color: blue;\\n}\\n.json-formatter-row .json-formatter-key {\\n color: #00008B;\\n padding-right: 0.2rem;\\n}\\n.json-formatter-row .json-formatter-toggler-link {\\n cursor: pointer;\\n}\\n.json-formatter-row .json-formatter-toggler {\\n line-height: 1.2rem;\\n font-size: 0.7rem;\\n vertical-align: middle;\\n opacity: 0.6;\\n cursor: pointer;\\n padding-right: 0.2rem;\\n}\\n.json-formatter-row .json-formatter-toggler:after {\\n display: inline-block;\\n transition: transform 100ms ease-in;\\n content: \\\"\\\\25BA\\\";\\n}\\n.json-formatter-row > a > .json-formatter-preview-text {\\n opacity: 0;\\n transition: opacity 0.15s ease-in;\\n font-style: italic;\\n}\\n.json-formatter-row:hover > a > .json-formatter-preview-text {\\n opacity: 0.6;\\n}\\n.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {\\n transform: rotate(90deg);\\n}\\n.json-formatter-row.json-formatter-open > .json-formatter-children:after {\\n display: inline-block;\\n}\\n.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {\\n display: none;\\n}\\n.json-formatter-row.json-formatter-open.json-formatter-empty:after {\\n display: block;\\n}\\n.json-formatter-dark.json-formatter-row {\\n font-family: monospace;\\n}\\n.json-formatter-dark.json-formatter-row,\\n.json-formatter-dark.json-formatter-row a,\\n.json-formatter-dark.json-formatter-row a:hover {\\n color: white;\\n text-decoration: none;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-row {\\n margin-left: 1rem;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty {\\n opacity: 0.5;\\n margin-left: 1rem;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty:after {\\n display: none;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {\\n content: \\\"No properties\\\";\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {\\n content: \\\"[]\\\";\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-string,\\n.json-formatter-dark.json-formatter-row .json-formatter-stringifiable {\\n color: #31F031;\\n white-space: pre;\\n word-wrap: break-word;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-number {\\n color: #66C2FF;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-boolean {\\n color: #EC4242;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-null {\\n color: #EEC97D;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-undefined {\\n color: #ef8fbe;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-function {\\n color: #FD48CB;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-date {\\n background-color: rgba(255, 255, 255, 0.05);\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-url {\\n text-decoration: underline;\\n color: #027BFF;\\n cursor: pointer;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-bracket {\\n color: #9494FF;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-key {\\n color: #23A0DB;\\n padding-right: 0.2rem;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-toggler-link {\\n cursor: pointer;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-toggler {\\n line-height: 1.2rem;\\n font-size: 0.7rem;\\n vertical-align: middle;\\n opacity: 0.6;\\n cursor: pointer;\\n padding-right: 0.2rem;\\n}\\n.json-formatter-dark.json-formatter-row .json-formatter-toggler:after {\\n display: inline-block;\\n transition: transform 100ms ease-in;\\n content: \\\"\\\\25BA\\\";\\n}\\n.json-formatter-dark.json-formatter-row > a > .json-formatter-preview-text {\\n opacity: 0;\\n transition: opacity 0.15s ease-in;\\n font-style: italic;\\n}\\n.json-formatter-dark.json-formatter-row:hover > a > .json-formatter-preview-text {\\n opacity: 0.6;\\n}\\n.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {\\n transform: rotate(90deg);\\n}\\n.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-children:after {\\n display: inline-block;\\n}\\n.json-formatter-dark.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {\\n display: none;\\n}\\n.json-formatter-dark.json-formatter-row.json-formatter-open.json-formatter-empty:after {\\n display: block;\\n}\\n\");\n },\n function(Bokeh) {\n inject_raw_css(\".bk.card {\\n border: 1px solid rgba(0,0,0,.125);\\n border-radius: 0.25rem;\\n}\\n.bk.accordion {\\n border: 1px solid rgba(0,0,0,.125);\\n}\\n.bk.card-header {\\n align-items: center;\\n background-color: rgba(0, 0, 0, 0.03);\\n border-radius: 0.25rem;\\n display: flex;\\n justify-content: space-between;\\n padding: 0 1.25rem 0 0;\\n width: 100%;\\n}\\n.bk.accordion-header {\\n align-items: center;\\n background-color: rgba(0, 0, 0, 0.03);\\n border-radius: 0;\\n display: flex;\\n justify-content: space-between;\\n padding: 0 1.25rem 0 0;\\n width: 100%;\\n}\\np.bk.card-button {\\n background-color: transparent;\\n font-size: 1.25rem;\\n font-weight: 700;\\n margin: 0;\\n margin-left: -15px;\\n}\\n.bk.card-header-row {\\n position: relative !important;\\n}\\n.bk.card-title {\\n align-items: center;\\n display: flex !important;\\n font-size: 1.4em;\\n font-weight: bold;\\n padding: 0.25em;\\n position: relative !important;\\n}\\n\");\n },\n function(Bokeh) {\n inject_raw_css(\".codehilite .hll { background-color: #ffffcc }\\n.codehilite { background: #f8f8f8; }\\n.codehilite .c { color: #408080; font-style: italic } /* Comment */\\n.codehilite .err { border: 1px solid #FF0000 } /* Error */\\n.codehilite .k { color: #008000; font-weight: bold } /* Keyword */\\n.codehilite .o { color: #666666 } /* Operator */\\n.codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */\\n.codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */\\n.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */\\n.codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */\\n.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */\\n.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */\\n.codehilite .gd { color: #A00000 } /* Generic.Deleted */\\n.codehilite .ge { font-style: italic } /* Generic.Emph */\\n.codehilite .gr { color: #FF0000 } /* Generic.Error */\\n.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */\\n.codehilite .gi { color: #00A000 } /* Generic.Inserted */\\n.codehilite .go { color: #888888 } /* Generic.Output */\\n.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */\\n.codehilite .gs { font-weight: bold } /* Generic.Strong */\\n.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */\\n.codehilite .gt { color: #0044DD } /* Generic.Traceback */\\n.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */\\n.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */\\n.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */\\n.codehilite .kp { color: #008000 } /* Keyword.Pseudo */\\n.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */\\n.codehilite .kt { color: #B00040 } /* Keyword.Type */\\n.codehilite .m { color: #666666 } /* Literal.Number */\\n.codehilite .s { color: #BA2121 } /* Literal.String */\\n.codehilite .na { color: #7D9029 } /* Name.Attribute */\\n.codehilite .nb { color: #008000 } /* Name.Builtin */\\n.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */\\n.codehilite .no { color: #880000 } /* Name.Constant */\\n.codehilite .nd { color: #AA22FF } /* Name.Decorator */\\n.codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */\\n.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */\\n.codehilite .nf { color: #0000FF } /* Name.Function */\\n.codehilite .nl { color: #A0A000 } /* Name.Label */\\n.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */\\n.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */\\n.codehilite .nv { color: #19177C } /* Name.Variable */\\n.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */\\n.codehilite .w { color: #bbbbbb } /* Text.Whitespace */\\n.codehilite .mb { color: #666666 } /* Literal.Number.Bin */\\n.codehilite .mf { color: #666666 } /* Literal.Number.Float */\\n.codehilite .mh { color: #666666 } /* Literal.Number.Hex */\\n.codehilite .mi { color: #666666 } /* Literal.Number.Integer */\\n.codehilite .mo { color: #666666 } /* Literal.Number.Oct */\\n.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */\\n.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */\\n.codehilite .sc { color: #BA2121 } /* Literal.String.Char */\\n.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */\\n.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */\\n.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */\\n.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */\\n.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */\\n.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */\\n.codehilite .sx { color: #008000 } /* Literal.String.Other */\\n.codehilite .sr { color: #BB6688 } /* Literal.String.Regex */\\n.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */\\n.codehilite .ss { color: #19177C } /* Literal.String.Symbol */\\n.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */\\n.codehilite .fm { color: #0000FF } /* Name.Function.Magic */\\n.codehilite .vc { color: #19177C } /* Name.Variable.Class */\\n.codehilite .vg { color: #19177C } /* Name.Variable.Global */\\n.codehilite .vi { color: #19177C } /* Name.Variable.Instance */\\n.codehilite .vm { color: #19177C } /* Name.Variable.Magic */\\n.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */\\n\\n.markdown h1 { margin-block-start: 0.34em }\\n.markdown h2 { margin-block-start: 0.42em }\\n.markdown h3 { margin-block-start: 0.5em }\\n.markdown h4 { margin-block-start: 0.67em }\\n.markdown h5 { margin-block-start: 0.84em }\\n.markdown h6 { margin-block-start: 1.17em }\\n.markdown ul { padding-inline-start: 2em }\\n.markdown ol { padding-inline-start: 2em }\\n.markdown strong { font-weight: 600 }\\n.markdown a { color: -webkit-link }\\n.markdown a { color: -moz-hyperlinkText }\\n\");\n },\n function(Bokeh) {\n inject_raw_css(\".bk.alert {\\n padding: 0.75rem 1.25rem;\\n border: 1px solid transparent;\\n border-radius: 0.25rem;\\n /* Don't set margin because that will not render correctly! */\\n /* margin-bottom: 1rem; */\\n margin-top: 15px;\\n margin-bottom: 15px;\\n}\\n.bk.alert a {\\n color: rgb(11, 46, 19); /* #002752; */\\n font-weight: 700;\\n text-decoration: rgb(11, 46, 19);\\n text-decoration-color: rgb(11, 46, 19);\\n text-decoration-line: none;\\n text-decoration-style: solid;\\n text-decoration-thickness: auto;\\n }\\n.bk.alert a:hover {\\n color: rgb(11, 46, 19);\\n font-weight: 700;\\n text-decoration: underline;\\n}\\n\\n.bk.alert-primary {\\n color: #004085;\\n background-color: #cce5ff;\\n border-color: #b8daff;\\n}\\n.bk.alert-primary hr {\\n border-top-color: #9fcdff;\\n}\\n\\n.bk.alert-secondary {\\n color: #383d41;\\n background-color: #e2e3e5;\\n border-color: #d6d8db;\\n }\\n.bk.alert-secondary hr {\\n border-top-color: #c8cbcf;\\n}\\n\\n.bk.alert-success {\\n color: #155724;\\n background-color: #d4edda;\\n border-color: #c3e6cb;\\n }\\n\\n.bk.alert-success hr {\\n border-top-color: #b1dfbb;\\n}\\n\\n.bk.alert-info {\\n color: #0c5460;\\n background-color: #d1ecf1;\\n border-color: #bee5eb;\\n }\\n.bk.alert-info hr {\\n border-top-color: #abdde5;\\n}\\n\\n.bk.alert-warning {\\n color: #856404;\\n background-color: #fff3cd;\\n border-color: #ffeeba;\\n }\\n\\n.bk.alert-warning hr {\\n border-top-color: #ffe8a1;\\n}\\n\\n.bk.alert-danger {\\n color: #721c24;\\n background-color: #f8d7da;\\n border-color: #f5c6cb;\\n}\\n.bk.alert-danger hr {\\n border-top-color: #f1b0b7;\\n}\\n\\n.bk.alert-light {\\n color: #818182;\\n background-color: #fefefe;\\n border-color: #fdfdfe;\\n }\\n.bk.alert-light hr {\\n border-top-color: #ececf6;\\n}\\n\\n.bk.alert-dark {\\n color: #1b1e21;\\n background-color: #d6d8d9;\\n border-color: #c6c8ca;\\n }\\n.bk.alert-dark hr {\\n border-top-color: #b9bbbe;\\n}\\n\\n\\n/* adjf\\u00e6l */\\n\\n.bk.alert-primary a {\\n color: #002752;\\n}\\n\\n.bk.alert-secondary a {\\n color: #202326;\\n}\\n\\n\\n.bk.alert-success a {\\n color: #0b2e13;\\n}\\n\\n\\n.bk.alert-info a {\\n color: #062c33;\\n}\\n\\n\\n.bk.alert-warning a {\\n color: #533f03;\\n}\\n\\n\\n.bk.alert-danger a {\\n color: #491217;\\n}\\n\\n.bk.alert-light a {\\n color: #686868;\\n}\\n\\n.bk.alert-dark a {\\n color: #040505;\\n}\");\n },\n function(Bokeh) {\n /* BEGIN bokeh.min.js */\n /*!\n * Copyright (c) 2012 - 2020, Anaconda, Inc., and Bokeh Contributors\n * All rights reserved.\n * \n * Redistribution and use in source and binary forms, with or without modification,\n * are permitted provided that the following conditions are met:\n * \n * Redistributions of source code must retain the above copyright notice,\n * this list of conditions and the following disclaimer.\n * \n * Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the documentation\n * and/or other materials provided with the distribution.\n * \n * Neither the name of Anaconda nor the names of any contributors\n * may be used to endorse or promote products derived from this software\n * without specific prior written permission.\n * \n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\n * THE POSSIBILITY OF SUCH DAMAGE.\n */\n (function(root, factory) {\n const bokeh = factory();\n bokeh.__bokeh__ = true;\n if (typeof root.Bokeh === \"undefined\" || typeof root.Bokeh.__bokeh__ === \"undefined\") {\n root.Bokeh = bokeh;\n }\n const Bokeh = root.Bokeh;\n Bokeh[bokeh.version] = bokeh;\n })(this, function() {\n var define;\n var parent_require = typeof require === \"function\" && require\n return (function(modules, entry, aliases, externals) {\n if (aliases === undefined) aliases = {};\n if (externals === undefined) externals = {};\n\n var cache = {};\n\n var normalize = function(name) {\n if (typeof name === \"number\")\n return name;\n\n if (name === \"bokehjs\")\n return entry;\n\n var prefix = \"@bokehjs/\"\n if (name.slice(0, prefix.length) === prefix)\n name = name.slice(prefix.length)\n\n var alias = aliases[name]\n if (alias != null)\n return alias;\n\n var trailing = name.length > 0 && name[name.lenght-1] === \"/\";\n var index = aliases[name + (trailing ? \"\" : \"/\") + \"index\"];\n if (index != null)\n return index;\n\n return name;\n }\n\n var require = function(name) {\n var mod = cache[name];\n if (!mod) {\n var id = normalize(name);\n\n mod = cache[id];\n if (!mod) {\n if (!modules[id]) {\n if (externals[id] === false || (externals[id] == true && parent_require)) {\n try {\n mod = {exports: externals[id] ? parent_require(id) : {}};\n cache[id] = cache[name] = mod;\n return mod.exports;\n } catch (e) {}\n }\n\n var err = new Error(\"Cannot find module '\" + name + \"'\");\n err.code = 'MODULE_NOT_FOUND';\n throw err;\n }\n\n mod = {exports: {}};\n cache[id] = cache[name] = mod;\n modules[id].call(mod.exports, require, mod, mod.exports);\n } else\n cache[name] = mod;\n }\n\n return mod.exports;\n }\n require.resolve = function(name) {\n return \"\"\n }\n\n var main = require(entry);\n main.require = require;\n\n if (typeof Proxy !== \"undefined\") {\n // allow Bokeh.loader[\"@bokehjs/module/name\"] syntax\n main.loader = new Proxy({}, {\n get: function(_obj, module) {\n return require(module);\n }\n });\n }\n\n main.register_plugin = function(plugin_modules, plugin_entry, plugin_aliases, plugin_externals) {\n if (plugin_aliases === undefined) plugin_aliases = {};\n if (plugin_externals === undefined) plugin_externals = {};\n\n for (var name in plugin_modules) {\n modules[name] = plugin_modules[name];\n }\n\n for (var name in plugin_aliases) {\n aliases[name] = plugin_aliases[name];\n }\n\n for (var name in plugin_externals) {\n externals[name] = plugin_externals[name];\n }\n\n var plugin = require(plugin_entry);\n\n for (var name in plugin) {\n main[name] = plugin[name];\n }\n\n return plugin;\n }\n\n return main;\n })\n ([\n function _(e,t,_){Object.defineProperty(_,\"__esModule\",{value:!0});e(1).__exportStar(e(2),_)},\n function _(t,e,n){\n /*! *****************************************************************************\n Copyright (c) Microsoft Corporation.\n \n Permission to use, copy, modify, and/or distribute this software for any\n purpose with or without fee is hereby granted.\n \n THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n PERFORMANCE OF THIS SOFTWARE.\n ***************************************************************************** */\n Object.defineProperty(n,\"__esModule\",{value:!0});var r=function(t,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,e)};function o(t){var e=\"function\"==typeof Symbol&&Symbol.iterator,n=e&&t[e],r=0;if(n)return n.call(t);if(t&&\"number\"==typeof t.length)return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(e?\"Object is not iterable.\":\"Symbol.iterator is not defined.\")}function a(t,e){var n=\"function\"==typeof Symbol&&t[Symbol.iterator];if(!n)return t;var r,o,a=n.call(t),i=[];try{for(;(void 0===e||e-- >0)&&!(r=a.next()).done;)i.push(r.value)}catch(t){o={error:t}}finally{try{r&&!r.done&&(n=a.return)&&n.call(a)}finally{if(o)throw o.error}}return i}function i(t){return this instanceof i?(this.v=t,this):new i(t)}n.__extends=function(t,e){function n(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)},n.__assign=function(){return n.__assign=Object.assign||function(t){for(var e,n=1,r=arguments.length;n=0;u--)(o=t[u])&&(i=(a<3?o(i):a>3?o(e,n,i):o(e,n))||i);return a>3&&i&&Object.defineProperty(e,n,i),i},n.__param=function(t,e){return function(n,r){e(n,r,t)}},n.__metadata=function(t,e){if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.metadata)return Reflect.metadata(t,e)},n.__awaiter=function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{c(r.next(t))}catch(t){a(t)}}function u(t){try{c(r.throw(t))}catch(t){a(t)}}function c(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,u)}c((r=r.apply(t,e||[])).next())}))},n.__generator=function(t,e){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},\"function\"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError(\"Generator is already executing.\");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==a[0]&&2!==a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]1||c(t,e)}))})}function c(t,e){try{(n=o[t](e)).value instanceof i?Promise.resolve(n.value.v).then(f,l):s(a[0][2],n)}catch(t){s(a[0][3],t)}var n}function f(t){c(\"next\",t)}function l(t){c(\"throw\",t)}function s(t,e){t(e),a.shift(),a.length&&c(a[0][0],a[0][1])}},n.__asyncDelegator=function(t){var e,n;return e={},r(\"next\"),r(\"throw\",(function(t){throw t})),r(\"return\"),e[Symbol.iterator]=function(){return this},e;function r(r,o){e[r]=t[r]?function(e){return(n=!n)?{value:i(t[r](e)),done:\"return\"===r}:o?o(e):e}:o}},n.__asyncValues=function(t){if(!Symbol.asyncIterator)throw new TypeError(\"Symbol.asyncIterator is not defined.\");var e,n=t[Symbol.asyncIterator];return n?n.call(t):(t=o(t),e={},r(\"next\"),r(\"throw\"),r(\"return\"),e[Symbol.asyncIterator]=function(){return this},e);function r(n){e[n]=t[n]&&function(e){return new Promise((function(r,o){(function(t,e,n,r){Promise.resolve(r).then((function(e){t({value:e,done:n})}),e)})(r,o,(e=t[n](e)).done,e.value)}))}}},n.__makeTemplateObject=function(t,e){return Object.defineProperty?Object.defineProperty(t,\"raw\",{value:e}):t.raw=e,t},n.__importStar=function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e.default=t,e},n.__importDefault=function(t){return t&&t.__esModule?t:{default:t}},n.__classPrivateFieldGet=function(t,e){if(!e.has(t))throw new TypeError(\"attempted to get private field on non-instance\");return e.get(t)},n.__classPrivateFieldSet=function(t,e,n){if(!e.has(t))throw new TypeError(\"attempted to set private field on non-instance\");return e.set(t,n),n}},\n function _(e,r,t){var l=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)Object.hasOwnProperty.call(e,t)&&(r[t]=e[t]);return r.default=e,r};Object.defineProperty(t,\"__esModule\",{value:!0});var o=e(3);t.version=o.version;var s=e(4);t.index=s.index,t.embed=l(e(4)),t.protocol=l(e(390)),t._testing=l(e(391));var n=e(19);t.logger=n.logger,t.set_log_level=n.set_log_level;var a=e(27);t.settings=a.settings;var i=e(7);t.Models=i.Models;var v=e(5);t.documents=v.documents;var _=e(392);t.safely=_.safely},\n function _(e,n,o){Object.defineProperty(o,\"__esModule\",{value:!0}),o.version=\"2.2.3\"},\n function _(e,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(5),s=e(19),r=e(29),d=e(13),_=e(8),c=e(16),i=e(381),a=e(383),u=e(382);var l=e(381);t.add_document_standalone=l.add_document_standalone,t.index=l.index;var m=e(383);t.add_document_from_session=m.add_document_from_session;var f=e(388);t.embed_items_notebook=f.embed_items_notebook,t.kernels=f.kernels;var g=e(382);async function O(e,o,t,c){_.isString(e)&&(e=JSON.parse(r.unescape(e)));const l={};for(const[o,t]of d.entries(e))l[o]=n.Document.from_json(t);const m=[];for(const e of o){const o=u._resolve_element(e),n=u._resolve_root_elements(e);if(null!=e.docid)m.push(await i.add_document_standalone(l[e.docid],o,n,e.use_for_title));else{if(null==e.token)throw new Error(\"Error rendering Bokeh items: either 'docid' or 'token' was expected.\");{const r=a._get_ws_url(t,c);s.logger.debug(\"embed: computed ws url: \"+r);try{m.push(await a.add_document_from_session(r,e.token,o,n,e.use_for_title)),console.log(\"Bokeh items were rendered successfully\")}catch(e){console.log(\"Error rendering Bokeh items:\",e)}}}}return m}t.BOKEH_ROOT=g.BOKEH_ROOT,t.embed_item=async function(e,o){const t={},n=r.uuid4();t[n]=e.doc,null==o&&(o=e.target_id);const s=document.getElementById(o);null!=s&&s.classList.add(u.BOKEH_ROOT);const d={roots:{[e.root_id]:o},root_ids:[e.root_id],docid:n},[_]=await c.defer(()=>O(t,[d]));return _},t.embed_items=async function(e,o,t,n){return await c.defer(()=>O(e,o,t,n))}},\n function _(e,t,_){Object.defineProperty(_,\"__esModule\",{value:!0});const o=e(1);o.__exportStar(e(6),_),o.__exportStar(e(121),_)},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const o=e(1),n=e(7),r=e(3),i=e(19),_=e(313),a=e(14),l=e(15),c=e(17),h=e(31),d=e(9),f=e(13),u=o.__importStar(e(120)),m=e(25),g=e(8),p=e(272),w=e(85),v=e(81),b=e(121);class y{constructor(e){this.document=e,this.session=null,this.subscribed_models=new Set}send_event(e){const t=new b.MessageSentEvent(this.document,\"bokeh_event\",e.to_json());this.document._trigger_on_change(t)}trigger(e){for(const t of this.subscribed_models)null!=e.origin&&e.origin!=t||t._process_event(e)}}s.EventManager=y,y.__name__=\"EventManager\",s.documents=[],s.DEFAULT_TITLE=\"Bokeh Application\";class j{constructor(){s.documents.push(this),this._init_timestamp=Date.now(),this._title=s.DEFAULT_TITLE,this._roots=[],this._all_models=new Map,this._all_models_freeze_count=0,this._callbacks=new Map,this._message_callbacks=new Map,this.event_manager=new y(this),this.idle=new l.Signal0(this,\"idle\"),this._idle_roots=new WeakMap,this._interactive_timestamp=null,this._interactive_plot=null}get layoutables(){return this._roots.filter(e=>e instanceof p.LayoutDOM)}get is_idle(){for(const e of this.layoutables)if(!this._idle_roots.has(e))return!1;return!0}notify_idle(e){this._idle_roots.set(e,!0),this.is_idle&&(i.logger.info(`document idle at ${Date.now()-this._init_timestamp} ms`),this.event_manager.send_event(new _.DocumentReady),this.idle.emit())}clear(){this._push_all_models_freeze();try{for(;this._roots.length>0;)this.remove_root(this._roots[0])}finally{this._pop_all_models_freeze()}}interactive_start(e){null==this._interactive_plot&&(this._interactive_plot=e,this._interactive_plot.trigger_event(new _.LODStart)),this._interactive_timestamp=Date.now()}interactive_stop(){null!=this._interactive_plot&&this._interactive_plot.trigger_event(new _.LODEnd),this._interactive_plot=null,this._interactive_timestamp=null}interactive_duration(){return null==this._interactive_timestamp?-1:Date.now()-this._interactive_timestamp}destructively_move(e){if(e===this)throw new Error(\"Attempted to overwrite a document with itself\");e.clear();const t=d.copy(this._roots);this.clear();for(const e of t)if(null!=e.document)throw new Error(\"Somehow we didn't detach \"+e);if(0!=this._all_models.size)throw new Error(\"this._all_models still had stuff in it: \"+this._all_models);for(const s of t)e.add_root(s);e.set_title(this._title)}_push_all_models_freeze(){this._all_models_freeze_count+=1}_pop_all_models_freeze(){this._all_models_freeze_count-=1,0===this._all_models_freeze_count&&this._recompute_all_models()}_invalidate_all_models(){i.logger.debug(\"invalidating document models\"),0===this._all_models_freeze_count&&this._recompute_all_models()}_recompute_all_models(){let e=new Set;for(const t of this._roots)e=u.union(e,t.references());const t=new Set(this._all_models.values()),s=u.difference(t,e),o=u.difference(e,t),n=new Map;for(const t of e)n.set(t.id,t);for(const e of s)e.detach_document();for(const e of o)e.attach_document(this);this._all_models=n}roots(){return this._roots}add_root(e,t){if(i.logger.debug(\"Adding root: \"+e),!d.includes(this._roots,e)){this._push_all_models_freeze();try{this._roots.push(e)}finally{this._pop_all_models_freeze()}this._trigger_on_change(new b.RootAddedEvent(this,e,t))}}remove_root(e,t){const s=this._roots.indexOf(e);if(!(s<0)){this._push_all_models_freeze();try{this._roots.splice(s,1)}finally{this._pop_all_models_freeze()}this._trigger_on_change(new b.RootRemovedEvent(this,e,t))}}title(){return this._title}set_title(e,t){e!==this._title&&(this._title=e,this._trigger_on_change(new b.TitleChangedEvent(this,e,t)))}get_model_by_id(e){var t;return null!==(t=this._all_models.get(e))&&void 0!==t?t:null}get_model_by_name(e){const t=[];for(const s of this._all_models.values())s instanceof v.Model&&s.name==e&&t.push(s);switch(t.length){case 0:return null;case 1:return t[0];default:throw new Error(`Multiple models are named '${e}'`)}}on_message(e,t){const s=this._message_callbacks.get(e);null==s?this._message_callbacks.set(e,new Set([t])):s.add(t)}remove_on_message(e,t){var s;null===(s=this._message_callbacks.get(e))||void 0===s||s.delete(t)}_trigger_on_message(e,t){const s=this._message_callbacks.get(e);if(null!=s)for(const e of s)e(t)}on_change(e,t=!1){this._callbacks.has(e)||this._callbacks.set(e,t)}remove_on_change(e){this._callbacks.delete(e)}_trigger_on_change(e){for(const[t,s]of this._callbacks)if(!s&&e instanceof b.DocumentEventBatch)for(const s of e.events)t(s);else t(e)}_notify_change(e,t,s,o,n){this._trigger_on_change(new b.ModelChangedEvent(this,e,t,s,o,null==n?void 0:n.setter_id,null==n?void 0:n.hint))}static _references_json(e,t=!0){const s=[];for(const o of e){const e=o.struct();e.attributes=o.attributes_as_json(t),delete e.attributes.id,s.push(e)}return s}static _instantiate_object(e,t,s){const o=Object.assign(Object.assign({},s),{id:e,__deferred__:!0});return new(n.Models(t))(o)}static _instantiate_references_json(e,t){const s=new Map;for(const o of e){const e=o.id,n=o.type,r=o.attributes||{};let i=t.get(e);null==i&&(i=j._instantiate_object(e,n,r),null!=o.subtype&&i.set_subtype(o.subtype)),s.set(i.id,i)}return s}static _resolve_refs(e,t,s,o){function n(e){if(c.is_ref(e)){if(t.has(e.id))return t.get(e.id);if(s.has(e.id))return s.get(e.id);throw new Error(`reference ${JSON.stringify(e)} isn't known (not in Document?)`)}return h.is_NDArray_ref(e)?h.decode_NDArray(e,o):g.isArray(e)?function(e){const t=[];for(const s of e)t.push(n(s));return t}(e):g.isPlainObject(e)?function(e){const t={};for(const[s,o]of f.entries(e))t[s]=n(o);return t}(e):e}return n(e)}static _initialize_references_json(e,t,s,o){const n=new Map;for(const{id:r,attributes:i}of e){const e=!t.has(r),_=e?s.get(r):t.get(r),a=j._resolve_refs(i,t,s,o);_.setv(a,{silent:!0}),n.set(r,{instance:_,is_new:e})}const r=[],i=new Set;function _(e){if(e instanceof a.HasProps){if(n.has(e.id)&&!i.has(e.id)){i.add(e.id);const{instance:t,is_new:s}=n.get(e.id),{attributes:o}=t;for(const e of f.values(o))_(e);s&&(t.finalize(),r.push(t))}}else if(g.isArray(e))for(const t of e)_(t);else if(g.isPlainObject(e))for(const t of f.values(e))_(t)}for(const e of n.values())_(e.instance);for(const e of r)e.connect_signals()}static _event_for_attribute_change(e,t,s,o,n){if(o.get_model_by_id(e.id).property(t).syncable){const r={kind:\"ModelChanged\",model:{id:e.id},attr:t,new:s};return a.HasProps._json_record_references(o,s,n,{recursive:!0}),r}return null}static _events_to_sync_objects(e,t,s,o){const n=Object.keys(e.attributes),r=Object.keys(t.attributes),_=d.difference(n,r),a=d.difference(r,n),l=d.intersection(n,r),c=[];for(const e of _)i.logger.warn(`Server sent key ${e} but we don't seem to have it in our JSON`);for(const n of a){const r=t.attributes[n];c.push(j._event_for_attribute_change(e,n,r,s,o))}for(const n of l){const r=e.attributes[n],i=t.attributes[n];null==r&&null==i||(null==r||null==i?c.push(j._event_for_attribute_change(e,n,i,s,o)):m.isEqual(r,i)||c.push(j._event_for_attribute_change(e,n,i,s,o)))}return c.filter(e=>null!=e)}static _compute_patch_since_json(e,t){const s=t.to_json(!1);function o(e){const t=new Map;for(const s of e.roots.references)t.set(s.id,s);return t}const n=o(e),r=new Map,i=[];for(const t of e.roots.root_ids)r.set(t,n.get(t)),i.push(t);const _=o(s),a=new Map,l=[];for(const e of s.roots.root_ids)a.set(e,_.get(e)),l.push(e);if(i.sort(),l.sort(),d.difference(i,l).length>0||d.difference(l,i).length>0)throw new Error(\"Not implemented: computing add/remove of document roots\");const c=new Set;let h=[];for(const e of t._all_models.keys())if(n.has(e)){const s=j._events_to_sync_objects(n.get(e),_.get(e),t,c);h=h.concat(s)}return{references:j._references_json(c,!1),events:h}}to_json_string(e=!0){return JSON.stringify(this.to_json(e))}to_json(e=!0){const t=this._roots.map(e=>e.id),s=this._all_models.values();return{version:r.version,title:this._title,roots:{root_ids:t,references:j._references_json(s,e)}}}static from_json_string(e){const t=JSON.parse(e);return j.from_json(t)}static from_json(e){i.logger.debug(\"Creating Document from JSON\");const t=e.version,s=-1!==t.indexOf(\"+\")||-1!==t.indexOf(\"-\"),o=`Library versions: JS (${r.version}) / Python (${t})`;s||r.version.replace(/-(dev|rc)\\./,\"$1\")==t?i.logger.debug(o):(i.logger.warn(\"JS/Python version mismatch\"),i.logger.warn(o));const n=e.roots,_=n.root_ids,a=n.references,l=j._instantiate_references_json(a,new Map);j._initialize_references_json(a,new Map,l,new Map);const c=new j;for(const e of _){const t=l.get(e);null!=t&&c.add_root(t)}return c.set_title(e.title),c}replace_with_json(e){j.from_json(e).destructively_move(this)}create_json_patch_string(e){return JSON.stringify(this.create_json_patch(e))}create_json_patch(e){const t=new Set,s=[];for(const o of e){if(o.document!==this)throw i.logger.warn(\"Cannot create a patch using events from a different document, event had \",o.document,\" we are \",this),new Error(\"Cannot create a patch using events from a different document\");s.push(o.json(t))}return{events:s,references:j._references_json(t)}}apply_json_patch(e,t=new Map,s){const o=e.references,n=e.events,r=j._instantiate_references_json(o,this._all_models);t instanceof Map||(t=new Map(t));for(const e of n)switch(e.kind){case\"RootAdded\":case\"RootRemoved\":case\"ModelChanged\":{const t=e.model.id,s=this._all_models.get(t);if(null!=s)r.set(t,s);else if(!r.has(t))throw i.logger.warn(`Got an event for unknown model ${e.model}\"`),new Error(\"event model wasn't known\");break}}const _=new Map,a=new Map;for(const[e,t]of r)this._all_models.has(e)?_.set(e,t):a.set(e,t);j._initialize_references_json(o,_,a,t);for(const e of n)switch(e.kind){case\"MessageSent\":{const{msg_type:s,msg_data:o}=e;let n;if(void 0===o){if(1!=t.size)throw new Error(\"expected exactly one buffer\");{const[[,e]]=t;n=e}}else n=j._resolve_refs(o,_,a,t);this._trigger_on_message(s,n);break}case\"ModelChanged\":{const o=e.model.id,n=this._all_models.get(o);if(null==n)throw new Error(`Cannot apply patch to ${o} which is not in the document`);const r=e.attr,i=j._resolve_refs(e.new,_,a,t);n.setv({[r]:i},{setter_id:s});break}case\"ColumnDataChanged\":{const o=e.column_source.id,n=this._all_models.get(o);if(null==n)throw new Error(`Cannot stream to ${o} which is not in the document`);const r=j._resolve_refs(e.new,new Map,new Map,t);if(null!=e.cols)for(const e in n.data)e in r||(r[e]=n.data[e]);n.setv({data:r},{setter_id:s,check_eq:!1});break}case\"ColumnsStreamed\":{const t=e.column_source.id,o=this._all_models.get(t);if(null==o)throw new Error(`Cannot stream to ${t} which is not in the document`);if(!(o instanceof w.ColumnDataSource))throw new Error(\"Cannot stream to non-ColumnDataSource\");const n=e.data,r=e.rollover;o.stream(n,r,s);break}case\"ColumnsPatched\":{const t=e.column_source.id,o=this._all_models.get(t);if(null==o)throw new Error(`Cannot patch ${t} which is not in the document`);if(!(o instanceof w.ColumnDataSource))throw new Error(\"Cannot patch non-ColumnDataSource\");const n=e.patches;o.patch(n,s);break}case\"RootAdded\":{const t=e.model.id,o=r.get(t);this.add_root(o,s);break}case\"RootRemoved\":{const t=e.model.id,o=r.get(t);this.remove_root(o,s);break}case\"TitleChanged\":this.set_title(e.title,s);break;default:throw new Error(\"Unknown patch event \"+JSON.stringify(e))}}}s.Document=j,j.__name__=\"Document\"},\n function _(e,r,s){Object.defineProperty(s,\"__esModule\",{value:!0});const o=e(1),t=e(8),d=e(13),i=e(14);s.overrides={};const l=new Map;s.Models=e=>{const r=s.overrides[e]||l.get(e);if(null==r)throw new Error(`Model '${e}' does not exist. This could be due to a widget or a custom model not being registered before first usage.`);return r},s.Models.register=(e,r)=>{s.overrides[e]=r},s.Models.unregister=e=>{delete s.overrides[e]},s.Models.register_models=(e,r=!1,s)=>{var o;if(null!=e)for(const n of d.values(e))if(o=n,t.isObject(o)&&o.prototype instanceof i.HasProps){const e=n.__qualified__;r||!l.has(e)?l.set(e,n):null!=s?s(e):console.warn(`Model '${e}' was already registered`)}},s.register_models=s.Models.register_models,s.Models.registered_names=()=>Array.from(l.keys());const n=o.__importStar(e(34));s.register_models(n)},\n function _(n,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});\n // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n // Underscore may be freely distributed under the MIT license.\n const e=n(9),i=Object.prototype.toString;function o(n){return\"[object Number]\"===i.call(n)}function c(n){const t=typeof n;return\"function\"===t||\"object\"===t&&!!n}r.isBoolean=function(n){return!0===n||!1===n||\"[object Boolean]\"===i.call(n)},r.isNumber=o,r.isInteger=function(n){return o(n)&&Number.isInteger(n)},r.isString=function(n){return\"[object String]\"===i.call(n)},r.isFunction=function(n){return\"[object Function]\"===i.call(n)},r.isArray=function(n){return Array.isArray(n)},r.isArrayOf=function(n,t){return e.every(n,t)},r.isArrayableOf=function(n,t){for(let r=0,e=n.length;r0,\"'step' must be a positive number\"),null==t&&(t=n,n=0);const{max:r,ceil:i,abs:u}=Math,c=n<=t?e:-e,f=r(i(u(t-n)/e),0),s=new Array(f);for(let t=0;t=0?t:n.length+t]},e.zip=function(...n){if(0==n.length)return[];const t=i.min(n.map(n=>n.length)),e=n.length,r=new Array(t);for(let o=0;on.length)),r=Array(e);for(let n=0;nn[t])},e.argmax=function(n){return i.max_by(a(n.length),t=>n[t])},e.sort_by=function(n,t){const e=n.map((n,e)=>({value:n,index:e,key:t(n)}));return e.sort((n,t)=>{const e=n.key,r=t.key;if(e!==r){if(e>r||void 0===e)return 1;if(en.value)},e.uniq=function(n){const t=new Set;for(const e of n)t.add(e);return[...t]},e.uniq_by=function(n,t){const e=[],r=[];for(const o of n){const n=t(o);s(r,n)||(r.push(n),e.push(o))}return e},e.union=function(...n){const t=new Set;for(const e of n)for(const n of e)t.add(n);return[...t]},e.intersection=function(n,...t){const e=[];n:for(const r of n)if(!s(e,r)){for(const n of t)if(!s(n,r))continue n;e.push(r)}return e},e.difference=function(n,...t){const e=f(t);return n.filter(n=>!s(e,n))},e.remove_at=function(n,t){const e=c(n);return e.splice(t,1),e},e.remove_by=function(n,t){for(let e=0;e2*Math.PI;)n-=2*Math.PI;return n}function a(n,t){return e(n-t)}function o(){return Math.random()}Object.defineProperty(r,\"__esModule\",{value:!0}),r.angle_norm=e,r.angle_dist=a,r.angle_between=function(n,t,r,o){const u=a(t,r);if(0==u)return!1;if(u==2*Math.PI)return!0;const f=e(n),i=a(t,f)<=u&&a(f,r)<=u;return 0==o?i:!i},r.random=o,r.randomIn=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},r.atan2=function(n,t){return Math.atan2(t[1]-n[1],t[0]-n[0])},r.radians=function(n){return n*(Math.PI/180)},r.degrees=function(n){return n/(Math.PI/180)},r.rnorm=function(n,t){let r,e;for(;r=o(),e=o(),e=(2*e-1)*Math.sqrt(1/Math.E*2),!(-4*r*r*Math.log(r)>=e*e););let a=e/r;return a=n+t*a,a},r.clamp=function(n,t,r){return nr?r:n}},\n function _(e,r,n){Object.defineProperty(n,\"__esModule\",{value:!0});class o extends Error{}n.AssertionError=o,o.__name__=\"AssertionError\",n.assert=function(e,r){if(!(!0===e||!1!==e&&e()))throw new o(null!=r?r:\"Assertion failed\")},n.unreachable=function(){throw new Error(\"unreachable code\")}},\n function _(n,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const r=n(8),o=n(10);function i(n,t,e,...r){const o=n.length;t<0&&(t+=o),t<0?t=0:t>o&&(t=o),null==e||e>o-t?e=o-t:e<0&&(e=0);const i=o-e+r.length,u=new n.constructor(i);let l=0;for(;l0?0:r-1;for(;o>=0&&ot[t.length-1])return t.length;let e=0,r=t.length-1;for(;r-e!=1;){const o=e+Math.floor((r-e)/2);n>=t[o]?e=o:r=o}return e}e.is_empty=function(n){return 0==n.length},e.copy=function(n){return r.isArray(n)?n.slice():new n.constructor(n)},e.splice=i,e.head=u,e.insert=function(n,t,e){return i(n,e,0,t)},e.append=function(n,t){return i(n,n.length,0,t)},e.prepend=function(n,t){return i(n,0,0,t)},e.indexOf=function(n,t){for(let e=0,r=n.length;ee&&(e=t);return e},e.minmax=function(n){let t,e=1/0,r=-1/0;for(let o=0,i=n.length;or&&(r=t));return[e,r]},e.min_by=function(n,t){if(0==n.length)throw new Error(\"min_by() called with an empty array\");let e=n[0],r=t(e);for(let o=1,i=n.length;or&&(e=i,r=u)}return e},e.sum=function(n){let t=0;for(let e=0,r=n.length;et[r]=n+e,0),t},e.every=function(n,t){for(let e=0,r=n.length;e(n-t)/r)}},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const c=e(9);function o(e){return Object.keys(e).length}n.keys=Object.keys,n.values=Object.values,n.entries=Object.entries,n.extend=Object.assign,n.clone=function(e){return Object.assign({},e)},n.merge=function(e,t){const n=Object.create(Object.prototype),o=c.concat([Object.keys(e),Object.keys(t)]);for(const s of o){const o=e.hasOwnProperty(s)?e[s]:[],r=t.hasOwnProperty(s)?t[s]:[];n[s]=c.union(o,r)}return n},n.size=o,n.isEmpty=function(e){return 0==o(e)},n.to_object=function(e){const t={};for(const[n,c]of e)t[n]=c;return t}},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const s=t(1),n=t(15),i=t(17),o=s.__importStar(t(18)),c=s.__importStar(t(21)),a=s.__importStar(t(28)),_=t(29),u=t(9),f=t(13),l=t(8),h=t(25),p=t(5),d=t(30),y=t(31),g=t(25),v=t(33),m=s.__importStar(t(21));class b extends(n.Signalable()){constructor(t={}){var e;super(),this._subtype=void 0,this.document=null,this.destroyed=new n.Signal0(this,\"destroyed\"),this.change=new n.Signal0(this,\"change\"),this.transformchange=new n.Signal0(this,\"transformchange\"),this.properties={},this._pending=!1,this._changing=!1;const r=t instanceof Map?t.get:e=>t[e];for(const[t,{type:e,default_value:s,options:n}]of f.entries(this._props)){let i;i=e instanceof c.Kind?new o.PrimitiveProperty(this,t,e,s,r(t),n):new e(this,t,c.Any,s,r(t),n),this.properties[t]=i}null!==(e=r(\"__deferred__\"))&&void 0!==e&&e||(this.finalize(),this.connect_signals())}set type(t){console.warn(\"prototype.type = 'ModelName' is deprecated, use static __name__ instead\"),this.constructor.__name__=t}get type(){return this.constructor.__qualified__}static get __qualified__(){const{__module__:t,__name__:e}=this;return null!=t?`${t}.${e}`:e}static get[Symbol.toStringTag](){return this.__name__}static init_HasProps(){this.prototype._props={},this.prototype._mixins=[],this.define({id:[o.String,()=>_.uniqueId()]})}static _fix_default(t,e){if(void 0!==t){if(l.isFunction(t))return t;if(l.isArray(t))return()=>u.copy(t);if(l.isPlainObject(t))return()=>f.clone(t);if(l.isObject(t))throw new Error(t+\" must be explicitly wrapped in a function\");return()=>t}}static define(t){for(const[e,r]of f.entries(l.isFunction(t)?t(m):t)){if(null!=this.prototype._props[e])throw new Error(`attempted to redefine property '${this.prototype.type}.${e}'`);if(null!=this.prototype[e])throw new Error(`attempted to redefine attribute '${this.prototype.type}.${e}'`);Object.defineProperty(this.prototype,e,{get(){return this.properties[e].get_value()},set(t){return this.setv({[e]:t}),this},configurable:!1,enumerable:!0});const[t,s,n]=r,i={type:t,default_value:this._fix_default(s,e),options:n},o=f.clone(this.prototype._props);o[e]=i,this.prototype._props=o}}static internal(t){const e={};for(const[r,s]of f.entries(t)){const[t,n,i={}]=s;e[r]=[t,n,Object.assign(Object.assign({},i),{internal:!0})]}this.define(e)}static mixins(t){function e(t){switch(t){case\"line\":return a.LineVector;case\"fill\":return a.FillVector;case\"hatch\":return a.HatchVector;case\"text\":return a.TextVector;default:throw new Error(`Unknown property mixin kind '${t}'`)}}function r(t,e){const r={};for(const[s,n]of f.entries(e))r[t+s]=n;return r}function s(t){const[e]=Object.keys(t),[r]=e.split(\"_\",1);return r}l.isArray(t)||(t=[t]);const n={},i=[];for(const o of t)if(l.isString(o)){const[t,s=\"\"]=o.split(\":\"),c=e(t);i.push(o),f.extend(n,r(s,c))}else if(l.isArray(o)){const[t,e]=o;i.push(`${s(e)}:${t}`),f.extend(n,r(t,e))}else{const t=o;i.push(s(t)),f.extend(n,t)}this.define(n),this.prototype._mixins=[...this.prototype._mixins,...i]}static override(t){for(const[e,r]of f.entries(t)){const t=this._fix_default(r,e),s=this.prototype._props[e];if(null==s)throw new Error(`attempted to override nonexistent '${this.prototype.type}.${e}'`);const n=f.clone(this.prototype._props);n[e]=Object.assign(Object.assign({},s),{default_value:t}),this.prototype._props=n}}toString(){return`${this.type}(${this.id})`}property(t){const e=this.properties[t];if(null!=e)return e;throw new Error(`unknown property ${this.type}.${t}`)}get attributes(){const t={};for(const e of this)t[e.attr]=e.get_value();return t}[g.equals](t,e){for(const r of this){const s=t.property(r.attr);if(e.eq(r.get_value(),s.get_value()))return!1}return!0}[v.pretty](t){const e=t.token,r=[];for(const s of this)if(s.dirty){const n=s.get_value();r.push(`${s.attr}${e(\":\")} ${t.to_string(n)}`)}return`${this.constructor.__qualified__}${e(\"(\")}${e(\"{\")}${r.join(e(\",\")+\" \")}${e(\"}\")}${e(\")\")}`}finalize(){for(const t of this)null!=t.spec.transform&&this.connect(t.spec.transform.change,()=>this.transformchange.emit());this.initialize()}initialize(){}connect_signals(){}disconnect_signals(){n.Signal.disconnectReceiver(this)}destroy(){this.disconnect_signals(),this.destroyed.emit()}clone(){return new this.constructor(this.attributes)}_setv(t,e){const r=e.check_eq,s=[],n=this._changing;this._changing=!0;for(const[e,n]of t)!1!==r&&h.isEqual(e.get_value(),n)||(e.set_value(n),s.push(e));s.length>0&&(this._pending=!0);for(const t of s)t.change.emit();if(!n){if(!e.no_change)for(;this._pending;)this._pending=!1,this.change.emit();this._pending=!1,this._changing=!1}}setv(t,e={}){const r=f.entries(t);if(0==r.length)return;if(!0===e.silent){for(const[t,e]of r)this.properties[t].set_value(e);return}const s=new Map,n=new Map;for(const[t,e]of r){const r=this.properties[t];s.set(r,e),n.set(r,r.get_value())}this._setv(s,e);const{document:i}=this;if(null!=i){const t=[];for(const[e,r]of n)t.push([e,r,e.get_value()]);for(const[,e,r]of t)if(this._needs_invalidate(e,r)){i._invalidate_all_models();break}this._push_changes(t,e)}}getv(t){return this.property(t).get_value()}ref(){return{id:this.id}}struct(){const t={type:this.type,id:this.id,attributes:{}};return null!=this._subtype&&(t.subtype=this._subtype),t}set_subtype(t){this._subtype=t}*[Symbol.iterator](){yield*f.values(this.properties)}*syncable_properties(){for(const t of this)t.syncable&&(yield t)}serializable_attributes(){const t={};for(const e of this.syncable_properties())t[e.attr]=e.get_value();return t}static _value_to_json(t){if(t instanceof b)return t.ref();if(d.is_NDArray(t))return y.encode_NDArray(t);if(l.isArray(t)||l.isTypedArray(t)){const e=t.length,r=new Array(e);for(let s=0;sn.signal===t&&n.slot===e&&n.context===l)}const g=new Set;function a(n){0===g.size&&l.defer(f),g.add(n)}function f(){for(const n of g)s.remove_by(n,n=>null==n.signal);g.clear()}},\n function _(n,e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.delay=\n // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n // Underscore may be freely distributed under the MIT license.\n function(n,e){return setTimeout(n,e)};const u=\"function\"==typeof requestAnimationFrame?requestAnimationFrame:setImmediate;t.defer=function(n){return new Promise(e=>{u(()=>e(n()))})},t.throttle=function(n,e,t={}){let u,o,i,r=null,l=0;const c=function(){l=!1===t.leading?0:Date.now(),r=null,i=n.apply(u,o),r||(u=o=null)};return function(){const a=Date.now();l||!1!==t.leading||(l=a);const f=e-(a-l);return u=this,o=arguments,f<=0||f>e?(r&&(clearTimeout(r),r=null),l=a,i=n.apply(u,o),r||(u=o=null)):r||!1===t.trailing||(r=setTimeout(c,f)),i}},t.once=function(n){let e,t=!1;return function(){return t||(t=!0,e=n()),e}}},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});const i=e(8),r=e(13);t.is_ref=function(e){if(i.isPlainObject(e)){const n=r.keys(e);return 1==n.length&&\"id\"==n[0]}return!1}},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const a=e(1),s=e(15),i=e(19),r=a.__importStar(e(20)),l=e(24),o=e(9),c=e(12),_=e(22),u=e(8),d=e(27);function p(e){try{return JSON.stringify(e)}catch(t){return e.toString()}}function S(e){return u.isPlainObject(e)&&(void 0===e.value?0:1)+(void 0===e.field?0:1)+(void 0===e.expr?0:1)==1}n.isSpec=S;class m{constructor(e,t,n,a,i,r={}){var l,o;let c;if(this.obj=e,this.attr=t,this.kind=n,this.default_value=a,this._dirty=!1,this.change=new s.Signal0(this.obj,\"change\"),this.internal=null!==(l=r.internal)&&void 0!==l&&l,this.optional=null!==(o=r.optional)&&void 0!==o&&o,void 0!==i)c=i,this._dirty=!0;else{const t=this._default_override();c=void 0!==t?t:void 0!==a?a(e):null}this._update(c)}get is_value(){return void 0!==this.spec.value}get syncable(){return!this.internal}get_value(){return this.spec.value}set_value(e){this._update(e),this._dirty=!0}_default_override(){}get dirty(){return this._dirty}_update(e){null!=e&&this.validate(e),this.spec={value:e}}toString(){return`Prop(${this.obj}.${this.attr}, spec: ${p(this.spec)})`}normalize(e){return e}validate(e){if(!this.valid(e))throw new Error(`${this.obj.type}.${this.attr} given invalid value: ${p(e)}`)}valid(e){return this.kind.valid(e)}value(e=!0){if(!this.is_value)throw new Error(\"attempted to retrieve property value for property without value specification\");let t=this.normalize([this.spec.value])[0];return null!=this.spec.transform&&e&&(t=this.spec.transform.compute(t)),t}}n.Property=m,m.__name__=\"Property\";class h extends m{}n.PrimitiveProperty=h,h.__name__=\"PrimitiveProperty\";class v extends m{}n.Any=v,v.__name__=\"Any\";class g extends m{valid(e){return u.isArray(e)||e instanceof Float32Array||e instanceof Float64Array}}n.Array=g,g.__name__=\"Array\";class x extends m{valid(e){return u.isBoolean(e)}}n.Boolean=x,x.__name__=\"Boolean\";class y extends m{valid(e){return u.isString(e)&&_.is_color(e)}}n.Color=y,y.__name__=\"Color\";class f extends m{}n.Instance=f,f.__name__=\"Instance\";class A extends m{valid(e){return u.isNumber(e)}}n.Number=A,A.__name__=\"Number\";class P extends A{valid(e){return u.isNumber(e)&&(0|e)==e}}n.Int=P,P.__name__=\"Int\";class C extends A{}n.Angle=C,C.__name__=\"Angle\";class b extends A{valid(e){return u.isNumber(e)&&0<=e&&e<=1}}n.Percent=b,b.__name__=\"Percent\";class L extends m{valid(e){return u.isString(e)}}n.String=L,L.__name__=\"String\";class N extends m{valid(e){return null===e||u.isString(e)}}n.NullString=N,N.__name__=\"NullString\";class T extends L{}n.FontSize=T,T.__name__=\"FontSize\";class q extends L{_default_override(){return d.settings.dev?\"Bokeh\":void 0}}n.Font=q,q.__name__=\"Font\";class B extends m{valid(e){return u.isString(e)&&o.includes(this.enum_values,e)}}function M(e){return class extends B{get enum_values(){return[...e]}}}n.EnumProperty=B,B.__name__=\"EnumProperty\",n.Enum=M;class w extends B{get enum_values(){return[...r.Direction]}normalize(e){const t=new Uint8Array(e.length);for(let n=0;ne*Math.PI/180)),e=c.map(e,e=>-e),super.normalize(e)}}n.AngleSpec=re,re.__name__=\"AngleSpec\";class le extends G{get default_units(){return\"data\"}get valid_units(){return[...r.SpatialUnits]}}n.DistanceSpec=le,le.__name__=\"DistanceSpec\";class oe extends J{array(e){return new Uint8Array(super.array(e))}}n.BooleanSpec=oe,oe.__name__=\"BooleanSpec\";class ce extends J{array(e){return new l.NumberArray(super.array(e))}}n.NumberSpec=ce,ce.__name__=\"NumberSpec\";class _e extends J{array(e){const t=super.array(e),n=t.length,a=new l.ColorArray(n);for(let e=0;e0){let o=s[e];return null==o&&(s[e]=o=new r(e,l)),o}throw new TypeError(\"Logger.get() expects a non-empty string name and an optional log-level\")}get level(){return this.get_level()}get_level(){return this._log_level}set_level(e){if(e instanceof g)this._log_level=e;else{if(!n.isString(e)||null==r.log_levels[e])throw new Error(\"Logger.set_level() expects a log-level object or a string name of a log-level\");this._log_level=r.log_levels[e]}const l=`[${this._name}]`;for(const[e,o]of t.entries(r.log_levels))o.level\",\"*\"),t.HTTPMethod=o.Enum(\"POST\",\"GET\"),t.HexTileOrientation=o.Enum(\"pointytop\",\"flattop\"),t.HoverMode=o.Enum(\"mouse\",\"hline\",\"vline\"),t.LatLon=o.Enum(\"lat\",\"lon\"),t.LegendClickPolicy=o.Enum(\"none\",\"hide\",\"mute\"),t.LegendLocation=t.Anchor,t.LineCap=o.Enum(\"butt\",\"round\",\"square\"),t.LineJoin=o.Enum(\"miter\",\"round\",\"bevel\"),t.LinePolicy=o.Enum(\"prev\",\"next\",\"nearest\",\"interp\",\"none\"),t.Location=o.Enum(\"above\",\"below\",\"left\",\"right\"),t.Logo=o.Enum(\"normal\",\"grey\"),t.MarkerType=o.Enum(\"asterisk\",\"circle\",\"circle_cross\",\"circle_dot\",\"circle_x\",\"circle_y\",\"cross\",\"dash\",\"diamond\",\"diamond_cross\",\"diamond_dot\",\"dot\",\"hex\",\"hex_dot\",\"inverted_triangle\",\"plus\",\"square\",\"square_cross\",\"square_dot\",\"square_pin\",\"square_x\",\"triangle\",\"triangle_dot\",\"triangle_pin\",\"x\",\"y\"),t.MutedPolicy=o.Enum(\"show\",\"ignore\"),t.Orientation=o.Enum(\"vertical\",\"horizontal\"),t.OutputBackend=o.Enum(\"canvas\",\"svg\",\"webgl\"),t.PaddingUnits=o.Enum(\"percent\",\"absolute\"),t.Place=o.Enum(\"above\",\"below\",\"left\",\"right\",\"center\"),t.PointPolicy=o.Enum(\"snap_to_data\",\"follow_mouse\",\"none\"),t.RadiusDimension=o.Enum(\"x\",\"y\",\"max\",\"min\"),t.RenderLevel=o.Enum(\"image\",\"underlay\",\"glyph\",\"guide\",\"annotation\",\"overlay\"),t.RenderMode=o.Enum(\"canvas\",\"css\"),t.ResetPolicy=o.Enum(\"standard\",\"event_only\"),t.RoundingFunction=o.Enum(\"round\",\"nearest\",\"floor\",\"rounddown\",\"ceil\",\"roundup\"),t.SelectionMode=o.Enum(\"replace\",\"append\",\"intersect\",\"subtract\"),t.Side=o.Enum(\"above\",\"below\",\"left\",\"right\"),t.SizingMode=o.Enum(\"stretch_width\",\"stretch_height\",\"stretch_both\",\"scale_width\",\"scale_height\",\"scale_both\",\"fixed\"),t.Sort=o.Enum(\"ascending\",\"descending\"),t.SpatialUnits=o.Enum(\"screen\",\"data\"),t.StartEnd=o.Enum(\"start\",\"end\"),t.StepMode=o.Enum(\"after\",\"before\",\"center\"),t.TapBehavior=o.Enum(\"select\",\"inspect\"),t.TextAlign=o.Enum(\"left\",\"right\",\"center\"),t.TextBaseline=o.Enum(\"top\",\"middle\",\"bottom\",\"alphabetic\",\"hanging\",\"ideographic\"),t.TextureRepetition=o.Enum(\"repeat\",\"repeat_x\",\"repeat_y\",\"no_repeat\"),t.TickLabelOrientation=o.Enum(\"vertical\",\"horizontal\",\"parallel\",\"normal\"),t.TooltipAttachment=o.Enum(\"horizontal\",\"vertical\",\"left\",\"right\",\"above\",\"below\"),t.UpdateMode=o.Enum(\"replace\",\"append\"),t.VerticalAlign=o.Enum(\"top\",\"middle\",\"bottom\")},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=e(1).__importStar(e(8)),r=e(22);class i{}t.Kind=i,i.__name__=\"Kind\",function(e){class n extends i{valid(e){return!0}}n.__name__=\"Any\",e.Any=n;class t extends i{valid(e){return!0}}t.__name__=\"Unknown\",e.Unknown=t;class l extends i{valid(e){return s.isBoolean(e)}}l.__name__=\"Boolean\",e.Boolean=l;class a extends i{constructor(e){super(),this.obj_type=e}valid(e){return!0}}a.__name__=\"Ref\",e.Ref=a;class _ extends i{valid(e){return s.isNumber(e)}}_.__name__=\"Number\",e.Number=_;class u extends _{valid(e){return super.valid(e)&&s.isInteger(e)}}u.__name__=\"Int\",e.Int=u;class d extends i{constructor(e){super(),this.types=e,this.types=e}valid(e){return this.types.some(n=>n.valid(e))}}d.__name__=\"Or\",e.Or=d;class o extends i{constructor(e){super(),this.types=e,this.types=e}valid(e){if(!s.isArray(e))return!1;for(let n=0;nthis.item_type.valid(e))}}c.__name__=\"Array\",e.Array=c;class m extends i{valid(e){return null===e}}m.__name__=\"Null\",e.Null=m;class p extends i{constructor(e){super(),this.base_type=e}valid(e){return null===e||this.base_type.valid(e)}}p.__name__=\"Nullable\",e.Nullable=p;class y extends i{valid(e){return s.isString(e)}}y.__name__=\"String\",e.String=y;class v extends i{constructor(e){super(),this.values=new Set(e)}valid(e){return this.values.has(e)}*[Symbol.iterator](){yield*this.values}}v.__name__=\"Enum\",e.Enum=v;class h extends i{constructor(e){super(),this.item_type=e}valid(e){if(!s.isPlainObject(e))return!1;for(const n in e)if(e.hasOwnProperty(n)){const t=e[n];if(!this.item_type.valid(t))return!1}return!0}}h.__name__=\"Struct\",e.Struct=h;class w extends i{constructor(e,n){super(),this.key_type=e,this.item_type=n}valid(e){if(!(e instanceof Map))return!1;for(const[n,t]of e.entries())if(!this.key_type.valid(n)||!this.item_type.valid(t))return!1;return!0}}w.__name__=\"Dict\",e.Dict=w;class K extends i{valid(e){return s.isString(e)&&r.is_color(e)}}K.__name__=\"Color\",e.Color=K;class f extends _{valid(e){return super.valid(e)&&0<=e&&e<=1}}f.__name__=\"Percent\",e.Percent=f}(t.Kinds||(t.Kinds={})),t.Any=new t.Kinds.Any,t.Unknown=new t.Kinds.Unknown,t.Boolean=new t.Kinds.Boolean,t.Number=new t.Kinds.Number,t.Int=new t.Kinds.Int,t.String=new t.Kinds.String,t.Null=new t.Kinds.Null,t.Nullable=e=>new t.Kinds.Nullable(e),t.Or=(...e)=>new t.Kinds.Or(e),t.Tuple=(...e)=>new t.Kinds.Tuple(e),t.Array=e=>new t.Kinds.Array(e),t.Struct=e=>new t.Kinds.Struct(e),t.Dict=(e,n)=>new t.Kinds.Dict(e,n),t.Enum=(...e)=>new t.Kinds.Enum(e),t.Ref=e=>new t.Kinds.Ref(e),t.Percent=new t.Kinds.Percent,t.Color=new t.Kinds.Color,t.Auto=t.Enum(\"auto\"),t.FontSize=t.String,t.Font=t.String,t.Angle=t.Number},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(23),l=e(9);function a(e){const r=Number(e).toString(16);return 1==r.length?\"0\"+r:r}function o(e){if(0==(e+=\"\").indexOf(\"#\"))return e;if(n.is_svg_color(e))return n.svg_colors[e];if(0==e.indexOf(\"rgb\")){const r=e.replace(/^rgba?\\(|\\s+|\\)$/g,\"\").split(\",\");let t=r.slice(0,3).map(a).join(\"\");return 4==r.length&&(t+=a(Math.floor(255*parseFloat(r[3])))),\"#\"+t.slice(0,8)}return e}function s(e){let r;switch(e.substring(0,4)){case\"rgba\":r={start:\"rgba(\",len:4,alpha:!0};break;case\"rgb(\":r={start:\"rgb(\",len:3,alpha:!1};break;default:return!1}if(new RegExp(\".*?(\\\\.).*(,)\").test(e))return!1;const t=e.replace(r.start,\"\").replace(\")\",\"\").split(\",\").map(parseFloat);return t.length==r.len&&((!r.alpha||0<=t[3]&&t[3]<=1)&&!l.includes(t.slice(0,3).map(e=>0<=e&&e<=255),!1))}t.is_color=function(e){return n.is_svg_color(e.toLowerCase())||\"#\"==e.substring(0,1)||s(e)},t.rgb2hex=function(e,r,t){return`#${a(255&e)}${a(255&r)}${a(255&t)}`},t.color2hex=o,t.encode_rgba=function([e,r,t,n]){return(255*e|0)<<24|(255*r|0)<<16|(255*t|0)<<8|255*n|0},t.decode_rgba=function(e){return[(e>>24&255)/255,(e>>16&255)/255,(e>>8&255)/255,(e>>0&255)/255]},t.color2rgba=function(e,r=1){if(!e)return[0,0,0,0];let t=o(e);t=t.replace(/ |#/g,\"\"),t.length<=4&&(t=t.replace(/(.)/g,\"$1$1\"));const n=t.match(/../g).map(e=>parseInt(e,16)/255);for(;n.length<3;)n.push(0);return n.length<4&&n.push(r),n.slice(0,4)},t.valid_rgb=s},\n function _(e,F,r){Object.defineProperty(r,\"__esModule\",{value:!0}),r.svg_colors={indianred:\"#CD5C5C\",lightcoral:\"#F08080\",salmon:\"#FA8072\",darksalmon:\"#E9967A\",lightsalmon:\"#FFA07A\",crimson:\"#DC143C\",red:\"#FF0000\",firebrick:\"#B22222\",darkred:\"#8B0000\",pink:\"#FFC0CB\",lightpink:\"#FFB6C1\",hotpink:\"#FF69B4\",deeppink:\"#FF1493\",mediumvioletred:\"#C71585\",palevioletred:\"#DB7093\",coral:\"#FF7F50\",tomato:\"#FF6347\",orangered:\"#FF4500\",darkorange:\"#FF8C00\",orange:\"#FFA500\",gold:\"#FFD700\",yellow:\"#FFFF00\",lightyellow:\"#FFFFE0\",lemonchiffon:\"#FFFACD\",lightgoldenrodyellow:\"#FAFAD2\",papayawhip:\"#FFEFD5\",moccasin:\"#FFE4B5\",peachpuff:\"#FFDAB9\",palegoldenrod:\"#EEE8AA\",khaki:\"#F0E68C\",darkkhaki:\"#BDB76B\",lavender:\"#E6E6FA\",thistle:\"#D8BFD8\",plum:\"#DDA0DD\",violet:\"#EE82EE\",orchid:\"#DA70D6\",fuchsia:\"#FF00FF\",magenta:\"#FF00FF\",mediumorchid:\"#BA55D3\",mediumpurple:\"#9370DB\",blueviolet:\"#8A2BE2\",darkviolet:\"#9400D3\",darkorchid:\"#9932CC\",darkmagenta:\"#8B008B\",purple:\"#800080\",indigo:\"#4B0082\",slateblue:\"#6A5ACD\",darkslateblue:\"#483D8B\",mediumslateblue:\"#7B68EE\",greenyellow:\"#ADFF2F\",chartreuse:\"#7FFF00\",lawngreen:\"#7CFC00\",lime:\"#00FF00\",limegreen:\"#32CD32\",palegreen:\"#98FB98\",lightgreen:\"#90EE90\",mediumspringgreen:\"#00FA9A\",springgreen:\"#00FF7F\",mediumseagreen:\"#3CB371\",seagreen:\"#2E8B57\",forestgreen:\"#228B22\",green:\"#008000\",darkgreen:\"#006400\",yellowgreen:\"#9ACD32\",olivedrab:\"#6B8E23\",olive:\"#808000\",darkolivegreen:\"#556B2F\",mediumaquamarine:\"#66CDAA\",darkseagreen:\"#8FBC8F\",lightseagreen:\"#20B2AA\",darkcyan:\"#008B8B\",teal:\"#008080\",aqua:\"#00FFFF\",cyan:\"#00FFFF\",lightcyan:\"#E0FFFF\",paleturquoise:\"#AFEEEE\",aquamarine:\"#7FFFD4\",turquoise:\"#40E0D0\",mediumturquoise:\"#48D1CC\",darkturquoise:\"#00CED1\",cadetblue:\"#5F9EA0\",steelblue:\"#4682B4\",lightsteelblue:\"#B0C4DE\",powderblue:\"#B0E0E6\",lightblue:\"#ADD8E6\",skyblue:\"#87CEEB\",lightskyblue:\"#87CEFA\",deepskyblue:\"#00BFFF\",dodgerblue:\"#1E90FF\",cornflowerblue:\"#6495ED\",royalblue:\"#4169E1\",blue:\"#0000FF\",mediumblue:\"#0000CD\",darkblue:\"#00008B\",navy:\"#000080\",midnightblue:\"#191970\",cornsilk:\"#FFF8DC\",blanchedalmond:\"#FFEBCD\",bisque:\"#FFE4C4\",navajowhite:\"#FFDEAD\",wheat:\"#F5DEB3\",burlywood:\"#DEB887\",tan:\"#D2B48C\",rosybrown:\"#BC8F8F\",sandybrown:\"#F4A460\",goldenrod:\"#DAA520\",darkgoldenrod:\"#B8860B\",peru:\"#CD853F\",chocolate:\"#D2691E\",saddlebrown:\"#8B4513\",sienna:\"#A0522D\",brown:\"#A52A2A\",maroon:\"#800000\",white:\"#FFFFFF\",snow:\"#FFFAFA\",honeydew:\"#F0FFF0\",mintcream:\"#F5FFFA\",azure:\"#F0FFFF\",aliceblue:\"#F0F8FF\",ghostwhite:\"#F8F8FF\",whitesmoke:\"#F5F5F5\",seashell:\"#FFF5EE\",beige:\"#F5F5DC\",oldlace:\"#FDF5E6\",floralwhite:\"#FFFAF0\",ivory:\"#FFFFF0\",antiquewhite:\"#FAEBD7\",linen:\"#FAF0E6\",lavenderblush:\"#FFF0F5\",mistyrose:\"#FFE4E1\",gainsboro:\"#DCDCDC\",lightgray:\"#D3D3D3\",lightgrey:\"#D3D3D3\",silver:\"#C0C0C0\",darkgray:\"#A9A9A9\",darkgrey:\"#A9A9A9\",gray:\"#808080\",grey:\"#808080\",dimgray:\"#696969\",dimgrey:\"#696969\",lightslategray:\"#778899\",lightslategrey:\"#778899\",slategray:\"#708090\",slategrey:\"#708090\",darkslategray:\"#2F4F4F\",darkslategrey:\"#2F4F4F\",black:\"#000000\"},r.is_svg_color=function(e){return e in r.svg_colors}},\n function _(r,t,e){Object.defineProperty(e,\"__esModule\",{value:!0}),e.NumberArray=Float32Array,e.ColorArray=Uint32Array;const s=r(25);class a{constructor(r,t){this.offsets=r,this.array=t}[s.equals](r,t){return t.arrays(this.offsets,r.offsets)&&t.arrays(this.array,r.array)}get length(){return this.offsets.length}clone(){return new a(new Uint32Array(this.offsets),new e.NumberArray(this.array))}static from(r){const t=r.length,s=new Uint32Array(t);let n=0;for(let e=0;e{if(null!=t[r.equals]&&null!=e[r.equals])return t[r.equals](e,this);switch(s){case\"[object Array]\":case\"[object Uint8Array]\":case\"[object Int8Array]\":case\"[object Uint16Array]\":case\"[object Int16Array]\":case\"[object Uint32Array]\":case\"[object Int32Array]\":case\"[object Float32Array]\":case\"[object Float64Array]\":return this.arrays(t,e);case\"[object Map]\":return this.maps(t,e);case\"[object Set]\":return this.sets(t,e);case\"[object Object]\":if(t.constructor==e.constructor&&(null==t.constructor||t.constructor===Object))return this.objects(t,e);case\"[object Function]\":if(t.constructor==e.constructor&&t.constructor===Function)return this.eq(\"\"+t,\"\"+e)}if(t instanceof Node)return this.nodes(t,e);throw Error(\"can't compare objects of type \"+s)})();return o.pop(),c.pop(),i}numbers(t,e){return Object.is(t,e)}arrays(t,e){const{length:r}=t;if(r!=e.length)return!1;for(let n=0;n>>5,r=31&t;return!!(this._array[s]>>r&1)}set(t,s=!0){this._check_bounds(t),this._count=null;const r=t>>>5,e=31&t;s?this._array[r]|=1<>>t&1&&(e+=1)}return e}*ones(){const{_array:t,_nwords:s,size:r}=this;for(let e=0,i=0;i>>t&1&&(yield e);else e+=32}}*zeros(){const{_array:t,_nwords:s,size:r}=this;for(let e=0,i=0;i>>t&1||(yield e);else e+=32}}_check_size(t){e.assert(this.size==t.size,\"Size mismatch\")}add(t){this._check_size(t);for(let s=0;st(this.at(s,r),s,r))}apply(t){const s=a.from(t),{nrows:r,ncols:e}=this;if(r==s.nrows&&e==s.ncols)return new a(r,e,(t,r)=>s.at(t,r)(this.at(t,r),t,r));throw new Error(\"dimensions don't match\")}to_sparse(){return[...this]}static from(t,s){if(t instanceof a)return t;if(null!=s){const r=t,e=Math.floor(r.length/s);return new a(e,s,(t,e)=>r[t*s+e])}{const s=t,r=t.length,e=i.min(s.map(t=>t.length));return new a(r,e,(t,r)=>s[t][r])}}}r.Matrix=a,a.__name__=\"Matrix\"},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});class n{constructor(){this._dev=!1}set dev(e){this._dev=e}get dev(){return this._dev}}s.Settings=n,n.__name__=\"Settings\",s.settings=new n},\n function _(e,l,t){Object.defineProperty(t,\"__esModule\",{value:!0});const a=e(1).__importStar(e(18));t.Line={line_color:[a.Color,\"black\"],line_alpha:[a.Number,1],line_width:[a.Number,1],line_join:[a.LineJoin,\"bevel\"],line_cap:[a.LineCap,\"butt\"],line_dash:[a.Array,[]],line_dash_offset:[a.Number,0]},t.Fill={fill_color:[a.Color,\"gray\"],fill_alpha:[a.Number,1]},t.Hatch={hatch_color:[a.Color,\"black\"],hatch_alpha:[a.Number,1],hatch_scale:[a.Number,12],hatch_pattern:[a.NullString,null],hatch_weight:[a.Number,1],hatch_extra:[a.Any,{}]},t.Text={text_color:[a.Color,\"#444444\"],text_alpha:[a.Number,1],text_font:[a.Font,\"helvetica\"],text_font_size:[a.FontSize,\"16px\"],text_font_style:[a.FontStyle,\"normal\"],text_align:[a.TextAlign,\"left\"],text_baseline:[a.TextBaseline,\"bottom\"],text_line_height:[a.Number,1.2]},t.LineScalar={line_color:[a.ColorScalar,\"black\"],line_alpha:[a.NumberScalar,1],line_width:[a.NumberScalar,1],line_join:[a.LineJoinScalar,\"bevel\"],line_cap:[a.LineCapScalar,\"butt\"],line_dash:[a.ArrayScalar,[]],line_dash_offset:[a.NumberScalar,0]},t.FillScalar={fill_color:[a.ColorScalar,\"gray\"],fill_alpha:[a.NumberScalar,1]},t.HatchScalar={hatch_color:[a.ColorScalar,\"black\"],hatch_alpha:[a.NumberScalar,1],hatch_scale:[a.NumberScalar,12],hatch_pattern:[a.NullStringScalar,null],hatch_weight:[a.NumberScalar,1],hatch_extra:[a.AnyScalar,{}]},t.TextScalar={text_color:[a.ColorScalar,\"#444444\"],text_alpha:[a.NumberScalar,1],text_font:[a.Font,\"helvetica\"],text_font_size:[a.FontSizeScalar,\"16px\"],text_font_style:[a.FontStyleScalar,\"normal\"],text_align:[a.TextAlignScalar,\"left\"],text_baseline:[a.TextBaselineScalar,\"bottom\"],text_line_height:[a.NumberScalar,1.2]},t.LineVector={line_color:[a.ColorSpec,\"black\"],line_alpha:[a.NumberSpec,1],line_width:[a.NumberSpec,1],line_join:[a.LineJoin,\"bevel\"],line_cap:[a.LineCap,\"butt\"],line_dash:[a.Array,[]],line_dash_offset:[a.Number,0]},t.FillVector={fill_color:[a.ColorSpec,\"gray\"],fill_alpha:[a.NumberSpec,1]},t.HatchVector={hatch_color:[a.ColorSpec,\"black\"],hatch_alpha:[a.NumberSpec,1],hatch_scale:[a.NumberSpec,12],hatch_pattern:[a.NullStringSpec,null],hatch_weight:[a.NumberSpec,1],hatch_extra:[a.Any,{}]},t.TextVector={text_color:[a.ColorSpec,\"#444444\"],text_alpha:[a.NumberSpec,1],text_font:[a.Font,\"helvetica\"],text_font_size:[a.FontSizeSpec,\"16px\"],text_font_style:[a.FontStyle,\"normal\"],text_align:[a.TextAlign,\"left\"],text_baseline:[a.TextBaseline,\"bottom\"],text_line_height:[a.Number,1.2]}},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const n=t(27);function u(){const t=new Array(32);for(let e=0;e<32;e++)t[e]=\"0123456789ABCDEF\".substr(Math.floor(16*Math.random()),1);return t[12]=\"4\",t[16]=\"0123456789ABCDEF\".substr(3&t[16].charCodeAt(0)|8,1),t.join(\"\")}r.startsWith=function(t,e,r=0){return t.substr(r,e.length)==e},r.uuid4=u;let s=1e3;r.uniqueId=function(t){const e=n.settings.dev?\"j\"+s++:u();return null!=t?`${t}-${e}`:e},r.escape=function(t){return t.replace(/(?:[&<>\"'`])/g,t=>{switch(t){case\"&\":return\"&\";case\"<\":return\"<\";case\">\":return\">\";case'\"':return\""\";case\"'\":return\"'\";case\"`\":return\"`\";default:return t}})},r.unescape=function(t){return t.replace(/&(amp|lt|gt|quot|#x27|#x60);/g,(t,e)=>{switch(e){case\"amp\":return\"&\";case\"lt\":return\"<\";case\"gt\":return\">\";case\"quot\":return'\"';case\"#x27\":return\"'\";case\"#x60\":return\"`\";default:return e}})},r.use_strict=function(t){return\"'use strict';\\n\"+t}},\n function _(t,s,e){Object.defineProperty(e,\"__esModule\",{value:!0});const r=t(8),a=t(11),n=t(25),i=Symbol(\"__ndarray__\");class h extends Uint8Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"uint8\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Uint8NDArray=h,h.__name__=\"Uint8NDArray\";class _ extends Int8Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"int8\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Int8NDArray=_,_.__name__=\"Int8NDArray\";class u extends Uint16Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"uint16\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Uint16NDArray=u,u.__name__=\"Uint16NDArray\";class l extends Int16Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"int16\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Int16NDArray=l,l.__name__=\"Int16NDArray\";class y extends Uint32Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"uint32\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Uint32NDArray=y,y.__name__=\"Uint32NDArray\";class c extends Int32Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"int32\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Int32NDArray=c,c.__name__=\"Int32NDArray\";class p extends Float32Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"float32\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}e.Float32NDArray=p,p.__name__=\"Float32NDArray\";class o extends Float64Array{constructor(t,s){super(t),this.__ndarray__=i,this.dtype=\"float64\",this.shape=null!=s?s:d(t)?t.shape:[this.length],this.dimension=this.shape.length}[n.equals](t,s){return s.eq(this.shape,t.shape)&&s.arrays(this,t)}}function d(t){return r.isObject(t)&&t.__ndarray__==i}e.Float64NDArray=o,o.__name__=\"Float64NDArray\",e.is_NDArray=d,e.ndarray=function(t,s={}){let{dtype:e}=s;null==e&&(e=t instanceof ArrayBuffer||r.isArray(t)?\"float32\":(()=>{switch(!0){case t instanceof Uint8Array:return\"uint8\";case t instanceof Int8Array:return\"int8\";case t instanceof Uint16Array:return\"uint16\";case t instanceof Int16Array:return\"int16\";case t instanceof Uint32Array:return\"uint32\";case t instanceof Int32Array:return\"int32\";case t instanceof Float32Array:return\"float32\";case t instanceof Float64Array:return\"float64\";default:a.unreachable()}})());const{shape:n}=s;switch(e){case\"uint8\":return new h(t,n);case\"int8\":return new _(t,n);case\"uint16\":return new u(t,n);case\"int16\":return new l(t,n);case\"uint32\":return new y(t,n);case\"int32\":return new c(t,n);case\"float32\":return new p(t,n);case\"float64\":return new o(t,n)}}},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1),a=e(8),f=e(32),_=n.__importStar(e(30));function o(e){const r=new Uint8Array(e),t=Array.from(r).map(e=>String.fromCharCode(e));return btoa(t.join(\"\"))}function s(e){const r=atob(e),t=r.length,n=new Uint8Array(t);for(let e=0,a=t;e{switch(a){case\"uint8\":return new _.Uint8NDArray(o,n);case\"int8\":return new _.Int8NDArray(o,n);case\"uint16\":return new _.Uint16NDArray(o,n);case\"int16\":return new _.Int16NDArray(o,n);case\"uint32\":return new _.Uint32NDArray(o,n);case\"int32\":return new _.Int32NDArray(o,n);case\"float32\":return new _.Float32NDArray(o,n);case\"float64\":return new _.Float64NDArray(o,n)}})();if(f!==t.BYTE_ORDER)switch(l.BYTES_PER_ELEMENT){case 2:i(l);break;case 4:u(l);break;case 8:c(l)}return l},t.encode_NDArray=function(e,r){const n={order:t.BYTE_ORDER,dtype:e.dtype,shape:e.shape};if(null!=r){const t=\"\"+r.size;return r.set(t,e.buffer),Object.assign({__buffer__:t},n)}{const r=o(e.buffer);return Object.assign({__ndarray__:r},n)}}},\n function _(e,n,i){Object.defineProperty(i,\"__esModule\",{value:!0}),i.is_ie=(()=>{const e=\"undefined\"!=typeof navigator?navigator.userAgent:\"\";return e.indexOf(\"MSIE\")>=0||e.indexOf(\"Trident\")>0||e.indexOf(\"Edge\")>0})(),i.is_mobile=\"undefined\"!=typeof window&&(\"ontouchstart\"in window||navigator.maxTouchPoints>0),i.is_little_endian=(()=>{const e=new ArrayBuffer(4),n=new Uint8Array(e);new Uint32Array(e)[1]=168496141;let i=!0;return 10==n[4]&&11==n[5]&&12==n[6]&&13==n[7]&&(i=!1),i})()},\n function _(t,r,n){Object.defineProperty(n,\"__esModule\",{value:!0});const e=t(8),i=t(13);n.pretty=Symbol(\"pretty\");class o{constructor(t){this.precision=null==t?void 0:t.precision}to_string(t){return function(t){return n.pretty in Object(t)}(t)?t[n.pretty](this):e.isBoolean(t)?this.boolean(t):e.isNumber(t)?this.number(t):e.isString(t)?this.string(t):e.isArray(t)?this.array(t):e.isIterable(t)?this.iterable(t):e.isPlainObject(t)?this.object(t):\"\"+t}token(t){return t}boolean(t){return\"\"+t}number(t){return null!=this.precision?t.toFixed(this.precision):\"\"+t}string(t){return`\"${t.replace(/'/g,\"\\\\'\")}\"`}array(t){const r=this.token,n=[];for(const r of t)n.push(this.to_string(r));return`${r(\"[\")}${n.join(r(\",\")+\" \")}${r(\"]\")}`}iterable(t){var r;const n=this.token,e=null!==(r=Object(t)[Symbol.toStringTag])&&void 0!==r?r:\"Object\",i=this.array(t);return`${e}${n(\"(\")}${i}${n(\")\")}`}object(t){const r=this.token,n=[];for(const[e,o]of i.entries(t))n.push(`${e}${r(\":\")} ${this.to_string(o)}`);return`${r(\"{\")}${n.join(r(\",\")+\" \")}${r(\"}\")}`}}n.Printer=o,o.__name__=\"Printer\",n.to_string=function(t,r){return new o(r).to_string(t)}},\n function _(t,_,r){Object.defineProperty(r,\"__esModule\",{value:!0});const e=t(1);e.__exportStar(t(35),r),e.__exportStar(t(176),r),e.__exportStar(t(203),r),e.__exportStar(t(207),r),e.__exportStar(t(218),r),e.__exportStar(t(222),r),e.__exportStar(t(228),r),e.__exportStar(t(232),r),e.__exportStar(t(265),r),e.__exportStar(t(268),r),e.__exportStar(t(270),r),e.__exportStar(t(132),r),e.__exportStar(t(148),r),e.__exportStar(t(287),r),e.__exportStar(t(291),r),e.__exportStar(t(320),r),e.__exportStar(t(321),r),e.__exportStar(t(322),r),e.__exportStar(t(323),r),e.__exportStar(t(324),r),e.__exportStar(t(329),r),e.__exportStar(t(331),r),e.__exportStar(t(342),r),e.__exportStar(t(346),r)},\n function _(a,e,o){Object.defineProperty(o,\"__esModule\",{value:!0});var r=a(36);o.Annotation=r.Annotation;var n=a(83);o.Arrow=n.Arrow;var t=a(84);o.ArrowHead=t.ArrowHead;var v=a(84);o.OpenHead=v.OpenHead;var l=a(84);o.NormalHead=l.NormalHead;var d=a(84);o.TeeHead=d.TeeHead;var i=a(84);o.VeeHead=i.VeeHead;var A=a(122);o.Band=A.Band;var H=a(124);o.BoxAnnotation=H.BoxAnnotation;var T=a(125);o.ColorBar=T.ColorBar;var p=a(160);o.Label=p.Label;var L=a(162);o.LabelSet=L.LabelSet;var b=a(163);o.Legend=b.Legend;var B=a(164);o.LegendItem=B.LegendItem;var S=a(166);o.PolyAnnotation=S.PolyAnnotation;var P=a(167);o.Slope=P.Slope;var g=a(168);o.Span=g.Span;var m=a(161);o.TextAnnotation=m.TextAnnotation;var w=a(169);o.Title=w.Title;var x=a(170);o.ToolbarPanel=x.ToolbarPanel;var s=a(171);o.Tooltip=s.Tooltip;var u=a(175);o.Whisker=u.Whisker},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const s=t(1).__importStar(t(37)),i=t(13),o=t(70);class _ extends o.RendererView{get panel(){return this.layout}connect_signals(){super.connect_signals();const t=this.model.properties;this.on_change(t.visible,()=>this.plot_view.request_layout())}get_size(){if(this.model.visible){const{width:t,height:e}=this._get_size();return{width:Math.round(t),height:Math.round(e)}}return{width:0,height:0}}_get_size(){throw new Error(\"not implemented\")}set_data(t){const e=this.model.materialize_dataspecs(t);if(i.extend(this,e),this.plot_model.use_map){const t=this;null!=t._x&&([t._x,t._y]=s.project_xy(t._x,t._y)),null!=t._xs&&([t._xs,t._ys]=s.project_xsys(t._xs,t._ys))}}get needs_clip(){return null==this.layout}serializable_state(){const t=super.serializable_state();return null==this.layout?t:Object.assign(Object.assign({},t),{bbox:this.layout.bbox.box})}}n.AnnotationView=_,_.__name__=\"AnnotationView\";class a extends o.Renderer{constructor(t){super(t)}static init_Annotation(){this.override({level:\"annotation\"})}}n.Annotation=a,a.__name__=\"Annotation\",a.init_Annotation()},\n function _(n,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const r=n(1),o=r.__importDefault(n(38)),l=r.__importDefault(n(39)),c=n(24),i=new l.default(\"GOOGLE\"),u=new l.default(\"WGS84\"),a=o.default(u,i);e.wgs84_mercator={compute:(n,t)=>isFinite(n)&&isFinite(t)?a.forward([n,t]):[NaN,NaN],invert:(n,t)=>isFinite(n)&&isFinite(t)?a.inverse([n,t]):[NaN,NaN]};const s={lon:[-20026376.39,20026376.39],lat:[-20048966.1,20048966.1]},f={lon:[-180,180],lat:[-85.06,85.06]},{min:_,max:p}=Math;function m(n,t){const r=_(n.length,t.length),o=new c.NumberArray(r),l=new c.NumberArray(r);return e.inplace.project_xy(n,t,o,l),[o,l]}e.clip_mercator=function(n,t,e){const[r,o]=s[e];return[p(n,r),_(t,o)]},e.in_bounds=function(n,t){const[e,r]=f[t];return e2?void 0!==e.name&&\"geocent\"===e.name||void 0!==n.name&&\"geocent\"===n.name?\"number\"==typeof r.z?[r.x,r.y,r.z].concat(t.splice(3)):[r.x,r.y,t[2]].concat(t.splice(3)):[r.x,r.y].concat(t.splice(2)):[r.x,r.y]):(o=a.default(e,n,t),2===(i=Object.keys(t)).length||i.forEach((function(r){if(void 0!==e.name&&\"geocent\"===e.name||void 0!==n.name&&\"geocent\"===n.name){if(\"x\"===r||\"y\"===r||\"z\"===r)return}else if(\"x\"===r||\"y\"===r)return;o[r]=t[r]})),o)}function u(e){return e instanceof o.default?e:e.oProj?e.oProj:o.default(e)}t.default=function(e,n,t){e=u(e);var r,o=!1;return void 0===n?(n=e,e=i,o=!0):(void 0!==n.x||Array.isArray(n))&&(t=n,n=e,e=i,o=!0),n=u(n),t?c(e,n,t):(r={forward:function(t){return c(e,n,t)},inverse:function(t){return c(n,e,t)}},o&&(r.oProj=n),r)}},\n function _(e,t,a){Object.defineProperty(a,\"__esModule\",{value:!0});const s=e(1),i=s.__importDefault(e(40)),u=s.__importDefault(e(51)),l=s.__importDefault(e(52)),o=e(60),r=s.__importDefault(e(62)),f=s.__importDefault(e(63)),d=s.__importDefault(e(47));function p(e,t){if(!(this instanceof p))return new p(e);t=t||function(e){if(e)throw e};var a=i.default(e);if(\"object\"==typeof a){var s=p.projections.get(a.projName);if(s){if(a.datumCode&&\"none\"!==a.datumCode){var l=d.default(r.default,a.datumCode);l&&(a.datum_params=l.towgs84?l.towgs84.split(\",\"):null,a.ellps=l.ellipse,a.datumName=l.datumName?l.datumName:a.datumCode)}a.k0=a.k0||1,a.axis=a.axis||\"enu\",a.ellps=a.ellps||\"wgs84\";var m=o.sphere(a.a,a.b,a.rf,a.ellps,a.sphere),n=o.eccentricity(m.a,m.b,m.rf,a.R_A),h=a.datum||f.default(a.datumCode,a.datum_params,m.a,m.b,n.es,n.ep2);u.default(this,a),u.default(this,s),this.a=m.a,this.b=m.b,this.rf=m.rf,this.sphere=m.sphere,this.es=n.es,this.e=n.e,this.ep2=n.ep2,this.datum=h,this.init(),t(null,this)}else t(e)}else t(e)}p.projections=l.default,p.projections.start(),a.default=p},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const u=t(1),n=u.__importDefault(t(41)),f=u.__importDefault(t(48)),i=u.__importDefault(t(43)),a=u.__importDefault(t(47));var o=[\"PROJECTEDCRS\",\"PROJCRS\",\"GEOGCS\",\"GEOCCS\",\"PROJCS\",\"LOCAL_CS\",\"GEODCRS\",\"GEODETICCRS\",\"GEODETICDATUM\",\"ENGCRS\",\"ENGINEERINGCRS\"];var l=[\"3857\",\"900913\",\"3785\",\"102113\"];r.default=function(t){if(!function(t){return\"string\"==typeof t}(t))return t;if(function(t){return t in n.default}(t))return n.default[t];if(function(t){return o.some((function(e){return t.indexOf(e)>-1}))}(t)){var e=f.default(t);if(function(t){var e=a.default(t,\"authority\");if(e){var r=a.default(e,\"epsg\");return r&&l.indexOf(r)>-1}}(e))return n.default[\"EPSG:3857\"];var r=function(t){var e=a.default(t,\"extension\");if(e)return a.default(e,\"proj4\")}(e);return r?i.default(r):e}return function(t){return\"+\"===t[0]}(t)?i.default(t):void 0}},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=t(1),n=i.__importDefault(t(42)),f=i.__importDefault(t(43)),a=i.__importDefault(t(48));function l(t){var e=this;if(2===arguments.length){var r=arguments[1];\"string\"==typeof r?\"+\"===r.charAt(0)?l[t]=f.default(arguments[1]):l[t]=a.default(arguments[1]):l[t]=r}else if(1===arguments.length){if(Array.isArray(t))return t.map((function(t){Array.isArray(t)?l.apply(e,t):l(t)}));if(\"string\"==typeof t){if(t in l)return l[t]}else\"EPSG\"in t?l[\"EPSG:\"+t.EPSG]=t:\"ESRI\"in t?l[\"ESRI:\"+t.ESRI]=t:\"IAU2000\"in t?l[\"IAU2000:\"+t.IAU2000]=t:console.log(t);return}}n.default(l),r.default=l},\n function _(e,t,l){Object.defineProperty(l,\"__esModule\",{value:!0}),l.default=function(e){e(\"EPSG:4326\",\"+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees\"),e(\"EPSG:4269\",\"+title=NAD83 (long/lat) +proj=longlat +a=6378137.0 +b=6356752.31414036 +ellps=GRS80 +datum=NAD83 +units=degrees\"),e(\"EPSG:3857\",\"+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs\"),e.WGS84=e[\"EPSG:4326\"],e[\"EPSG:3785\"]=e[\"EPSG:3857\"],e.GOOGLE=e[\"EPSG:3857\"],e[\"EPSG:900913\"]=e[\"EPSG:3857\"],e[\"EPSG:102113\"]=e[\"EPSG:3857\"]}},\n function _(t,n,o){Object.defineProperty(o,\"__esModule\",{value:!0});const e=t(1),a=t(44),u=e.__importDefault(t(45)),r=e.__importDefault(t(46)),i=e.__importDefault(t(47));o.default=function(t){var n,o,e,f={},l=t.split(\"+\").map((function(t){return t.trim()})).filter((function(t){return t})).reduce((function(t,n){var o=n.split(\"=\");return o.push(!0),t[o[0].toLowerCase()]=o[1],t}),{}),c={proj:\"projName\",datum:\"datumCode\",rf:function(t){f.rf=parseFloat(t)},lat_0:function(t){f.lat0=t*a.D2R},lat_1:function(t){f.lat1=t*a.D2R},lat_2:function(t){f.lat2=t*a.D2R},lat_ts:function(t){f.lat_ts=t*a.D2R},lon_0:function(t){f.long0=t*a.D2R},lon_1:function(t){f.long1=t*a.D2R},lon_2:function(t){f.long2=t*a.D2R},alpha:function(t){f.alpha=parseFloat(t)*a.D2R},lonc:function(t){f.longc=t*a.D2R},x_0:function(t){f.x0=parseFloat(t)},y_0:function(t){f.y0=parseFloat(t)},k_0:function(t){f.k0=parseFloat(t)},k:function(t){f.k0=parseFloat(t)},a:function(t){f.a=parseFloat(t)},b:function(t){f.b=parseFloat(t)},r_a:function(){f.R_A=!0},zone:function(t){f.zone=parseInt(t,10)},south:function(){f.utmSouth=!0},towgs84:function(t){f.datum_params=t.split(\",\").map((function(t){return parseFloat(t)}))},to_meter:function(t){f.to_meter=parseFloat(t)},units:function(t){f.units=t;var n=i.default(r.default,t);n&&(f.to_meter=n.to_meter)},from_greenwich:function(t){f.from_greenwich=t*a.D2R},pm:function(t){var n=i.default(u.default,t);f.from_greenwich=(n||parseFloat(t))*a.D2R},nadgrids:function(t){\"@null\"===t?f.datumCode=\"none\":f.nadgrids=t},axis:function(t){3===t.length&&-1!==\"ewnsud\".indexOf(t.substr(0,1))&&-1!==\"ewnsud\".indexOf(t.substr(1,1))&&-1!==\"ewnsud\".indexOf(t.substr(2,1))&&(f.axis=t)}};for(n in l)o=l[n],n in c?\"function\"==typeof(e=c[n])?e(o):f[e]=o:f[n]=o;return\"string\"==typeof f.datumCode&&\"WGS84\"!==f.datumCode&&(f.datumCode=f.datumCode.toLowerCase()),f}},\n function _(P,_,e){Object.defineProperty(e,\"__esModule\",{value:!0}),e.PJD_3PARAM=1,e.PJD_7PARAM=2,e.PJD_WGS84=4,e.PJD_NODATUM=5,e.SEC_TO_RAD=484813681109536e-20,e.HALF_PI=Math.PI/2,e.SIXTH=.16666666666666666,e.RA4=.04722222222222222,e.RA6=.022156084656084655,e.EPSLN=1e-10,e.D2R=.017453292519943295,e.R2D=57.29577951308232,e.FORTPI=Math.PI/4,e.TWO_PI=2*Math.PI,e.SPI=3.14159265359},\n function _(e,o,r){Object.defineProperty(r,\"__esModule\",{value:!0});var a={};r.default=a,a.greenwich=0,a.lisbon=-9.131906111111,a.paris=2.337229166667,a.bogota=-74.080916666667,a.madrid=-3.687938888889,a.rome=12.452333333333,a.bern=7.439583333333,a.jakarta=106.807719444444,a.ferro=-17.666666666667,a.brussels=4.367975,a.stockholm=18.058277777778,a.athens=23.7163375,a.oslo=10.722916666667},\n function _(e,t,f){Object.defineProperty(f,\"__esModule\",{value:!0}),f.default={ft:{to_meter:.3048},\"us-ft\":{to_meter:1200/3937}}},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});var o=/[\\s_\\-\\/\\(\\)]/g;t.default=function(e,r){if(e[r])return e[r];for(var t,a=Object.keys(e),n=r.toLowerCase().replace(o,\"\"),f=-1;++f0?90:-90),e.lat_ts=e.lat1)}(l),l}},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0}),r.default=function(t){return new a(t).output()};var i=/\\s/,s=/[A-Za-z]/,h=/[A-Za-z84]/,o=/[,\\]]/,n=/[\\d\\.E\\-\\+]/;function a(t){if(\"string\"!=typeof t)throw new Error(\"not a string\");this.text=t.trim(),this.level=0,this.place=0,this.root=null,this.stack=[],this.currentObject=null,this.state=1}a.prototype.readCharicter=function(){var t=this.text[this.place++];if(4!==this.state)for(;i.test(t);){if(this.place>=this.text.length)return;t=this.text[this.place++]}switch(this.state){case 1:return this.neutral(t);case 2:return this.keyword(t);case 4:return this.quoted(t);case 5:return this.afterquote(t);case 3:return this.number(t);case-1:return}},a.prototype.afterquote=function(t){if('\"'===t)return this.word+='\"',void(this.state=4);if(o.test(t))return this.word=this.word.trim(),void this.afterItem(t);throw new Error(\"havn't handled \\\"\"+t+'\" in afterquote yet, index '+this.place)},a.prototype.afterItem=function(t){return\",\"===t?(null!==this.word&&this.currentObject.push(this.word),this.word=null,void(this.state=1)):\"]\"===t?(this.level--,null!==this.word&&(this.currentObject.push(this.word),this.word=null),this.state=1,this.currentObject=this.stack.pop(),void(this.currentObject||(this.state=-1))):void 0},a.prototype.number=function(t){if(!n.test(t)){if(o.test(t))return this.word=parseFloat(this.word),void this.afterItem(t);throw new Error(\"havn't handled \\\"\"+t+'\" in number yet, index '+this.place)}this.word+=t},a.prototype.quoted=function(t){'\"'!==t?this.word+=t:this.state=5},a.prototype.keyword=function(t){if(h.test(t))this.word+=t;else{if(\"[\"===t){var e=[];return e.push(this.word),this.level++,null===this.root?this.root=e:this.currentObject.push(e),this.stack.push(this.currentObject),this.currentObject=e,void(this.state=1)}if(!o.test(t))throw new Error(\"havn't handled \\\"\"+t+'\" in keyword yet, index '+this.place);this.afterItem(t)}},a.prototype.neutral=function(t){if(s.test(t))return this.word=t,void(this.state=2);if('\"'===t)return this.word=\"\",void(this.state=4);if(n.test(t))return this.word=t,void(this.state=3);if(!o.test(t))throw new Error(\"havn't handled \\\"\"+t+'\" in neutral yet, index '+this.place);this.afterItem(t)},a.prototype.output=function(){for(;this.place90&&a*l.R2D<-90&&h*l.R2D>180&&h*l.R2D<-180)return null;if(Math.abs(Math.abs(a)-l.HALF_PI)<=l.EPSLN)return null;if(this.sphere)i=this.x0+this.a*this.k0*e.default(h-this.long0),s=this.y0+this.a*this.k0*Math.log(Math.tan(l.FORTPI+.5*a));else{var n=Math.sin(a),u=r.default(this.e,a,n);i=this.x0+this.a*this.k0*e.default(h-this.long0),s=this.y0-this.a*this.k0*Math.log(u)}return t.x=i,t.y=s,t}function f(t){var i,s,h=t.x-this.x0,a=t.y-this.y0;if(this.sphere)s=l.HALF_PI-2*Math.atan(Math.exp(-a/(this.a*this.k0)));else{var r=Math.exp(-a/(this.a*this.k0));if(-9999===(s=n.default(this.e,r)))return null}return i=e.default(this.long0+h/(this.a*this.k0)),t.x=i,t.y=s,t}s.init=u,s.forward=o,s.inverse=f,s.names=[\"Mercator\",\"Popular Visualisation Pseudo Mercator\",\"Mercator_1SP\",\"Mercator_Auxiliary_Sphere\",\"merc\"],s.default={init:u,forward:o,inverse:f,names:s.names}},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0}),n.default=function(e,t,n){var r=e*t;return n/Math.sqrt(1-r*r)}},\n function _(e,t,u){Object.defineProperty(u,\"__esModule\",{value:!0});const n=e(1),a=e(44),f=n.__importDefault(e(56));u.default=function(e){return Math.abs(e)<=a.SPI?e:e-f.default(e)*a.TWO_PI}},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e){return e<0?-1:1}},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const a=t(44);n.default=function(t,e,n){var o=t*n,u=.5*t;return o=Math.pow((1-o)/(1+o),u),Math.tan(.5*(a.HALF_PI-e))/o}},\n function _(t,a,e){Object.defineProperty(e,\"__esModule\",{value:!0});const n=t(44);e.default=function(t,a){for(var e,r,o=.5*t,u=n.HALF_PI-2*Math.atan(a),f=0;f<=15;f++)if(e=t*Math.sin(u),u+=r=n.HALF_PI-2*Math.atan(a*Math.pow((1-e)/(1+e),o))-u,Math.abs(r)<=1e-10)return u;return-9999}},\n function _(e,n,i){function t(){}function r(e){return e}Object.defineProperty(i,\"__esModule\",{value:!0}),i.init=t,i.forward=r,i.inverse=r,i.names=[\"longlat\",\"identity\"],i.default={init:t,forward:r,inverse:r,names:i.names}},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const a=e(1),n=e(44),f=a.__importStar(e(61)),u=a.__importDefault(e(47));r.eccentricity=function(e,t,r,a){var f=e*e,u=t*t,i=(f-u)/f,c=0;return a?(f=(e*=1-i*(n.SIXTH+i*(n.RA4+i*n.RA6)))*e,i=0):c=Math.sqrt(i),{es:i,e:c,ep2:(f-u)/u}},r.sphere=function(e,t,r,a,i){if(!e){var c=u.default(f.default,a);c||(c=f.WGS84),e=c.a,t=c.b,r=c.rf}return r&&!t&&(t=(1-1/r)*e),(0===r||Math.abs(e-t)3&&(0===r.datum_params[3]&&0===r.datum_params[4]&&0===r.datum_params[5]&&0===r.datum_params[6]||(r.datum_type=t.PJD_7PARAM,r.datum_params[3]*=t.SEC_TO_RAD,r.datum_params[4]*=t.SEC_TO_RAD,r.datum_params[5]*=t.SEC_TO_RAD,r.datum_params[6]=r.datum_params[6]/1e6+1))),r.a=_,r.b=u,r.es=d,r.ep2=p,r}},\n function _(t,e,a){Object.defineProperty(a,\"__esModule\",{value:!0});const r=t(1),u=t(44),m=r.__importDefault(t(65)),_=r.__importDefault(t(67)),o=r.__importDefault(t(39)),d=r.__importDefault(t(68)),f=r.__importDefault(t(69));a.default=function t(e,a,r){var n;if(Array.isArray(r)&&(r=d.default(r)),f.default(r),e.datum&&a.datum&&function(t,e){return(t.datum.datum_type===u.PJD_3PARAM||t.datum.datum_type===u.PJD_7PARAM)&&\"WGS84\"!==e.datumCode||(e.datum.datum_type===u.PJD_3PARAM||e.datum.datum_type===u.PJD_7PARAM)&&\"WGS84\"!==t.datumCode}(e,a)&&(r=t(e,n=new o.default(\"WGS84\"),r),e=n),\"enu\"!==e.axis&&(r=_.default(e,!1,r)),\"longlat\"===e.projName)r={x:r.x*u.D2R,y:r.y*u.D2R,z:r.z||0};else if(e.to_meter&&(r={x:r.x*e.to_meter,y:r.y*e.to_meter,z:r.z||0}),!(r=e.inverse(r)))return;return e.from_greenwich&&(r.x+=e.from_greenwich),r=m.default(e.datum,a.datum,r),a.from_greenwich&&(r={x:r.x-a.from_greenwich,y:r.y,z:r.z||0}),\"longlat\"===a.projName?r={x:r.x*u.R2D,y:r.y*u.R2D,z:r.z||0}:(r=a.forward(r),a.to_meter&&(r={x:r.x/a.to_meter,y:r.y/a.to_meter,z:r.z||0})),\"enu\"!==a.axis?_.default(a,!0,r):r}},\n function _(e,t,a){Object.defineProperty(a,\"__esModule\",{value:!0});const u=e(44),o=e(66);function _(e){return e===u.PJD_3PARAM||e===u.PJD_7PARAM}a.default=function(e,t,a){return o.compareDatums(e,t)||e.datum_type===u.PJD_NODATUM||t.datum_type===u.PJD_NODATUM?a:e.es!==t.es||e.a!==t.a||_(e.datum_type)||_(t.datum_type)?(a=o.geodeticToGeocentric(a,e.es,e.a),_(e.datum_type)&&(a=o.geocentricToWgs84(a,e.datum_type,e.datum_params)),_(t.datum_type)&&(a=o.geocentricFromWgs84(a,t.datum_type,t.datum_params)),o.geocentricToGeodetic(a,t.es,t.a,t.b)):a}},\n function _(a,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const e=a(44);r.compareDatums=function(a,t){return a.datum_type===t.datum_type&&(!(a.a!==t.a||Math.abs(a.es-t.es)>5e-11)&&(a.datum_type===e.PJD_3PARAM?a.datum_params[0]===t.datum_params[0]&&a.datum_params[1]===t.datum_params[1]&&a.datum_params[2]===t.datum_params[2]:a.datum_type!==e.PJD_7PARAM||a.datum_params[0]===t.datum_params[0]&&a.datum_params[1]===t.datum_params[1]&&a.datum_params[2]===t.datum_params[2]&&a.datum_params[3]===t.datum_params[3]&&a.datum_params[4]===t.datum_params[4]&&a.datum_params[5]===t.datum_params[5]&&a.datum_params[6]===t.datum_params[6]))},r.geodeticToGeocentric=function(a,t,r){var m,u,s,_,n=a.x,d=a.y,i=a.z?a.z:0;if(d<-e.HALF_PI&&d>-1.001*e.HALF_PI)d=-e.HALF_PI;else if(d>e.HALF_PI&&d<1.001*e.HALF_PI)d=e.HALF_PI;else{if(d<-e.HALF_PI)return{x:-1/0,y:-1/0,z:a.z};if(d>e.HALF_PI)return{x:1/0,y:1/0,z:a.z}}return n>Math.PI&&(n-=2*Math.PI),u=Math.sin(d),_=Math.cos(d),s=u*u,{x:((m=r/Math.sqrt(1-t*s))+i)*_*Math.cos(n),y:(m+i)*_*Math.sin(n),z:(m*(1-t)+i)*u}},r.geocentricToGeodetic=function(a,t,r,m){var u,s,_,n,d,i,p,P,o,y,M,z,c,A,x,f=a.x,h=a.y,I=a.z?a.z:0;if(u=Math.sqrt(f*f+h*h),s=Math.sqrt(f*f+h*h+I*I),u/r<1e-12){if(A=0,s/r<1e-12)return e.HALF_PI,x=-m,{x:a.x,y:a.y,z:a.z}}else A=Math.atan2(h,f);_=I/s,P=(n=u/s)*(1-t)*(d=1/Math.sqrt(1-t*(2-t)*n*n)),o=_*d,c=0;do{c++,i=t*(p=r/Math.sqrt(1-t*o*o))/(p+(x=u*P+I*o-p*(1-t*o*o))),z=(M=_*(d=1/Math.sqrt(1-i*(2-i)*n*n)))*P-(y=n*(1-i)*d)*o,P=y,o=M}while(z*z>1e-24&&c<30);return{x:A,y:Math.atan(M/Math.abs(y)),z:x}},r.geocentricToWgs84=function(a,t,r){if(t===e.PJD_3PARAM)return{x:a.x+r[0],y:a.y+r[1],z:a.z+r[2]};if(t===e.PJD_7PARAM){var m=r[0],u=r[1],s=r[2],_=r[3],n=r[4],d=r[5],i=r[6];return{x:i*(a.x-d*a.y+n*a.z)+m,y:i*(d*a.x+a.y-_*a.z)+u,z:i*(-n*a.x+_*a.y+a.z)+s}}},r.geocentricFromWgs84=function(a,t,r){if(t===e.PJD_3PARAM)return{x:a.x-r[0],y:a.y-r[1],z:a.z-r[2]};if(t===e.PJD_7PARAM){var m=r[0],u=r[1],s=r[2],_=r[3],n=r[4],d=r[5],i=r[6],p=(a.x-m)/i,P=(a.y-u)/i,o=(a.z-s)/i;return{x:p+d*P-n*o,y:-d*p+P+_*o,z:n*p-_*P+o}}}},\n function _(e,a,i){Object.defineProperty(i,\"__esModule\",{value:!0}),i.default=function(e,a,i){var s,n,r,c=i.x,d=i.y,u=i.z||0,f={};for(r=0;r<3;r++)if(!a||2!==r||void 0!==i.z)switch(0===r?(s=c,n=-1!==\"ew\".indexOf(e.axis[r])?\"x\":\"y\"):1===r?(s=d,n=-1!==\"ns\".indexOf(e.axis[r])?\"y\":\"x\"):(s=u,n=\"z\"),e.axis[r]){case\"e\":case\"w\":case\"n\":case\"s\":f[n]=s;break;case\"u\":void 0!==i[n]&&(f.z=s);break;case\"d\":void 0!==i[n]&&(f.z=-s);break;default:return null}return f}},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e){var n={x:e[0],y:e[1]};return e.length>2&&(n.z=e[2]),e.length>3&&(n.m=e[3]),n}},\n function _(e,i,n){function t(e){if(\"function\"==typeof Number.isFinite){if(Number.isFinite(e))return;throw new TypeError(\"coordinates must be finite numbers\")}if(\"number\"!=typeof e||e!=e||!isFinite(e))throw new TypeError(\"coordinates must be finite numbers\")}Object.defineProperty(n,\"__esModule\",{value:!0}),n.default=function(e){t(e.x),t(e.y)}},\n function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1),r=e(71),s=n.__importStar(e(74)),_=n.__importStar(e(18)),a=e(81),o=e(82);class l extends r.View{get coordinates(){return this._coordinates}initialize(){super.initialize(),this.visuals=new s.Visuals(this.model),this.needs_webgl_blit=!1,this._initialize_coordinates()}connect_signals(){super.connect_signals();const{x_range_name:e,y_range_name:i}=this.model.properties;this.on_change([e,i],()=>this._initialize_coordinates())}_initialize_coordinates(){const{x_range_name:e,y_range_name:i}=this.model,{frame:t}=this.plot_view,n=t.x_scales.get(e),r=t.y_scales.get(i);this._coordinates=new o.CoordinateTransform(n,r)}get plot_view(){return this.parent}get plot_model(){return this.parent.model}get layer(){const{overlays:e,primary:i}=this.plot_view.canvas_view;return\"overlay\"==this.model.level?e:i}request_render(){this.plot_view.request_render()}notify_finished(){this.plot_view.notify_finished()}get needs_clip(){return!1}get has_webgl(){return!1}render(){this.model.visible&&this._render(),this._has_finished=!0}}t.RendererView=l,l.__name__=\"RendererView\";class d extends a.Model{constructor(e){super(e)}static init_Renderer(){this.define({level:[_.RenderLevel],visible:[_.Boolean,!0],x_range_name:[_.String,\"default\"],y_range_name:[_.String,\"default\"]})}}t.Renderer=d,d.__name__=\"Renderer\",d.init_Renderer()},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=t(1),r=t(15),n=t(72),o=t(8),h=i.__importDefault(t(73));class a{constructor(t){if(this.removed=new r.Signal0(this,\"removed\"),this._ready=Promise.resolve(void 0),null==t.model)throw new Error(\"model of a view wasn't configured\");this.model=t.model,this._parent=t.parent}get ready(){return this._ready}connect(t,e){return t.connect((t,s)=>{const i=Promise.resolve(e.call(this,t,s));this._ready=this._ready.then(()=>i)},this)}disconnect(t,e){return t.disconnect(e,this)}initialize(){this._has_finished=!1,this.is_root&&(this._stylesheet=n.stylesheet);for(const t of this.styles())this.stylesheet.append(t)}async lazy_initialize(){}remove(){this._parent=void 0,this.disconnect_signals(),this.removed.emit()}toString(){return`${this.model.type}View(${this.model.id})`}serializable_state(){return{type:this.model.type}}get parent(){if(void 0!==this._parent)return this._parent;throw new Error(\"parent of a view wasn't configured\")}get is_root(){return null===this.parent}get root(){return this.is_root?this:this.parent.root}assert_root(){if(!this.is_root)throw new Error(this.toString()+\" is not a root layout\")}has_finished(){return this._has_finished}get is_idle(){return this.has_finished()}connect_signals(){}disconnect_signals(){r.Signal.disconnectReceiver(this)}on_change(t,e){for(const s of o.isArray(t)?t:[t])this.connect(s.change,e)}cursor(t,e){return null}get stylesheet(){return this.is_root?this._stylesheet:this.root.stylesheet}styles(){return[h.default]}}s.View=a,a.__name__=\"View\"},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const i=t(8),o=t(13),s=t=>(e={},...n)=>{const s=document.createElement(t);s.classList.add(\"bk\");for(let[t,n]of o.entries(e))if(null!=n&&(!i.isBoolean(n)||n))if(\"class\"===t&&(i.isString(n)&&(n=n.split(/\\s+/)),i.isArray(n)))for(const t of n)null!=t&&s.classList.add(t);else if(\"style\"===t&&i.isPlainObject(n))for(const[t,e]of o.entries(n))s.style[t]=e;else if(\"data\"===t&&i.isPlainObject(n))for(const[t,e]of o.entries(n))s.dataset[t]=e;else s.setAttribute(t,n);function l(t){if(i.isString(t))s.appendChild(document.createTextNode(t));else if(t instanceof Node)s.appendChild(t);else if(t instanceof NodeList||t instanceof HTMLCollection)for(const e of t)s.appendChild(e);else if(null!=t&&!1!==t)throw new Error(\"expected a DOM element, string, false or null, got \"+JSON.stringify(t))}for(const t of n)if(i.isArray(t))for(const e of t)l(e);else l(t);return s};function l(t){const e=t.parentNode;null!=e&&e.removeChild(t)}function r(t,...e){const n=t.firstChild;for(const i of e)t.insertBefore(i,n)}function a(t,e){const n=Element.prototype;return(n.matches||n.webkitMatchesSelector||n.mozMatchesSelector||n.msMatchesSelector).call(t,e)}function c(t){return parseFloat(t)||0}function h(t){const e=getComputedStyle(t);return{border:{top:c(e.borderTopWidth),bottom:c(e.borderBottomWidth),left:c(e.borderLeftWidth),right:c(e.borderRightWidth)},margin:{top:c(e.marginTop),bottom:c(e.marginBottom),left:c(e.marginLeft),right:c(e.marginRight)},padding:{top:c(e.paddingTop),bottom:c(e.paddingBottom),left:c(e.paddingLeft),right:c(e.paddingRight)}}}function d(t){const e=t.getBoundingClientRect();return{width:Math.ceil(e.width),height:Math.ceil(e.height)}}n.createElement=function(t,e,...n){return s(t)(e,...n)},n.div=s(\"div\"),n.span=s(\"span\"),n.canvas=s(\"canvas\"),n.link=s(\"link\"),n.style=s(\"style\"),n.a=s(\"a\"),n.p=s(\"p\"),n.i=s(\"i\"),n.pre=s(\"pre\"),n.button=s(\"button\"),n.label=s(\"label\"),n.input=s(\"input\"),n.select=s(\"select\"),n.option=s(\"option\"),n.optgroup=s(\"optgroup\"),n.textarea=s(\"textarea\"),n.nbsp=function(){return document.createTextNode(\"Β \")},n.append=function(t,...e){for(const n of e)t.appendChild(n)},n.remove=l,n.removeElement=l,n.replaceWith=function(t,e){const n=t.parentNode;null!=n&&n.replaceChild(e,t)},n.prepend=r,n.empty=function(t,e=!1){let n;for(;n=t.firstChild;)t.removeChild(n);if(e&&t instanceof Element)for(const e of t.attributes)t.removeAttributeNode(e)},n.display=function(t){t.style.display=\"\"},n.undisplay=function(t){t.style.display=\"none\"},n.show=function(t){t.style.visibility=\"\"},n.hide=function(t){t.style.visibility=\"hidden\"},n.offset=function(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset-document.documentElement.clientTop,left:e.left+window.pageXOffset-document.documentElement.clientLeft}},n.matches=a,n.parent=function(t,e){let n=t;for(;n=n.parentElement;)if(a(n,e))return n;return null},n.extents=h,n.size=d,n.scroll_size=function(t){return{width:Math.ceil(t.scrollWidth),height:Math.ceil(t.scrollHeight)}},n.outer_size=function(t){const{margin:{left:e,right:n,top:i,bottom:o}}=h(t),{width:s,height:l}=d(t);return{width:Math.ceil(s+e+n),height:Math.ceil(l+i+o)}},n.content_size=function(t){const{left:e,top:n}=t.getBoundingClientRect(),{padding:i}=h(t);let o=0,s=0;for(const l of t.children){const t=l.getBoundingClientRect();o=Math.max(o,Math.ceil(t.left-e-i.left+t.width)),s=Math.max(s,Math.ceil(t.top-n-i.top+t.height))}return{width:o,height:s}},n.position=function(t,e,n){const{style:i}=t;if(i.left=e.x+\"px\",i.top=e.y+\"px\",i.width=e.width+\"px\",i.height=e.height+\"px\",null==n)i.margin=\"\";else{const{top:t,right:e,bottom:o,left:s}=n;i.margin=`${t}px ${e}px ${o}px ${s}px`}},n.children=function(t){return Array.from(t.children)};class f{constructor(t){this.el=t,this.classList=t.classList}get values(){const t=[];for(let e=0;e\":\"vertical_wave\",\"*\":\"criss_cross\"};class p{constructor(e,t=\"\"){this.obj=e,this.prefix=t,this.cache={};for(const a of this.attrs)this[a]=e.properties[t+a]}warm_cache(e,t){for(const a of this.attrs){const s=this.obj.properties[this.prefix+a];if(void 0!==s.spec.value)this.cache[a]=s.spec.value;else{if(!(null!=e&&s instanceof c.VectorSpec))throw new Error(\"source is required with a vectorized visual property\");{const l=s.array(e),c=null!=t?t.select(l):l;this.cache[a+\"_array\"]=c}}}}cache_select(e,t){const a=this.obj.properties[this.prefix+e];let s;return void 0!==a.spec.value?this.cache[e]=s=a.spec.value:this.cache[e]=s=this.cache[e+\"_array\"][t],s}get_array(e){return this.cache[e+\"_array\"]}set_vectorize(e,t){this._set_vectorize(e,t)}}a.ContextProperties=p,p.__name__=\"ContextProperties\";class f extends p{set_value(e){const t=this.line_color.value(),a=this.line_alpha.value();e.strokeStyle=n(t,a),e.lineWidth=this.line_width.value(),e.lineJoin=this.line_join.value(),e.lineCap=this.line_cap.value(),e.lineDash=this.line_dash.value(),e.lineDashOffset=this.line_dash_offset.value()}get doit(){return!(null===this.line_color.spec.value||0==this.line_alpha.spec.value||0==this.line_width.spec.value)}_set_vectorize(e,t){const a=this.cache_select(\"line_color\",t),s=this.cache_select(\"line_alpha\",t),l=this.cache_select(\"line_width\",t),c=this.cache_select(\"line_join\",t),i=this.cache_select(\"line_cap\",t),o=this.cache_select(\"line_dash\",t),r=this.cache_select(\"line_dash_offset\",t);e.strokeStyle=n(a,s),e.lineWidth=l,e.lineJoin=c,e.lineCap=i,e.lineDash=o,e.lineDashOffset=r}color_value(){return n(this.line_color.value(),this.line_alpha.value())}}a.Line=f,f.__name__=\"Line\",f.prototype.attrs=Object.keys(l.LineVector);class d extends p{set_value(e){const t=this.fill_color.value(),a=this.fill_alpha.value();e.fillStyle=n(t,a)}get doit(){return!(null===this.fill_color.spec.value||0==this.fill_alpha.spec.value)}_set_vectorize(e,t){const a=this.cache_select(\"fill_color\",t),s=this.cache_select(\"fill_alpha\",t);e.fillStyle=n(a,s)}color_value(){return n(this.fill_color.value(),this.fill_alpha.value())}}a.Fill=d,d.__name__=\"Fill\",d.prototype.attrs=Object.keys(l.FillVector);class k extends p{cache_select(e,t){let s;if(\"pattern\"==e){const e=this.cache_select(\"hatch_color\",t),s=this.cache_select(\"hatch_alpha\",t),l=this.cache_select(\"hatch_scale\",t),c=this.cache_select(\"hatch_pattern\",t),i=this.cache_select(\"hatch_weight\",t),{hatch_extra:o}=this.cache;if(null!=o&&o.hasOwnProperty(c)){const t=o[c];this.cache.pattern=t.get_pattern(e,s,l,i)}else this.cache.pattern=t=>{const o=t instanceof r.SVGRenderingContext2D?\"svg\":\"canvas\",p=new h.CanvasLayer(o,!0);return p.resize(l,l),p.prepare(),function(e,t,s,l,c,i){var o;const r=c,h=r/2,p=h/2;switch(e.strokeStyle=n(s,l),e.lineCap=\"square\",e.fillStyle=s,e.lineWidth=i,null!==(o=a.hatch_aliases[t])&&void 0!==o?o:t){case\"blank\":break;case\"dot\":e.arc(h,h,h/2,0,2*Math.PI,!0),e.fill();break;case\"ring\":e.arc(h,h,h/2,0,2*Math.PI,!0),e.stroke();break;case\"horizontal_line\":_(e,r,h);break;case\"vertical_line\":u(e,r,h);break;case\"cross\":_(e,r,h),u(e,r,h);break;case\"horizontal_dash\":_(e,h,h);break;case\"vertical_dash\":u(e,h,h);break;case\"spiral\":{const t=r/30;e.moveTo(h,h);for(let a=0;a<360;a++){const s=.1*a,l=h+t*s*Math.cos(s),c=h+t*s*Math.sin(s);e.lineTo(l,c)}e.stroke();break}case\"right_diagonal_line\":e.moveTo(.5-p,r),e.lineTo(p+.5,0),e.stroke(),e.moveTo(p+.5,r),e.lineTo(3*p+.5,0),e.stroke(),e.moveTo(3*p+.5,r),e.lineTo(5*p+.5,0),e.stroke(),e.stroke();break;case\"left_diagonal_line\":e.moveTo(p+.5,r),e.lineTo(.5-p,0),e.stroke(),e.moveTo(3*p+.5,r),e.lineTo(p+.5,0),e.stroke(),e.moveTo(5*p+.5,r),e.lineTo(3*p+.5,0),e.stroke(),e.stroke();break;case\"diagonal_cross\":v(e,r);break;case\"right_diagonal_dash\":e.moveTo(p+.5,3*p+.5),e.lineTo(3*p+.5,p+.5),e.stroke();break;case\"left_diagonal_dash\":e.moveTo(p+.5,p+.5),e.lineTo(3*p+.5,3*p+.5),e.stroke();break;case\"horizontal_wave\":e.moveTo(0,p),e.lineTo(h,3*p),e.lineTo(r,p),e.stroke();break;case\"vertical_wave\":e.moveTo(p,0),e.lineTo(3*p,h),e.lineTo(p,r),e.stroke();break;case\"criss_cross\":v(e,r),_(e,r,h),u(e,r,h)}}(p.ctx,c,e,s,l,i),t.createPattern(p.canvas,\"repeat\")}}else s=super.cache_select(e,t);return s}_try_defer(e){const{hatch_pattern:t,hatch_extra:a}=this.cache;if(null!=a&&a.hasOwnProperty(t)){a[t].onload(e)}}get doit(){return!(null===this.hatch_color.spec.value||0==this.hatch_alpha.spec.value||\" \"==this.hatch_pattern.spec.value||\"blank\"==this.hatch_pattern.spec.value||null===this.hatch_pattern.spec.value)}doit2(e,t,a,s){if(!this.doit)return;this.cache_select(\"pattern\",t);null==this.cache.pattern(e)?this._try_defer(s):(this.set_vectorize(e,t),a())}_set_vectorize(e,t){this.cache_select(\"pattern\",t),e.fillStyle=this.cache.pattern(e)}color_value(){return n(this.hatch_color.value(),this.hatch_alpha.value())}}a.Hatch=k,k.__name__=\"Hatch\",k.prototype.attrs=Object.keys(l.HatchVector);class x extends p{color_value(){return n(this.text_color.value(),this.text_alpha.value())}font_value(){const e=this.text_font.value(),t=this.text_font_size.value();return`${this.text_font_style.value()} ${t} ${e}`}v_font_value(e){super.cache_select(\"text_font_style\",e),super.cache_select(\"text_font_size\",e),super.cache_select(\"text_font\",e);const{text_font_style:t,text_font_size:a,text_font:s}=this.cache;return`${t} ${a} ${s}`}cache_select(e,t){let a;return\"font\"==e?this.cache.font=a=this.v_font_value(t):a=super.cache_select(e,t),a}set_value(e){const t=this.text_color.value(),a=this.text_alpha.value();e.fillStyle=n(t,a),e.font=this.font_value(),e.textAlign=this.text_align.value(),e.textBaseline=this.text_baseline.value()}get doit(){return!(null===this.text_color.spec.value||0==this.text_alpha.spec.value)}_set_vectorize(e,t){const a=this.cache_select(\"text_color\",t),s=this.cache_select(\"text_alpha\",t),l=this.cache_select(\"font\",t),c=this.cache_select(\"text_align\",t),i=this.cache_select(\"text_baseline\",t);e.fillStyle=n(a,s),e.font=l,e.textAlign=c,e.textBaseline=i}}a.Text=x,x.__name__=\"Text\",x.prototype.attrs=Object.keys(l.TextVector);class b{constructor(e){for(const t of e._mixins){const[a,s=\"\"]=t.split(\":\");let l;switch(a){case\"line\":l=f;break;case\"fill\":l=d;break;case\"hatch\":l=k;break;case\"text\":l=x;break;default:throw new Error(\"unknown visual: \"+a)}this[s+a]=new l(e,s)}}warm_cache(e,t){for(const a in this)if(this.hasOwnProperty(a)){const s=this[a];s instanceof p&&s.warm_cache(e,t)}}}a.Visuals=b,b.__name__=\"Visuals\"},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(76),n=t(8),r=t(72);function a(t){if(!t)throw new Error(\"cannot create a random attribute name for an undefined object\");const e=\"ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz\";let i=\"\";do{i=\"\";for(let t=0;t<12;t++)i+=e[Math.floor(Math.random()*e.length)]}while(t[i]);return i}function o(t){const e={left:\"start\",right:\"end\",center:\"middle\",start:\"start\",end:\"end\"};return e[t]||e.start}function l(t){const e={alphabetic:\"alphabetic\",hanging:\"hanging\",top:\"text-before-edge\",bottom:\"text-after-edge\",middle:\"central\"};return e[t]||e.alphabetic}const h=function(t,e){const i=new Map,s=t.split(\",\");e=e||10;for(let t=0;t=0?Math.acos(e):-Math.acos(e)}const b=w(f),v=w(g);this.lineTo(d+f[0]*n,m+f[1]*n),this.arc(d,m,n,b,v)}stroke(){\"path\"===this.__currentElement.nodeName&&this.__currentElement.setAttribute(\"paint-order\",\"fill\"),this.__applyCurrentDefaultPath(),this.__applyStyleToCurrentElement(\"stroke\"),null!=this._clip_path&&this.__currentElement.setAttribute(\"clip-path\",this._clip_path)}fill(){\"path\"===this.__currentElement.nodeName&&this.__currentElement.setAttribute(\"paint-order\",\"stroke\"),this.__applyCurrentDefaultPath(),this.__applyStyleToCurrentElement(\"fill\"),null!=this._clip_path&&this.__currentElement.setAttribute(\"clip-path\",this._clip_path)}rect(t,e,i,s){isFinite(t+e+i+s)&&(\"path\"!==this.__currentElement.nodeName&&this.beginPath(),this.moveTo(t,e),this.lineTo(t+i,e),this.lineTo(t+i,e+s),this.lineTo(t,e+s),this.lineTo(t,e))}fillRect(t,e,i,s){isFinite(t+e+i+s)&&(this.beginPath(),this.rect(t,e,i,s),this.fill())}strokeRect(t,e,i,s){isFinite(t+e+i+s)&&(this.beginPath(),this.rect(t,e,i,s),this.stroke())}__clearCanvas(){r.empty(this.__defs),r.empty(this.__root),this.__root.appendChild(this.__defs),this.__currentElement=this.__root}clearRect(t,e,i,s){if(!isFinite(t+e+i+s))return;if(0===t&&0===e&&i===this.width&&s===this.height)return void this.__clearCanvas();const n=this.__createElement(\"rect\",{x:t,y:e,width:i,height:s,fill:\"#FFFFFF\"},!0);this._apply_transform(n),this.__root.appendChild(n)}createLinearGradient(t,e,i,s){if(!isFinite(t+e+i+s))throw new Error(\"The provided double value is non-finite\");const[n,r]=this._transform.apply(t,e),[o,l]=this._transform.apply(i,s),h=this.__createElement(\"linearGradient\",{id:a(this.__ids),x1:n+\"px\",x2:o+\"px\",y1:r+\"px\",y2:l+\"px\",gradientUnits:\"userSpaceOnUse\"},!1);return this.__defs.appendChild(h),new _(h,this)}createRadialGradient(t,e,i,s,n,r){if(!isFinite(t+e+i+s+n+r))throw new Error(\"The provided double value is non-finite\");const[o,l]=this._transform.apply(t,e),[h,c]=this._transform.apply(s,n),u=this.__createElement(\"radialGradient\",{id:a(this.__ids),cx:h+\"px\",cy:c+\"px\",r:r+\"px\",fx:o+\"px\",fy:l+\"px\",gradientUnits:\"userSpaceOnUse\"},!1);return this.__defs.appendChild(u),new _(u,this)}__parseFont(){const t=/^\\s*(?=(?:(?:[-a-z]+\\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\\1|\\2|\\3)\\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\\d]+(?:\\%|in|[cem]m|ex|p[ctx]))(?:\\s*\\/\\s*(normal|[.\\d]+(?:\\%|in|[cem]m|ex|p[ctx])))?\\s*([-,\\'\\\"\\sa-z0-9]+?)\\s*$/i.exec(this.font),e={style:t[1]||\"normal\",size:t[4]||\"10px\",family:t[6]||\"sans-serif\",weight:t[3]||\"normal\",decoration:t[2]||\"normal\"};return\"underline\"===this.__fontUnderline&&(e.decoration=\"underline\"),null!=this.__fontHref&&(e.href=this.__fontHref),e}__wrapTextLink(t,e){if(t.href){const i=this.__createElement(\"a\");return i.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",t.href),i.appendChild(e),i}return e}__applyText(t,e,i,s){const n=this.__parseFont(),r=this.__createElement(\"text\",{\"font-family\":n.family,\"font-size\":n.size,\"font-style\":n.style,\"font-weight\":n.weight,\"text-decoration\":n.decoration,x:e,y:i,\"text-anchor\":o(this.textAlign),\"dominant-baseline\":l(this.textBaseline)},!0);r.appendChild(this.__document.createTextNode(t)),this._apply_transform(r),this.__currentElement=r,this.__applyStyleToCurrentElement(s),this.__root.appendChild(this.__wrapTextLink(n,r))}fillText(t,e,i){null!=t&&isFinite(e+i)&&this.__applyText(t,e,i,\"fill\")}strokeText(t,e,i){null!=t&&isFinite(e+i)&&this.__applyText(t,e,i,\"stroke\")}measureText(t){return this.__ctx.font=this.font,this.__ctx.measureText(t)}arc(t,e,i,s,n,r=!1){if(!isFinite(t+e+i+s+n))return;if(s===n)return;(s%=2*Math.PI)===(n%=2*Math.PI)&&(n=(n+2*Math.PI-.001*(r?-1:1))%(2*Math.PI));const a=t+i*Math.cos(n),o=e+i*Math.sin(n),l=t+i*Math.cos(s),h=e+i*Math.sin(s),c=r?0:1;let _=0,u=n-s;u<0&&(u+=2*Math.PI),_=r?u>Math.PI?0:1:u>Math.PI?1:0,this.lineTo(l,h);const p=i,d=i,[m,f]=this._transform.apply(a,o);this.__addPathCommand(m,f,`A ${p} ${d} 0 ${_} ${c} ${m} ${f}`)}clip(){const t=this.__createElement(\"clipPath\"),e=a(this.__ids);this.__applyCurrentDefaultPath(),t.setAttribute(\"id\",e),t.appendChild(this.__currentElement),this.__defs.appendChild(t),this._clip_path=`url(#${e})`}drawImage(t,...e){let i,s,n,r,a,o,l,h;if(2==e.length){if([i,s]=e,!isFinite(i+s))return;a=0,o=0,l=t.width,h=t.height,n=l,r=h}else if(4==e.length){if([i,s,n,r]=e,!isFinite(i+s+n+r))return;a=0,o=0,l=t.width,h=t.height}else{if(8!==e.length)throw new Error(\"Inavlid number of arguments passed to drawImage: \"+arguments.length);if([a,o,l,h,i,s,n,r]=e,!isFinite(a+o+l+h+i+s+n+r))return}const c=this.__root,_=\"translate(\"+i+\", \"+s+\")\",u=this._transform.clone().translate(i,s);if(t instanceof p||t instanceof SVGSVGElement){const e=(t instanceof SVGSVGElement?t:t.get_svg()).cloneNode(!0);let i;u.is_identity?i=c:(i=this.__createElement(\"g\"),this._apply_transform(i,u),c.appendChild(i));for(const t of[...e.childNodes])if(t instanceof SVGDefsElement){for(const e of[...t.childNodes])if(e instanceof Element){const t=e.getAttribute(\"id\");this.__ids[t]=t,this.__defs.appendChild(e)}}else i.appendChild(t)}else if(t instanceof HTMLImageElement||t instanceof SVGImageElement){const e=this.__createElement(\"image\");if(e.setAttribute(\"width\",\"\"+n),e.setAttribute(\"height\",\"\"+r),e.setAttribute(\"preserveAspectRatio\",\"none\"),a||o||l!==t.width||h!==t.height){const e=this.__document.createElement(\"canvas\");e.width=n,e.height=r;e.getContext(\"2d\").drawImage(t,a,o,l,h,0,0,n,r),t=e}e.setAttribute(\"transform\",_);const i=t instanceof HTMLCanvasElement?t.toDataURL():t.getAttribute(\"src\");e.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",i),c.appendChild(e)}else if(t instanceof HTMLCanvasElement){const e=this.__createElement(\"image\");e.setAttribute(\"width\",\"\"+n),e.setAttribute(\"height\",\"\"+r),e.setAttribute(\"preserveAspectRatio\",\"none\");const i=this.__document.createElement(\"canvas\");i.width=n,i.height=r;const s=i.getContext(\"2d\");s.imageSmoothingEnabled=!1,s.drawImage(t,a,o,l,h,0,0,n,r),t=i,e.setAttribute(\"transform\",_),e.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",t.toDataURL()),c.appendChild(e)}}createPattern(t,e){const i=this.__document.createElementNS(\"http://www.w3.org/2000/svg\",\"pattern\"),s=a(this.__ids);if(i.setAttribute(\"id\",s),i.setAttribute(\"width\",\"\"+this._to_number(t.width)),i.setAttribute(\"height\",\"\"+this._to_number(t.height)),i.setAttribute(\"patternUnits\",\"userSpaceOnUse\"),t instanceof HTMLCanvasElement||t instanceof HTMLImageElement||t instanceof SVGImageElement){const e=this.__document.createElementNS(\"http://www.w3.org/2000/svg\",\"image\"),s=t instanceof HTMLCanvasElement?t.toDataURL():t.getAttribute(\"src\");e.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",s),i.appendChild(e),this.__defs.appendChild(i)}else if(t instanceof p){for(const e of[...t.__root.childNodes])e instanceof SVGDefsElement||i.appendChild(e);this.__defs.appendChild(i)}else{if(!(t instanceof SVGSVGElement))throw new Error(\"unsupported\");for(const e of[...t.childNodes])e instanceof SVGDefsElement||i.appendChild(e);this.__defs.appendChild(i)}return new u(i,this)}setLineDash(t){t&&t.length>0?this.lineDash=t.join(\",\"):this.lineDash=null}_to_number(t){return n.isNumber(t)?t:t.baseVal.value}}i.SVGRenderingContext2D=p,p.__name__=\"SVGRenderingContext2D\"},\n function _(t,s,r){Object.defineProperty(r,\"__esModule\",{value:!0});const{sin:e,cos:n}=Math;class i{constructor(t=1,s=0,r=0,e=1,n=0,i=0){this.a=t,this.b=s,this.c=r,this.d=e,this.e=n,this.f=i}toString(){const{a:t,b:s,c:r,d:e,e:n,f:i}=this;return`matrix(${t}, ${s}, ${r}, ${e}, ${n}, ${i})`}clone(){const{a:t,b:s,c:r,d:e,e:n,f:a}=this;return new i(t,s,r,e,n,a)}get is_identity(){const{a:t,b:s,c:r,d:e,e:n,f:i}=this;return 1==t&&0==s&&0==r&&1==e&&0==n&&0==i}apply(t,s){const{a:r,b:e,c:n,d:i,e:a,f:h}=this;return[r*t+n*s+a,e*t+i*s+h]}iv_apply(t,s){const{a:r,b:e,c:n,d:i,e:a,f:h}=this,c=t.length;for(let o=0;o{const e=document.createElement(\"canvas\"),t=e.getContext(\"webgl\",{premultipliedAlpha:!0});return null!=t?{canvas:e,gl:t}:void l.logger.trace(\"WebGL is not supported\")})(),v={position:\"absolute\",top:\"0\",left:\"0\",width:\"100%\",height:\"100%\"};class b{constructor(e,t){switch(this.backend=e,this.hidpi=t,this.pixel_ratio=1,this.bbox=new c.BBox,e){case\"webgl\":case\"canvas\":{this._el=this._canvas=r.canvas({style:v});const e=this.canvas.getContext(\"2d\");if(null==e)throw new Error(\"unable to obtain 2D rendering context\");this._ctx=e,t&&(this.pixel_ratio=devicePixelRatio);break}case\"svg\":{const e=new d.SVGRenderingContext2D;this._ctx=e,this._canvas=e.get_svg(),this._el=r.div({style:v},this._canvas);break}}_.fixup_ctx(this._ctx)}get canvas(){return this._canvas}get ctx(){return this._ctx}get el(){return this._el}resize(e,t){this.bbox=new c.BBox({left:0,top:0,width:e,height:t});const i=this._ctx instanceof d.SVGRenderingContext2D?this._ctx:this.canvas;i.width=e*this.pixel_ratio,i.height=t*this.pixel_ratio}prepare(){const{ctx:e,hidpi:t,pixel_ratio:i}=this;e.save(),t&&(e.scale(i,i),e.translate(.5,.5)),this.clear()}clear(){const{x:e,y:t,width:i,height:s}=this.bbox;this.ctx.clearRect(e,t,i,s)}finish(){this.ctx.restore()}to_blob(){const{_canvas:e}=this;if(e instanceof HTMLCanvasElement)return null!=e.msToBlob?Promise.resolve(e.msToBlob()):new Promise((t,i)=>{e.toBlob(e=>null!=e?t(e):i(),\"image/png\")});{const e=this._ctx.get_serialized_svg(!0),t=new Blob([e],{type:\"image/svg+xml\"});return Promise.resolve(t)}}}i.CanvasLayer=b,b.__name__=\"CanvasLayer\";class g extends n.DOMView{constructor(){super(...arguments),this.bbox=new c.BBox}initialize(){super.initialize();const{output_backend:e,hidpi:t}=this.model;\"webgl\"==e&&(this.webgl=p),this.underlays_el=r.div({style:v}),this.primary=new b(e,t),this.overlays=new b(e,t),this.overlays_el=r.div({style:v}),this.events_el=r.div({class:\"bk-canvas-events\",style:v});const i=[this.underlays_el,this.primary.el,this.overlays.el,this.overlays_el,this.events_el];h.extend(this.el.style,v),r.append(this.el,...i),l.logger.debug(\"CanvasView initialized\")}add_underlay(e){this.underlays_el.appendChild(e)}add_overlay(e){this.overlays_el.appendChild(e)}add_event(e){this.events_el.appendChild(e)}get pixel_ratio(){return this.primary.pixel_ratio}resize(e,t){this.bbox=new c.BBox({left:0,top:0,width:e,height:t}),this.primary.resize(e,t),this.overlays.resize(e,t)}prepare_webgl(e){const{webgl:t}=this;if(null!=t){const{width:i,height:s}=this.bbox;t.canvas.width=this.pixel_ratio*i,t.canvas.height=this.pixel_ratio*s;const{gl:a}=t;a.enable(a.SCISSOR_TEST);const[n,l,o,r]=e,{xview:h,yview:c}=this.bbox,_=h.compute(n),d=c.compute(l+r),p=this.pixel_ratio;a.scissor(p*_,p*d,p*o,p*r),a.enable(a.BLEND),a.blendFuncSeparate(a.SRC_ALPHA,a.ONE_MINUS_SRC_ALPHA,a.ONE_MINUS_DST_ALPHA,a.ONE)}}clear_webgl(){const{webgl:e}=this;if(null!=e){const{gl:t,canvas:i}=e;t.viewport(0,0,i.width,i.height),t.clearColor(0,0,0,0),t.clear(t.COLOR_BUFFER_BIT||t.DEPTH_BUFFER_BIT)}}blit_webgl(e){const{webgl:t}=this;if(null!=t&&(l.logger.debug(\"Blitting WebGL canvas\"),e.restore(),e.drawImage(t.canvas,0,0),e.save(),this.model.hidpi)){const t=this.pixel_ratio;e.scale(t,t),e.translate(.5,.5)}}compose(){const{output_backend:e,hidpi:t}=this.model,{width:i,height:s}=this.bbox,a=new b(e,t);return a.resize(i,s),a.ctx.drawImage(this.primary.canvas,0,0),a.ctx.drawImage(this.overlays.canvas,0,0),a}to_blob(){return this.compose().to_blob()}}i.CanvasView=g,g.__name__=\"CanvasView\";class x extends a.HasProps{constructor(e){super(e)}static init_Canvas(){this.prototype.default_view=g,this.internal({hidpi:[o.Boolean,!0],output_backend:[o.OutputBackend,\"canvas\"]})}}i.Canvas=x,x.__name__=\"Canvas\",x.init_Canvas()},\n function _(e,s,t){Object.defineProperty(t,\"__esModule\",{value:!0});const i=e(71),r=e(72);class n extends i.View{initialize(){super.initialize(),this.el=this._createElement()}remove(){r.remove(this.el),super.remove()}css_classes(){return[]}render(){}renderTo(e){e.appendChild(this.el),this.render()}_createElement(){return r.createElement(this.tagName,{class:this.css_classes()})}}t.DOMView=n,n.__name__=\"DOMView\",n.prototype.tagName=\"div\"},\n function _(t,i,e){Object.defineProperty(e,\"__esModule\",{value:!0});const h=t(24),{min:r,max:s}=Math;e.empty=function(){return{x0:1/0,y0:1/0,x1:-1/0,y1:-1/0}},e.positive_x=function(){return{x0:Number.MIN_VALUE,y0:-1/0,x1:1/0,y1:1/0}},e.positive_y=function(){return{x0:-1/0,y0:Number.MIN_VALUE,x1:1/0,y1:1/0}},e.union=function(t,i){return{x0:r(t.x0,i.x0),x1:s(t.x1,i.x1),y0:r(t.y0,i.y0),y1:s(t.y1,i.y1)}};class n{constructor(t){if(null==t)this.x0=0,this.y0=0,this.x1=0,this.y1=0;else if(\"x0\"in t){const{x0:i,y0:e,x1:h,y1:r}=t;if(!(i<=h&&e<=r))throw new Error(`invalid bbox {x0: ${i}, y0: ${e}, x1: ${h}, y1: ${r}}`);this.x0=i,this.y0=e,this.x1=h,this.y1=r}else if(\"x\"in t){const{x:i,y:e,width:h,height:r}=t;if(!(h>=0&&r>=0))throw new Error(`invalid bbox {x: ${i}, y: ${e}, width: ${h}, height: ${r}}`);this.x0=i,this.y0=e,this.x1=i+h,this.y1=e+r}else{let i,e,h,r;if(\"width\"in t)if(\"left\"in t)i=t.left,e=i+t.width;else if(\"right\"in t)e=t.right,i=e-t.width;else{const h=t.width/2;i=t.hcenter-h,e=t.hcenter+h}else i=t.left,e=t.right;if(\"height\"in t)if(\"top\"in t)h=t.top,r=h+t.height;else if(\"bottom\"in t)r=t.bottom,h=r-t.height;else{const i=t.height/2;h=t.vcenter-i,r=t.vcenter+i}else h=t.top,r=t.bottom;if(!(i<=e&&h<=r))throw new Error(`invalid bbox {left: ${i}, top: ${h}, right: ${e}, bottom: ${r}}`);this.x0=i,this.y0=h,this.x1=e,this.y1=r}}toString(){return`BBox({left: ${this.left}, top: ${this.top}, width: ${this.width}, height: ${this.height}})`}get left(){return this.x0}get top(){return this.y0}get right(){return this.x1}get bottom(){return this.y1}get p0(){return[this.x0,this.y0]}get p1(){return[this.x1,this.y1]}get x(){return this.x0}get y(){return this.y0}get width(){return this.x1-this.x0}get height(){return this.y1-this.y0}get rect(){return{x0:this.x0,y0:this.y0,x1:this.x1,y1:this.y1}}get box(){return{x:this.x,y:this.y,width:this.width,height:this.height}}get h_range(){return{start:this.x0,end:this.x1}}get v_range(){return{start:this.y0,end:this.y1}}get ranges(){return[this.h_range,this.v_range]}get aspect(){return this.width/this.height}get hcenter(){return(this.left+this.right)/2}get vcenter(){return(this.top+this.bottom)/2}relativize(){const{width:t,height:i}=this;return new n({x:0,y:0,width:t,height:i})}contains(t,i){return t>=this.x0&&t<=this.x1&&i>=this.y0&&i<=this.y1}clip(t,i){return tthis.x1&&(t=this.x1),ithis.y1&&(i=this.y1),[t,i]}union(t){return new n({x0:r(this.x0,t.x0),y0:r(this.y0,t.y0),x1:s(this.x1,t.x1),y1:s(this.y1,t.y1)})}equals(t){return this.x0==t.x0&&this.y0==t.y0&&this.x1==t.x1&&this.y1==t.y1}get xview(){return{compute:t=>this.left+t,v_compute:t=>{const i=new h.NumberArray(t.length),e=this.left;for(let h=0;hthis.bottom-t,v_compute:t=>{const i=new h.NumberArray(t.length),e=this.bottom;for(let h=0;he.getLineDash(),set:t=>e.setLineDash(t)})}(e),function(e){e.setImageSmoothingEnabled=t=>{e.imageSmoothingEnabled=t,e.mozImageSmoothingEnabled=t,e.oImageSmoothingEnabled=t,e.webkitImageSmoothingEnabled=t,e.msImageSmoothingEnabled=t},e.getImageSmoothingEnabled=()=>{const t=e.imageSmoothingEnabled;return null==t||t}}(e),function(e){e.measureText&&null==e.html5MeasureText&&(e.html5MeasureText=e.measureText,e.measureText=t=>{const n=e.html5MeasureText(t);return n.ascent=1.6*e.html5MeasureText(\"m\").width,n})}(e),function(e){e.ellipse||(e.ellipse=function(t,n,o,a,i,l,m,r=!1){const u=.551784;e.translate(t,n),e.rotate(i);let s=o,g=a;r&&(s=-o,g=-a),e.moveTo(-s,0),e.bezierCurveTo(-s,g*u,-s*u,g,0,g),e.bezierCurveTo(s*u,g,s,g*u,s,0),e.bezierCurveTo(s,-g*u,s*u,-g,0,-g),e.bezierCurveTo(-s*u,-g,-s,-g*u,-s,0),e.rotate(-i),e.translate(-t,-n)})}(e)}},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=e(1),c=e(14),i=n.__importStar(e(18)),a=e(8),r=e(13),o=e(19);class l extends c.HasProps{constructor(e){super(e)}static init_Model(){this.define({tags:[i.Array,[]],name:[i.String],js_property_callbacks:[i.Any,{}],js_event_callbacks:[i.Any,{}],subscribed_events:[i.Array,[]]})}initialize(){super.initialize(),this._js_callbacks=new Map}connect_signals(){super.connect_signals(),this._update_property_callbacks(),this.connect(this.properties.js_property_callbacks.change,()=>this._update_property_callbacks()),this.connect(this.properties.js_event_callbacks.change,()=>this._update_event_callbacks()),this.connect(this.properties.subscribed_events.change,()=>this._update_event_callbacks())}_process_event(e){for(const t of this.js_event_callbacks[e.event_name]||[])t.execute(e);null!=this.document&&this.subscribed_events.some(t=>t==e.event_name)&&this.document.event_manager.send_event(e)}trigger_event(e){null!=this.document&&(e.origin=this,this.document.event_manager.trigger(e))}_update_event_callbacks(){null!=this.document?this.document.event_manager.subscribed_models.add(this):o.logger.warn(\"WARNING: Document not defined for updating event callbacks\")}_update_property_callbacks(){const e=e=>{const[t,s=null]=e.split(\":\");return null!=s?this.properties[s][t]:this[t]};for(const[t,s]of this._js_callbacks){const n=e(t);for(const e of s)this.disconnect(n,e)}this._js_callbacks.clear();for(const[t,s]of r.entries(this.js_property_callbacks)){const n=s.map(e=>()=>e.execute(this));this._js_callbacks.set(t,n);const c=e(t);for(const e of n)this.connect(c,e)}}_doc_attached(){r.isEmpty(this.js_event_callbacks)&&0==this.subscribed_events.length||this._update_event_callbacks()}_doc_detached(){this.document.event_manager.subscribed_models.delete(this)}select(e){if(a.isString(e))return[...this.references()].filter(t=>t instanceof l&&t.name===e);if(e.prototype instanceof c.HasProps)return[...this.references()].filter(t=>t instanceof e);throw new Error(\"invalid selector\")}select_one(e){const t=this.select(e);switch(t.length){case 0:return null;case 1:return t[0];default:throw new Error(\"found more than one object matching given selector\")}}}s.Model=l,l.__name__=\"Model\",l.init_Model()},\n function _(e,s,_){Object.defineProperty(_,\"__esModule\",{value:!0});class t{constructor(e,s){this.x_scale=e,this.y_scale=s,this.x_range=this.x_scale.source_range,this.y_range=this.y_scale.source_range,this.ranges=[this.x_range,this.y_range],this.scales=[this.x_scale,this.y_scale]}map_to_screen(e,s){return[this.x_scale.v_compute(e),this.y_scale.v_compute(s)]}map_from_screen(e,s){return[this.x_scale.v_invert(e),this.y_scale.v_invert(s)]}}_.CoordinateTransform=t,t.__name__=\"CoordinateTransform\"},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=t(1),a=t(36),o=t(84),r=t(85),n=t(28),_=i.__importStar(t(18)),h=t(10);class c extends a.AnnotationView{initialize(){super.initialize(),null==this.model.source&&(this.model.source=new r.ColumnDataSource),this.set_data(this.model.source)}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.set_data(this.model.source)),this.connect(this.model.source.streaming,()=>this.set_data(this.model.source)),this.connect(this.model.source.patching,()=>this.set_data(this.model.source)),this.connect(this.model.source.change,()=>this.set_data(this.model.source))}set_data(t){super.set_data(t),this.visuals.warm_cache(t),this.plot_view.request_render()}_map_data(){const{frame:t}=this.plot_view;let e,s,i,a;return\"data\"==this.model.start_units?(e=this.coordinates.x_scale.v_compute(this._x_start),s=this.coordinates.y_scale.v_compute(this._y_start)):(e=t.xview.v_compute(this._x_start),s=t.yview.v_compute(this._y_start)),\"data\"==this.model.end_units?(i=this.coordinates.x_scale.v_compute(this._x_end),a=this.coordinates.y_scale.v_compute(this._y_end)):(i=t.xview.v_compute(this._x_end),a=t.yview.v_compute(this._y_end)),[[e,s],[i,a]]}_render(){const{ctx:t}=this.layer;t.save();const[e,s]=this._map_data();null!=this.model.end&&this._arrow_head(t,\"render\",this.model.end,e,s),null!=this.model.start&&this._arrow_head(t,\"render\",this.model.start,s,e),t.beginPath();const{x:i,y:a,width:o,height:r}=this.plot_view.frame.bbox;t.rect(i,a,o,r),null!=this.model.end&&this._arrow_head(t,\"clip\",this.model.end,e,s),null!=this.model.start&&this._arrow_head(t,\"clip\",this.model.start,s,e),t.closePath(),t.clip(),this._arrow_body(t,e,s),t.restore()}_arrow_head(t,e,s,i,a){for(let o=0,r=this._x_start.length;onew o.OpenHead({})],source:[_.Instance]})}}s.Arrow=d,d.__name__=\"Arrow\",d.init_Arrow()},\n function _(i,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const t=i(1),o=i(36),l=i(74),n=i(28),h=t.__importStar(i(18));class a extends o.Annotation{constructor(i){super(i)}static init_ArrowHead(){this.define({size:[h.Number,25]})}initialize(){super.initialize(),this.visuals=new l.Visuals(this)}}s.ArrowHead=a,a.__name__=\"ArrowHead\",a.init_ArrowHead();class r extends a{constructor(i){super(i)}static init_OpenHead(){this.mixins(n.LineVector)}clip(i,e){this.visuals.line.set_vectorize(i,e),i.moveTo(.5*this.size,this.size),i.lineTo(.5*this.size,-2),i.lineTo(-.5*this.size,-2),i.lineTo(-.5*this.size,this.size),i.lineTo(0,0),i.lineTo(.5*this.size,this.size)}render(i,e){this.visuals.line.doit&&(this.visuals.line.set_vectorize(i,e),i.beginPath(),i.moveTo(.5*this.size,this.size),i.lineTo(0,0),i.lineTo(-.5*this.size,this.size),i.stroke())}}s.OpenHead=r,r.__name__=\"OpenHead\",r.init_OpenHead();class z extends a{constructor(i){super(i)}static init_NormalHead(){this.mixins([n.LineVector,n.FillVector]),this.override({fill_color:\"black\"})}clip(i,e){this.visuals.line.set_vectorize(i,e),i.moveTo(.5*this.size,this.size),i.lineTo(.5*this.size,-2),i.lineTo(-.5*this.size,-2),i.lineTo(-.5*this.size,this.size),i.lineTo(.5*this.size,this.size)}render(i,e){this.visuals.fill.doit&&(this.visuals.fill.set_vectorize(i,e),this._normal(i,e),i.fill()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(i,e),this._normal(i,e),i.stroke())}_normal(i,e){i.beginPath(),i.moveTo(.5*this.size,this.size),i.lineTo(0,0),i.lineTo(-.5*this.size,this.size),i.closePath()}}s.NormalHead=z,z.__name__=\"NormalHead\",z.init_NormalHead();class _ extends a{constructor(i){super(i)}static init_VeeHead(){this.mixins([n.LineVector,n.FillVector]),this.override({fill_color:\"black\"})}clip(i,e){this.visuals.line.set_vectorize(i,e),i.moveTo(.5*this.size,this.size),i.lineTo(.5*this.size,-2),i.lineTo(-.5*this.size,-2),i.lineTo(-.5*this.size,this.size),i.lineTo(0,.5*this.size),i.lineTo(.5*this.size,this.size)}render(i,e){this.visuals.fill.doit&&(this.visuals.fill.set_vectorize(i,e),this._vee(i,e),i.fill()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(i,e),this._vee(i,e),i.stroke())}_vee(i,e){i.beginPath(),i.moveTo(.5*this.size,this.size),i.lineTo(0,0),i.lineTo(-.5*this.size,this.size),i.lineTo(0,.5*this.size),i.closePath()}}s.VeeHead=_,_.__name__=\"VeeHead\",_.init_VeeHead();class c extends a{constructor(i){super(i)}static init_TeeHead(){this.mixins(n.LineVector)}render(i,e){this.visuals.line.doit&&(this.visuals.line.set_vectorize(i,e),i.beginPath(),i.moveTo(.5*this.size,0),i.lineTo(-.5*this.size,0),i.stroke())}clip(i,e){}}s.TeeHead=c,c.__name__=\"TeeHead\",c.init_TeeHead()},\n function _(t,n,e){Object.defineProperty(e,\"__esModule\",{value:!0});const s=t(1),o=t(86),r=s.__importStar(t(18)),i=t(8),l=t(13),a=s.__importStar(t(119)),c=t(120),u=t(121);function h(t,n,e){if(i.isArray(t)){const s=t.concat(n);return null!=e&&s.length>e?s.slice(-e):s}if(i.isTypedArray(t)){const s=t.length+n.length;if(null!=e&&s>e){const o=s-e,r=t.length;let i;t.lengthnew _.UnionRenderers]}),this.internal({selection_manager:[c.Instance,t=>new l.SelectionManager({source:t})],inspected:[c.Instance,()=>new g.Selection]})}initialize(){super.initialize(),this._select=new i.Signal0(this,\"select\"),this.inspect=new i.Signal(this,\"inspect\"),this.streaming=new i.Signal0(this,\"streaming\"),this.patching=new i.Signal(this,\"patching\")}get_column(t){const e=this.data[t];return null!=e?e:null}columns(){return h.keys(this.data)}get_length(t=!0){const e=u.uniq(h.values(this.data).map(t=>t.length));switch(e.length){case 0:return null;case 1:return e[0];default:{const n=\"data source has columns of inconsistent lengths\";if(t)return r.logger.warn(n),e.sort()[0];throw new Error(n)}}}get length(){var t;return null!==(t=this.get_length())&&void 0!==t?t:0}clear(){const t={};for(const e of this.columns())t[e]=new this.data[e].constructor(0);this.data=t}}n.ColumnarDataSource=d,d.__name__=\"ColumnarDataSource\",d.init_ColumnarDataSource()},\n function _(e,t,a){Object.defineProperty(a,\"__esModule\",{value:!0});const c=e(1),n=e(81),o=e(88),i=c.__importStar(e(18));class r extends n.Model{constructor(e){super(e)}static init_DataSource(){this.define({selected:[i.Instance,()=>new o.Selection]})}}a.DataSource=r,r.__name__=\"DataSource\",r.init_DataSource()},\n function _(i,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const t=i(1),n=i(81),l=t.__importStar(i(18)),c=i(9),h=i(13);class d extends n.Model{constructor(i){super(i)}get_view(){return this.view}static init_Selection(){this.define({indices:[l.Array,[]],line_indices:[l.Array,[]],multiline_indices:[l.Any,{}]}),this.internal({selected_glyphs:[l.Array,[]],view:[l.Any],image_indices:[l.Array,[]]})}initialize(){super.initialize()}get selected_glyph(){return this.selected_glyphs.length>0?this.selected_glyphs[0]:null}add_to_selected_glyphs(i){this.selected_glyphs.push(i)}update(i,e=!0,s=\"replace\"){switch(s){case\"replace\":this.indices=i.indices,this.line_indices=i.line_indices,this.selected_glyphs=i.selected_glyphs,this.view=i.view,this.multiline_indices=i.multiline_indices,this.image_indices=i.image_indices;break;case\"append\":this.update_through_union(i);break;case\"intersect\":this.update_through_intersection(i);break;case\"subtract\":this.update_through_subtraction(i)}}clear(){this.indices=[],this.line_indices=[],this.multiline_indices={},this.view=null,this.selected_glyphs=[]}is_empty(){return 0==this.indices.length&&0==this.line_indices.length&&0==this.image_indices.length}update_through_union(i){this.indices=c.union(this.indices,i.indices),this.selected_glyphs=c.union(i.selected_glyphs,this.selected_glyphs),this.line_indices=c.union(i.line_indices,this.line_indices),this.view=i.view,this.multiline_indices=h.merge(i.multiline_indices,this.multiline_indices)}update_through_intersection(i){this.indices=c.intersection(this.indices,i.indices),this.selected_glyphs=c.union(i.selected_glyphs,this.selected_glyphs),this.line_indices=c.union(i.line_indices,this.line_indices),this.view=i.view,this.multiline_indices=h.merge(i.multiline_indices,this.multiline_indices)}update_through_subtraction(i){this.indices=c.difference(this.indices,i.indices),this.selected_glyphs=c.union(i.selected_glyphs,this.selected_glyphs),this.line_indices=c.union(i.line_indices,this.line_indices),this.view=i.view,this.multiline_indices=h.merge(i.multiline_indices,this.multiline_indices)}}s.Selection=d,d.__name__=\"Selection\",d.init_Selection()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),n=e(14),o=e(88),c=e(90),r=e(116),l=i.__importStar(e(18));class p extends n.HasProps{constructor(e){super(e),this.inspectors=new Map}static init_SelectionManager(){this.internal({source:[l.Any]})}select(e,t,s,i=\"replace\"){const n=[],o=[];for(const t of e)t instanceof c.GlyphRendererView?n.push(t):t instanceof r.GraphRendererView&&o.push(t);let l=!1;for(const e of o){const n=e.model.selection_policy.hit_test(t,e);l=l||e.model.selection_policy.do_selection(n,e.model,s,i)}if(n.length>0){const e=this.source.selection_policy.hit_test(t,n);l=l||this.source.selection_policy.do_selection(e,this.source,s,i)}return l}inspect(e,t){let s=!1;if(e instanceof c.GlyphRendererView){const i=e.hit_test(t);if(null!=i){s=!i.is_empty();const n=this.get_or_create_inspector(e.model);n.update(i,!0,\"replace\"),this.source.setv({inspected:n},{silent:!0}),this.source.inspect.emit([e,{geometry:t}])}}else if(e instanceof r.GraphRendererView){const i=e.model.inspection_policy.hit_test(t,e);s=s||e.model.inspection_policy.do_inspection(i,t,e,!1,\"replace\")}return s}clear(e){this.source.selected.clear(),null!=e&&this.get_or_create_inspector(e.model).clear()}get_or_create_inspector(e){let t=this.inspectors.get(e);return null==t&&(t=new o.Selection,this.inspectors.set(e,t)),t}}s.SelectionManager=p,p.__name__=\"SelectionManager\",p.init_SelectionManager()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),l=e(91),n=e(92),h=e(110),o=e(111),a=e(113),c=e(114),_=e(24),d=s.__importStar(e(18)),r=e(12),p=e(9),g=e(13),u=e(115),y=e(98),m={fill:{},line:{}},v={fill:{fill_alpha:.3,fill_color:\"grey\"},line:{line_alpha:.3,line_color:\"grey\"}},f={fill:{fill_alpha:.2},line:{}};class w extends l.DataRendererView{async lazy_initialize(){await super.lazy_initialize();const e=this.model.glyph,t=p.includes(e._mixins,\"fill\"),i=p.includes(e._mixins,\"line\"),s=g.clone(e.attributes);function l(l){const n=g.clone(s);return t&&g.extend(n,l.fill),i&&g.extend(n,l.line),new e.constructor(n)}delete s.id,this.glyph=await this.build_glyph_view(e);let{selection_glyph:n}=this.model;null==n?n=l({fill:{},line:{}}):\"auto\"===n&&(n=l(m)),this.selection_glyph=await this.build_glyph_view(n);let{nonselection_glyph:h}=this.model;null==h?h=l({fill:{},line:{}}):\"auto\"===h&&(h=l(f)),this.nonselection_glyph=await this.build_glyph_view(h);const{hover_glyph:o}=this.model;null!=o&&(this.hover_glyph=await this.build_glyph_view(o));const{muted_glyph:a}=this.model;null!=a&&(this.muted_glyph=await this.build_glyph_view(a));const c=l(v);this.decimated_glyph=await this.build_glyph_view(c),this.set_data(!1)}async build_glyph_view(e){return u.build_view(e,{parent:this})}remove(){var e,t;this.glyph.remove(),this.selection_glyph.remove(),this.nonselection_glyph.remove(),null===(e=this.hover_glyph)||void 0===e||e.remove(),null===(t=this.muted_glyph)||void 0===t||t.remove(),this.decimated_glyph.remove(),super.remove()}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.request_render()),this.connect(this.model.glyph.change,()=>this.set_data()),this.connect(this.model.data_source.change,()=>this.set_data()),this.connect(this.model.data_source.streaming,()=>this.set_data()),this.connect(this.model.data_source.patching,e=>this.set_data(!0,e)),this.connect(this.model.data_source.selected.change,()=>this.request_render()),this.connect(this.model.data_source._select,()=>this.request_render()),null!=this.hover_glyph&&this.connect(this.model.data_source.inspect,()=>this.request_render()),this.connect(this.model.properties.view.change,()=>this.set_data()),this.connect(this.model.view.properties.indices.change,()=>this.set_data()),this.connect(this.model.view.properties.masked.change,()=>this.set_visuals()),this.connect(this.model.properties.visible.change,()=>this.plot_view.update_dataranges());const{x_ranges:e,y_ranges:t}=this.plot_view.frame;for(const[,t]of e)t instanceof y.FactorRange&&this.connect(t.change,()=>this.set_data());for(const[,e]of t)e instanceof y.FactorRange&&this.connect(e.change,()=>this.set_data());this.connect(this.model.glyph.transformchange,()=>this.set_data())}_update_masked_indices(){const e=this.glyph.mask_data();return this.model.view.masked=e,e}set_data(e=!0,t=null){const i=this.model.data_source;this.all_indices=this.model.view.indices;const{all_indices:s}=this;this.glyph.set_data(i,s,t),this.set_visuals(),this._update_masked_indices();const{lod_factor:l}=this.plot_model,n=this.all_indices.count;this.decimated=new _.Indices(n);for(let e=0;e!_||_.is_empty()?[]:_.selected_glyph?this.model.view.convert_indices_from_subset(i):_.indices.length>0?_.indices:Object.keys(_.multiline_indices).map(e=>parseInt(e)))()),g=r.filter(i,e=>d.has(t[e])),{lod_threshold:u}=this.plot_model;let y,m,v;if(null!=this.model.document&&this.model.document.interactive_duration()>0&&!e&&null!=u&&t.length>u?(i=[...this.decimated],y=this.decimated_glyph,m=this.decimated_glyph,v=this.selection_glyph):(y=this.model.muted&&null!=this.muted_glyph?this.muted_glyph:this.glyph,m=this.nonselection_glyph,v=this.selection_glyph),null!=this.hover_glyph&&g.length&&(i=p.difference(i,g)),c.length){const e={};for(const t of c)e[t]=!0;const l=new Array,h=new Array;if(this.glyph instanceof n.LineView)for(const i of t)null!=e[i]?l.push(i):h.push(i);else for(const s of i)null!=e[t[s]]?l.push(s):h.push(s);m.render(s,h,this.glyph),v.render(s,l,this.glyph),null!=this.hover_glyph&&(this.glyph instanceof n.LineView?this.hover_glyph.render(s,this.model.view.convert_indices_from_subset(g),this.glyph):this.hover_glyph.render(s,g,this.glyph))}else if(this.glyph instanceof n.LineView)this.hover_glyph&&g.length?this.hover_glyph.render(s,this.model.view.convert_indices_from_subset(g),this.glyph):y.render(s,t,this.glyph);else if(this.glyph instanceof h.PatchView||this.glyph instanceof o.HAreaView||this.glyph instanceof a.VAreaView)if(0==_.selected_glyphs.length||null==this.hover_glyph)y.render(s,t,this.glyph);else for(const e of _.selected_glyphs)e==this.glyph.model&&this.hover_glyph.render(s,t,this.glyph);else y.render(s,i,this.glyph),this.hover_glyph&&g.length&&this.hover_glyph.render(s,g,this.glyph);s.restore()}draw_legend(e,t,i,s,l,n,h,o){null==o&&(o=this.model.get_reference_point(n,h)),this.glyph.draw_legend_for_index(e,{x0:t,x1:i,y0:s,y1:l},o)}hit_test(e){if(!this.model.visible)return null;const t=this.glyph.hit_test(e);return null==t?null:this.model.view.convert_selection_from_subset(t)}}i.GlyphRendererView=w,w.__name__=\"GlyphRendererView\";class b extends l.DataRenderer{constructor(e){super(e)}static init_GlyphRenderer(){this.prototype.default_view=w,this.define({data_source:[d.Instance],view:[d.Instance,()=>new c.CDSView],glyph:[d.Instance],hover_glyph:[d.Instance],nonselection_glyph:[d.Any,\"auto\"],selection_glyph:[d.Any,\"auto\"],muted_glyph:[d.Instance],muted:[d.Boolean,!1]})}initialize(){super.initialize(),null==this.view.source&&(this.view.source=this.data_source,this.view.compute_indices())}get_reference_point(e,t){let i=0;if(null!=e){const s=this.data_source.get_column(e);if(null!=s){const e=r.indexOf(s,t);-1!=e&&(i=e)}}return i}get_selection_manager(){return this.data_source.selection_manager}}i.GlyphRenderer=b,b.__name__=\"GlyphRenderer\",b.init_GlyphRenderer()},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});const a=e(70);class n extends a.RendererView{get xscale(){return this.coordinates.x_scale}get yscale(){return this.coordinates.y_scale}}t.DataRendererView=n,n.__name__=\"DataRendererView\";class s extends a.Renderer{constructor(e){super(e)}static init_DataRenderer(){this.override({level:\"glyph\"})}}t.DataRenderer=s,s.__name__=\"DataRenderer\",s.init_DataRenderer()},\n function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=e(1),n=e(93),l=e(100),_=e(102),r=s.__importStar(e(28)),o=s.__importStar(e(101)),h=e(88);class a extends n.XYGlyphView{initialize(){super.initialize();const{webgl:e}=this.renderer.plot_view.canvas_view;null!=e&&(this.glglyph=new _.LineGL(e.gl,this))}_render(e,i,{sx:t,sy:s}){let n=!1,l=null;this.visuals.line.set_value(e);for(const _ of i){if(n){if(!isFinite(t[_]+s[_])){e.stroke(),e.beginPath(),n=!1,l=_;continue}null!=l&&_-l>1&&(e.stroke(),n=!1)}n?e.lineTo(t[_],s[_]):(e.beginPath(),e.moveTo(t[_],s[_]),n=!0),l=_}n&&e.stroke()}_hit_point(e){const i=new h.Selection,t={x:e.sx,y:e.sy};let s=9999;const n=Math.max(2,this.visuals.line.line_width.value()/2);for(let e=0,l=this.sx.length-1;et/2);a=new Float64Array(_);for(let i=0;i<_;i++)a[i]=e[i]-t[i];r=new Float64Array(_);for(let i=0;i<_;i++)r[i]=e[i]+t[i]}else{a=e,r=new Float64Array(_);for(let t=0;t<_;t++)r[t]=a[t]+i[t]}const l=t.v_compute(a),o=t.v_compute(r);return n?d.map(l,(t,e)=>Math.ceil(Math.abs(o[e]-l[e]))):d.map(l,(t,e)=>Math.abs(o[e]-l[e]))}draw_legend_for_index(t,e,i){}hit_test(t){switch(t.type){case\"point\":if(null!=this._hit_point)return this._hit_point(t);break;case\"span\":if(null!=this._hit_span)return this._hit_span(t);break;case\"rect\":if(null!=this._hit_rect)return this._hit_rect(t);break;case\"poly\":if(null!=this._hit_poly)return this._hit_poly(t)}return this._nohit_warned.has(t.type)||(o.logger.debug(`'${t.type}' selection not available for ${this.model.type}`),this._nohit_warned.add(t.type)),null}_hit_rect_against_index(t){const{sx0:e,sx1:i,sy0:s,sy1:n}=t,[a,r]=this.renderer.coordinates.x_scale.r_invert(e,i),[_,l]=this.renderer.coordinates.y_scale.r_invert(s,n),o=[...this.index.indices({x0:a,x1:r,y0:_,y1:l})];return new p.Selection({indices:o})}_project_data(){}set_data(t,e,i){var s,a;const{x_range:r,y_range:_}=this.renderer.coordinates;this._data_size=null!==(s=t.get_length())&&void 0!==s?s:1;for(const i of this.model){if(!(i instanceof n.VectorSpec))continue;if(i.optional&&null==i.spec.value&&!i.dirty)continue;const s=i.attr,a=i.array(t);let l=e.select(a);if(i instanceof n.BaseCoordinateSpec){const t=\"x\"==i.dimension?r:_;if(t instanceof u.FactorRange)if(i instanceof n.CoordinateSpec)l=t.v_synthetic(l);else if(i instanceof n.CoordinateSeqSpec)for(let e=0;e>1;n[s]>e?i=s:t=s+1}return n[t]}class x extends i.default{search_indices(e,n,t,i){if(this._pos!==this._boxes.length)throw new Error(\"Data not yet indexed - call index.finish().\");let o=this._boxes.length-4;const x=[],h=new s.Indices(this.numItems);for(;void 0!==o;){const s=Math.min(o+4*this.nodeSize,d(o,this._levelBounds));for(let d=o;d>2];tthis._boxes[d+2]||n>this._boxes[d+3]||(o<4*this.numItems?h.set(s):x.push(s)))}o=x.pop()}return h}}x.__name__=\"_FlatBush\";class h{constructor(e){this.index=null,e>0&&(this.index=new x(e))}add(e,n,t,i){var s;null===(s=this.index)||void 0===s||s.add(e,n,t,i)}add_empty(){var e;null===(e=this.index)||void 0===e||e.add(1/0,1/0,-1/0,-1/0)}finish(){var e;null===(e=this.index)||void 0===e||e.finish()}_normalize(e){let{x0:n,y0:t,x1:i,y1:s}=e;return n>i&&([n,i]=[i,n]),t>s&&([t,s]=[s,t]),{x0:n,y0:t,x1:i,y1:s}}get bbox(){if(null==this.index)return o.empty();{const{minX:e,minY:n,maxX:t,maxY:i}=this.index;return{x0:e,y0:n,x1:t,y1:i}}}indices(e){if(null==this.index)return new s.Indices(0);{const{x0:n,y0:t,x1:i,y1:s}=this._normalize(e);return this.index.search_indices(n,t,i,s)}}bounds(e){const n=o.empty();for(const t of this.indices(e)){const e=this.index._boxes,i=e[4*t+0],s=e[4*t+1],o=e[4*t+2],d=e[4*t+3];on.x1&&(n.x1=i),dn.y1&&(n.y1=s)}return n}}t.SpatialIndex=h,h.__name__=\"SpatialIndex\"},\n function _(t,s,i){Object.defineProperty(i,\"__esModule\",{value:!0});const e=t(1).__importDefault(t(97)),h=[Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];class n{static from(t){if(!(t instanceof ArrayBuffer))throw new Error(\"Data must be an instance of ArrayBuffer.\");const[s,i]=new Uint8Array(t,0,2);if(251!==s)throw new Error(\"Data does not appear to be in a Flatbush format.\");if(i>>4!=3)throw new Error(`Got v${i>>4} data when expected v3.`);const[e]=new Uint16Array(t,2,1),[o]=new Uint32Array(t,4,1);return new n(o,e,h[15&i],t)}constructor(t,s=16,i=Float64Array,n){if(void 0===t)throw new Error(\"Missing required argument: numItems.\");if(isNaN(t)||t<=0)throw new Error(`Unpexpected numItems value: ${t}.`);this.numItems=+t,this.nodeSize=Math.min(Math.max(+s,2),65535);let o=t,r=o;this._levelBounds=[4*o];do{o=Math.ceil(o/this.nodeSize),r+=o,this._levelBounds.push(4*r)}while(1!==o);this.ArrayType=i||Float64Array,this.IndexArrayType=r<16384?Uint16Array:Uint32Array;const a=h.indexOf(this.ArrayType),_=4*r*this.ArrayType.BYTES_PER_ELEMENT;if(a<0)throw new Error(`Unexpected typed array class: ${i}.`);n&&n instanceof ArrayBuffer?(this.data=n,this._boxes=new this.ArrayType(this.data,8,4*r),this._indices=new this.IndexArrayType(this.data,8+_,r),this._pos=4*r,this.minX=this._boxes[this._pos-4],this.minY=this._boxes[this._pos-3],this.maxX=this._boxes[this._pos-2],this.maxY=this._boxes[this._pos-1]):(this.data=new ArrayBuffer(8+_+r*this.IndexArrayType.BYTES_PER_ELEMENT),this._boxes=new this.ArrayType(this.data,8,4*r),this._indices=new this.IndexArrayType(this.data,8+_,r),this._pos=0,this.minX=1/0,this.minY=1/0,this.maxX=-1/0,this.maxY=-1/0,new Uint8Array(this.data,0,2).set([251,48+a]),new Uint16Array(this.data,2,1)[0]=s,new Uint32Array(this.data,4,1)[0]=t),this._queue=new e.default}add(t,s,i,e){const h=this._pos>>2;return this._indices[h]=h,this._boxes[this._pos++]=t,this._boxes[this._pos++]=s,this._boxes[this._pos++]=i,this._boxes[this._pos++]=e,tthis.maxX&&(this.maxX=i),e>this.maxY&&(this.maxY=e),h}finish(){if(this._pos>>2!==this.numItems)throw new Error(`Added ${this._pos>>2} items when expected ${this.numItems}.`);if(this.numItems<=this.nodeSize)return this._boxes[this._pos++]=this.minX,this._boxes[this._pos++]=this.minY,this._boxes[this._pos++]=this.maxX,void(this._boxes[this._pos++]=this.maxY);const t=this.maxX-this.minX,s=this.maxY-this.minY,i=new Uint32Array(this.numItems);for(let e=0;e=Math.floor(n/o))return;const r=s[h+n>>1];let _=h-1,d=n+1;for(;;){do{_++}while(s[_]r);if(_>=d)break;a(s,i,e,_,d)}t(s,i,e,h,d,o),t(s,i,e,d+1,n,o)}(i,this._boxes,this._indices,0,this.numItems-1,this.nodeSize);for(let t=0,s=0;t>2]=t,this._boxes[this._pos++]=e,this._boxes[this._pos++]=h,this._boxes[this._pos++]=n,this._boxes[this._pos++]=o}}}search(t,s,i,e,h){if(this._pos!==this._boxes.length)throw new Error(\"Data not yet indexed - call index.finish().\");let n=this._boxes.length-4;const o=[],a=[];for(;void 0!==n;){const _=Math.min(n+4*this.nodeSize,r(n,this._levelBounds));for(let r=n;r<_;r+=4){const _=0|this._indices[r>>2];ithis._boxes[r+2]||s>this._boxes[r+3]||(n<4*this.numItems?(void 0===h||h(_))&&a.push(_):o.push(_)))}n=o.pop()}return a}neighbors(t,s,i=1/0,e=1/0,h){if(this._pos!==this._boxes.length)throw new Error(\"Data not yet indexed - call index.finish().\");let n=this._boxes.length-4;const a=this._queue,_=[],d=e*e;for(;void 0!==n;){const e=Math.min(n+4*this.nodeSize,r(n,this._levelBounds));for(let i=n;i>2],r=o(t,this._boxes[i],this._boxes[i+2]),_=o(s,this._boxes[i+1],this._boxes[i+3]),d=r*r+_*_;n<4*this.numItems?(void 0===h||h(e))&&a.push(-e-1,d):a.push(e,d)}for(;a.length&&a.peek()<0;){if(a.peekValue()>d)return a.clear(),_;if(_.push(-a.pop()-1),_.length===i)return a.clear(),_}n=a.pop()}return a.clear(),_}}function o(t,s,i){return t>1;s[h]>t?e=h:i=h+1}return s[i]}function a(t,s,i,e,h){const n=t[e];t[e]=t[h],t[h]=n;const o=4*e,r=4*h,a=s[o],_=s[o+1],d=s[o+2],x=s[o+3];s[o]=s[r],s[o+1]=s[r+1],s[o+2]=s[r+2],s[o+3]=s[r+3],s[r]=a,s[r+1]=_,s[r+2]=d,s[r+3]=x;const l=i[e];i[e]=i[h],i[h]=l}function _(t,s){let i=t^s,e=65535^i,h=65535^(t|s),n=t&(65535^s),o=i|e>>1,r=i>>1^i,a=h>>1^e&n>>1^h,_=i&h>>1^n>>1^n;i=o,e=r,h=a,n=_,o=i&i>>2^e&e>>2,r=i&e>>2^e&(i^e)>>2,a^=i&h>>2^e&n>>2,_^=e&h>>2^(i^e)&n>>2,i=o,e=r,h=a,n=_,o=i&i>>4^e&e>>4,r=i&e>>4^e&(i^e)>>4,a^=i&h>>4^e&n>>4,_^=e&h>>4^(i^e)&n>>4,i=o,e=r,h=a,n=_,a^=i&h>>8^e&n>>8,_^=e&h>>8^(i^e)&n>>8,i=a^a>>1,e=_^_>>1;let d=t^s,x=e|65535^(d|i);return d=16711935&(d|d<<8),d=252645135&(d|d<<4),d=858993459&(d|d<<2),d=1431655765&(d|d<<1),x=16711935&(x|x<<8),x=252645135&(x|x<<4),x=858993459&(x|x<<2),x=1431655765&(x|x<<1),(x<<1|d)>>>0}i.default=n},\n function _(s,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});i.default=class{constructor(){this.ids=[],this.values=[],this.length=0}clear(){this.length=0}push(s,t){let i=this.length++;for(this.ids[i]=s,this.values[i]=t;i>0;){const s=i-1>>1,h=this.values[s];if(t>=h)break;this.ids[i]=this.ids[s],this.values[i]=h,i=s}this.ids[i]=s,this.values[i]=t}pop(){if(0===this.length)return;const s=this.ids[0];if(this.length--,this.length>0){const s=this.ids[0]=this.ids[this.length],t=this.values[0]=this.values[this.length],i=this.length>>1;let h=0;for(;h=t)break;this.ids[h]=e,this.values[h]=l,h=s}this.ids[h]=s,this.values[h]=t}return s}peek(){if(0!==this.length)return this.ids[0]}peekValue(){if(0!==this.length)return this.values[0]}}},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const s=t(1),i=t(99),r=s.__importStar(t(18)),a=t(24),o=t(9),p=t(8),g=t(11);function c(t,e,n=0){const s=new Map;for(let i=0;ia.get(t).value));r.set(t,{value:u/i,mapping:a}),p+=i+e+l}return[r,(a.size-1)*e+g]}function u(t,e,n,s,i=0){var r;const a=new Map,p=new Map;for(const[e,n,s]of t){const t=null!==(r=p.get(e))&&void 0!==r?r:[];p.set(e,[...t,[n,s]])}let g=i,c=0;for(const[t,i]of p){const r=i.length,[p,u]=l(i,n,s,g);c+=u;const h=o.sum(i.map(([t])=>p.get(t).value));a.set(t,{value:h/r,mapping:p}),g+=r+e+u}return[a,(p.size-1)*e+c]}n.map_one_level=c,n.map_two_levels=l,n.map_three_levels=u;class h extends i.Range{constructor(t){super(t)}static init_FactorRange(){this.define({factors:[r.Array,[]],factor_padding:[r.Number,0],subgroup_padding:[r.Number,.8],group_padding:[r.Number,1.4],range_padding:[r.Number,0],range_padding_units:[r.PaddingUnits,\"percent\"],start:[r.Number],end:[r.Number]}),this.internal({levels:[r.Number],mids:[r.Array,null],tops:[r.Array,null]})}get min(){return this.start}get max(){return this.end}initialize(){super.initialize(),this._init(!0)}connect_signals(){super.connect_signals(),this.connect(this.properties.factors.change,()=>this.reset()),this.connect(this.properties.factor_padding.change,()=>this.reset()),this.connect(this.properties.group_padding.change,()=>this.reset()),this.connect(this.properties.subgroup_padding.change,()=>this.reset()),this.connect(this.properties.range_padding.change,()=>this.reset()),this.connect(this.properties.range_padding_units.change,()=>this.reset())}reset(){this._init(!1),this.change.emit()}_lookup(t){switch(t.length){case 1:{const[e]=t,n=this._mapping.get(e);return null!=n?n.value:NaN}case 2:{const[e,n]=t,s=this._mapping.get(e);if(null!=s){const t=s.mapping.get(n);if(null!=t)return t.value}return NaN}case 3:{const[e,n,s]=t,i=this._mapping.get(e);if(null!=i){const t=i.mapping.get(n);if(null!=t){const e=t.mapping.get(s);if(null!=e)return e.value}}return NaN}default:g.unreachable()}}synthetic(t){if(p.isNumber(t))return t;if(p.isString(t))return this._lookup([t]);let e=0;const n=t[t.length-1];return p.isNumber(n)&&(e=n,t=t.slice(0,-1)),this._lookup(t)+e}v_synthetic(t){const e=t.length,n=new a.NumberArray(e);for(let s=0;s{if(o.every(this.factors,p.isString)){const t=this.factors,[e,n]=c(t,this.factor_padding);return{levels:1,mapping:e,tops:null,mids:null,inside_padding:n}}if(o.every(this.factors,t=>p.isArray(t)&&2==t.length&&p.isString(t[0])&&p.isString(t[1]))){const t=this.factors,[e,n]=l(t,this.group_padding,this.factor_padding),s=[...e.keys()];return{levels:2,mapping:e,tops:s,mids:null,inside_padding:n}}if(o.every(this.factors,t=>p.isArray(t)&&3==t.length&&p.isString(t[0])&&p.isString(t[1])&&p.isString(t[2]))){const t=this.factors,[e,n]=u(t,this.group_padding,this.subgroup_padding,this.factor_padding),s=[...e.keys()],i=[];for(const[t,n]of e)for(const e of n.mapping.keys())i.push([t,e]);return{levels:3,mapping:e,tops:s,mids:i,inside_padding:n}}g.unreachable()})();this._mapping=n,this.tops=s,this.mids=i;let a=0,h=this.factors.length+r;if(\"percent\"==this.range_padding_units){const t=(h-a)*this.range_padding/2;a-=t,h+=t}else a-=this.range_padding,h+=this.range_padding;this.setv({start:a,end:h,levels:e},{silent:t}),\"auto\"==this.bounds&&this.setv({bounds:[a,h]},{silent:!0})}}n.FactorRange=h,h.__name__=\"FactorRange\",h.init_FactorRange()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=e(81),a=n.__importStar(e(18));class r extends s.Model{constructor(e){super(e),this.have_updated_interactively=!1}static init_Range(){this.define({bounds:[a.Any],min_interval:[a.Any],max_interval:[a.Any]}),this.internal({plots:[a.Array,[]]})}get is_reversed(){return this.start>this.end}get is_valid(){return!isNaN(this.min)&&!isNaN(this.max)}}i.Range=r,r.__name__=\"Range\",r.init_Range()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1).__importStar(e(101));i.generic_line_legend=function(e,t,{x0:i,x1:n,y0:c,y1:o},r){t.save(),t.beginPath(),t.moveTo(i,(c+o)/2),t.lineTo(n,(c+o)/2),e.line.doit&&(e.line.set_vectorize(t,r),t.stroke()),t.restore()},i.generic_area_legend=function(e,t,{x0:i,x1:n,y0:c,y1:o},r){const l=.1*Math.abs(n-i),a=.1*Math.abs(o-c),s=i+l,_=n-l,h=c+a,v=o-a;e.fill.doit&&(e.fill.set_vectorize(t,r),t.fillRect(s,h,_-s,v-h)),null!=e.hatch&&e.hatch.doit&&(e.hatch.set_vectorize(t,r),t.fillRect(s,h,_-s,v-h)),e.line&&e.line.doit&&(t.beginPath(),t.rect(s,h,_-s,v-h),e.line.set_vectorize(t,r),t.stroke())},i.line_interpolation=function(e,t,i,c,o,r){const{sx:l,sy:a}=t;let s,_,h,v;\"point\"==t.type?([h,v]=e.yscale.r_invert(a-1,a+1),[s,_]=e.xscale.r_invert(l-1,l+1)):\"v\"==t.direction?([h,v]=e.yscale.r_invert(a,a),[s,_]=[Math.min(i-1,o-1),Math.max(i+1,o+1)]):([s,_]=e.xscale.r_invert(l,l),[h,v]=[Math.min(c-1,r-1),Math.max(c+1,r+1)]);const{x,y}=n.check_2_segments_intersect(s,h,_,v,i,c,o,r);return[x,y]}},\n function _(t,n,e){function i(t,n){return(t.x-n.x)**2+(t.y-n.y)**2}function r(t,n,e){const r=i(n,e);if(0==r)return i(t,n);const s=((t.x-n.x)*(e.x-n.x)+(t.y-n.y)*(e.y-n.y))/r;if(s<0)return i(t,n);if(s>1)return i(t,e);return i(t,{x:n.x+s*(e.x-n.x),y:n.y+s*(e.y-n.y)})}Object.defineProperty(e,\"__esModule\",{value:!0}),e.point_in_poly=function(t,n,e,i){let r=!1,s=e[e.length-1],o=i[i.length-1];for(let u=0;u0&&_<1&&l>0&&l<1,x:t+_*(e-t),y:n+_*(i-n)}}}},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=t(103),a=t(107),n=t(108),o=t(109),_=t(22);class h{constructor(t){this._atlas=new Map,this._width=256,this._height=256,this.tex=new i.Texture2d(t),this.tex.set_wrapping(t.REPEAT,t.REPEAT),this.tex.set_interpolation(t.NEAREST,t.NEAREST),this.tex.set_size([this._width,this._height],t.RGBA),this.tex.set_data([0,0],[this._width,this._height],new Uint8Array(4*this._width*this._height)),this.get_atlas_data([1])}get_atlas_data(t){const e=t.join(\"-\");let s=this._atlas.get(e);if(null==s){const[i,a]=this.make_pattern(t),n=this._atlas.size;this.tex.set_data([0,n],[this._width,1],new Uint8Array(i.map(t=>t+10))),s=[n/this._height,a],this._atlas.set(e,s)}return s}make_pattern(t){t.length>1&&t.length%2&&(t=t.concat(t));let e=0;for(const s of t)e+=s;const s=[];let i=0;for(let e=0,a=t.length+2;es[r]?-1:0,o=s[r-1],i=s[r]),n[4*t+0]=s[r],n[4*t+1]=_,n[4*t+2]=o,n[4*t+3]=i}return[n,e]}}h.__name__=\"DashAtlas\";const r={miter:0,round:1,bevel:2},l={\"\":0,none:0,\".\":0,round:1,\")\":1,\"(\":1,o:1,\"triangle in\":2,\"<\":2,\"triangle out\":3,\">\":3,square:4,\"[\":4,\"]\":4,\"=\":4,butt:5,\"|\":5};class g extends a.BaseGLGlyph{init(){const{gl:t}=this;this._scale_aspect=0;const e=n.vertex_shader,s=o.fragment_shader;this.prog=new i.Program(t),this.prog.set_shaders(e,s),this.index_buffer=new i.IndexBuffer(t),this.vbo_position=new i.VertexBuffer(t),this.vbo_tangents=new i.VertexBuffer(t),this.vbo_segment=new i.VertexBuffer(t),this.vbo_angles=new i.VertexBuffer(t),this.vbo_texcoord=new i.VertexBuffer(t),this.dash_atlas=new h(t)}draw(t,e,s){const i=e.glglyph;if(i.data_changed&&(i._set_data(),i.data_changed=!1),this.visuals_changed&&(this._set_visuals(),this.visuals_changed=!1),i._update_scale(1,1),this._scale_aspect=1,this.prog.set_attribute(\"a_position\",\"vec2\",i.vbo_position),this.prog.set_attribute(\"a_tangents\",\"vec4\",i.vbo_tangents),this.prog.set_attribute(\"a_segment\",\"vec2\",i.vbo_segment),this.prog.set_attribute(\"a_angles\",\"vec2\",i.vbo_angles),this.prog.set_attribute(\"a_texcoord\",\"vec2\",i.vbo_texcoord),this.prog.set_uniform(\"u_length\",\"float\",[i.cumsum]),this.prog.set_texture(\"u_dash_atlas\",this.dash_atlas.tex),this.prog.set_uniform(\"u_pixel_ratio\",\"float\",[s.pixel_ratio]),this.prog.set_uniform(\"u_canvas_size\",\"vec2\",[s.width,s.height]),this.prog.set_uniform(\"u_scale_aspect\",\"vec2\",[1,1]),this.prog.set_uniform(\"u_scale_length\",\"float\",[Math.sqrt(2)]),this.I_triangles=i.I_triangles,this.I_triangles.length<65535)this.index_buffer.set_size(2*this.I_triangles.length),this.index_buffer.set_data(0,new Uint16Array(this.I_triangles)),this.prog.draw(this.gl.TRIANGLES,this.index_buffer);else{t=Array.from(this.I_triangles);const e=this.I_triangles.length,s=64008,a=[];for(let t=0,i=Math.ceil(e/s);t1)for(let e=0;e0||console.log(`Variable ${t} is not an active attribute`));else if(this._unset_variables.has(t)&&this._unset_variables.delete(t),this.activate(),i instanceof s.VertexBuffer){const[s,n]=this.ATYPEINFO[e],h=\"vertexAttribPointer\",l=[s,n,!1,a,r];this._attributes.set(t,[i.handle,o,h,l])}else{const s=this.ATYPEMAP[e];this._attributes.set(t,[null,o,s,i])}}_pre_draw(){this.activate();for(const[t,e,i]of this._samplers.values())this.gl.activeTexture(this.gl.TEXTURE0+i),this.gl.bindTexture(t,e);for(const[t,e,i,s]of this._attributes.values())null!=t?(this.gl.bindBuffer(this.gl.ARRAY_BUFFER,t),this.gl.enableVertexAttribArray(e),this.gl[i].apply(this.gl,[e,...s])):(this.gl.bindBuffer(this.gl.ARRAY_BUFFER,null),this.gl.disableVertexAttribArray(e),this.gl[i].apply(this.gl,[e,...s]));this._validated||(this._validated=!0,this._validate())}_validate(){if(this._unset_variables.size&&console.log(\"Program has unset variables: \"+this._unset_variables),this.gl.validateProgram(this.handle),!this.gl.getProgramParameter(this.handle,this.gl.VALIDATE_STATUS))throw console.log(this.gl.getProgramInfoLog(this.handle)),new Error(\"Program validation error\")}draw(t,e){if(!this._linked)throw new Error(\"Cannot draw program if code has not been set\");if(e instanceof s.IndexBuffer){this._pre_draw(),e.activate();const i=e.buffer_size/2,s=this.gl.UNSIGNED_SHORT;this.gl.drawElements(t,i,s,0),e.deactivate()}else{const[i,s]=e;0!=s&&(this._pre_draw(),this.gl.drawArrays(t,i,s))}}}i.Program=a,a.__name__=\"Program\"},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});class i{constructor(e){this.gl=e,this._usage=35048,this.buffer_size=0,this.handle=this.gl.createBuffer()}delete(){this.gl.deleteBuffer(this.handle)}activate(){this.gl.bindBuffer(this._target,this.handle)}deactivate(){this.gl.bindBuffer(this._target,null)}set_size(e){e!=this.buffer_size&&(this.activate(),this.gl.bufferData(this._target,e,this._usage),this.buffer_size=e)}set_data(e,t){this.activate(),this.gl.bufferSubData(this._target,e,t)}}s.Buffer=i,i.__name__=\"Buffer\";class r extends i{constructor(){super(...arguments),this._target=34962}}s.VertexBuffer=r,r.__name__=\"VertexBuffer\";class a extends i{constructor(){super(...arguments),this._target=34963}}s.IndexBuffer=a,a.__name__=\"IndexBuffer\"},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const a=t(11);class r{constructor(t){this.gl=t,this._target=3553,this._types={Int8Array:5120,Uint8Array:5121,Int16Array:5122,Uint16Array:5123,Int32Array:5124,Uint32Array:5125,Float32Array:5126},this.handle=this.gl.createTexture()}delete(){this.gl.deleteTexture(this.handle)}activate(){this.gl.bindTexture(this._target,this.handle)}deactivate(){this.gl.bindTexture(this._target,0)}_get_alignment(t){const e=[4,8,2,1];for(const i of e)if(t%i==0)return i;a.unreachable()}set_wrapping(t,e){this.activate(),this.gl.texParameterf(this._target,this.gl.TEXTURE_WRAP_S,t),this.gl.texParameterf(this._target,this.gl.TEXTURE_WRAP_T,e)}set_interpolation(t,e){this.activate(),this.gl.texParameterf(this._target,this.gl.TEXTURE_MIN_FILTER,t),this.gl.texParameterf(this._target,this.gl.TEXTURE_MAG_FILTER,e)}set_size([t,e],i){var a,r,s;t==(null===(a=this._shape_format)||void 0===a?void 0:a.width)&&e==(null===(r=this._shape_format)||void 0===r?void 0:r.height)&&i==(null===(s=this._shape_format)||void 0===s?void 0:s.format)||(this._shape_format={width:t,height:e,format:i},this.activate(),this.gl.texImage2D(this._target,0,i,t,e,0,i,this.gl.UNSIGNED_BYTE,null))}set_data(t,[e,i],a){this.activate();const{format:r}=this._shape_format,[s,h]=t,l=this._types[a.constructor.name];if(null==l)throw new Error(`Type ${a.constructor.name} not allowed for texture`);const _=this._get_alignment(e);4!=_&&this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT,_),this.gl.texSubImage2D(this._target,0,s,h,e,i,r,l,a),4!=_&&this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT,4)}}i.Texture2d=r,r.__name__=\"Texture2d\"},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});class s{constructor(e,t){this.gl=e,this.glyph=t,this.nvertices=0,this.size_changed=!1,this.data_changed=!1,this.visuals_changed=!1,this.init()}set_data_changed(){const{data_size:e}=this.glyph;e!=this.nvertices&&(this.nvertices=e,this.size_changed=!0),this.data_changed=!0}set_visuals_changed(){this.visuals_changed=!0}render(e,t,i){if(0==t.length)return!0;const{width:s,height:h}=this.glyph.renderer.plot_view.canvas_view.webgl.canvas,a={pixel_ratio:this.glyph.renderer.plot_view.canvas_view.pixel_ratio,width:s,height:h};return this.draw(t,i,a),!0}}i.BaseGLGlyph=s,s.__name__=\"BaseGLGlyph\"},\n function _(n,e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.vertex_shader=\"\\nprecision mediump float;\\n\\nconst float PI = 3.14159265358979323846264;\\nconst float THETA = 15.0 * 3.14159265358979323846264/180.0;\\n\\nuniform float u_pixel_ratio;\\nuniform vec2 u_canvas_size, u_offset;\\nuniform vec2 u_scale_aspect;\\nuniform float u_scale_length;\\n\\nuniform vec4 u_color;\\nuniform float u_antialias;\\nuniform float u_length;\\nuniform float u_linewidth;\\nuniform float u_dash_index;\\nuniform float u_closed;\\n\\nattribute vec2 a_position;\\nattribute vec4 a_tangents;\\nattribute vec2 a_segment;\\nattribute vec2 a_angles;\\nattribute vec2 a_texcoord;\\n\\nvarying vec4 v_color;\\nvarying vec2 v_segment;\\nvarying vec2 v_angles;\\nvarying vec2 v_texcoord;\\nvarying vec2 v_miter;\\nvarying float v_length;\\nvarying float v_linewidth;\\n\\nfloat cross(in vec2 v1, in vec2 v2)\\n{\\n return v1.x*v2.y - v1.y*v2.x;\\n}\\n\\nfloat signed_distance(in vec2 v1, in vec2 v2, in vec2 v3)\\n{\\n return cross(v2-v1,v1-v3) / length(v2-v1);\\n}\\n\\nvoid rotate( in vec2 v, in float alpha, out vec2 result )\\n{\\n float c = cos(alpha);\\n float s = sin(alpha);\\n result = vec2( c*v.x - s*v.y,\\n s*v.x + c*v.y );\\n}\\n\\nvoid main()\\n{\\n bool closed = (u_closed > 0.0);\\n\\n // Attributes and uniforms to varyings\\n v_color = u_color;\\n v_linewidth = u_linewidth;\\n v_segment = a_segment * u_scale_length;\\n v_length = u_length * u_scale_length;\\n\\n // Scale to map to pixel coordinates. The original algorithm from the paper\\n // assumed isotropic scale. We obviously do not have this.\\n vec2 abs_scale_aspect = abs(u_scale_aspect);\\n vec2 abs_scale = u_scale_length * abs_scale_aspect;\\n\\n // Correct angles for aspect ratio\\n vec2 av;\\n av = vec2(1.0, tan(a_angles.x)) / abs_scale_aspect;\\n v_angles.x = atan(av.y, av.x);\\n av = vec2(1.0, tan(a_angles.y)) / abs_scale_aspect;\\n v_angles.y = atan(av.y, av.x);\\n\\n // Thickness below 1 pixel are represented using a 1 pixel thickness\\n // and a modified alpha\\n v_color.a = min(v_linewidth, v_color.a);\\n v_linewidth = max(v_linewidth, 1.0);\\n\\n // If color is fully transparent we just will discard the fragment anyway\\n if( v_color.a <= 0.0 ) {\\n gl_Position = vec4(0.0,0.0,0.0,1.0);\\n return;\\n }\\n\\n // This is the actual half width of the line\\n float w = ceil(u_antialias+v_linewidth)/2.0;\\n\\n vec2 position = a_position;\\n\\n vec2 t1 = normalize(a_tangents.xy * abs_scale_aspect); // note the scaling for aspect ratio here\\n vec2 t2 = normalize(a_tangents.zw * abs_scale_aspect);\\n float u = a_texcoord.x;\\n float v = a_texcoord.y;\\n vec2 o1 = vec2( +t1.y, -t1.x);\\n vec2 o2 = vec2( +t2.y, -t2.x);\\n\\n // This is a join\\n // ----------------------------------------------------------------\\n if( t1 != t2 ) {\\n float angle = atan (t1.x*t2.y-t1.y*t2.x, t1.x*t2.x+t1.y*t2.y); // Angle needs recalculation for some reason\\n vec2 t = normalize(t1+t2);\\n vec2 o = vec2( + t.y, - t.x);\\n\\n if ( u_dash_index > 0.0 )\\n {\\n // Broken angle\\n // ----------------------------------------------------------------\\n if( (abs(angle) > THETA) ) {\\n position += v * w * o / cos(angle/2.0);\\n float s = sign(angle);\\n if( angle < 0.0 ) {\\n if( u == +1.0 ) {\\n u = v_segment.y + v * w * tan(angle/2.0);\\n if( v == 1.0 ) {\\n position -= 2.0 * w * t1 / sin(angle);\\n u -= 2.0 * w / sin(angle);\\n }\\n } else {\\n u = v_segment.x - v * w * tan(angle/2.0);\\n if( v == 1.0 ) {\\n position += 2.0 * w * t2 / sin(angle);\\n u += 2.0*w / sin(angle);\\n }\\n }\\n } else {\\n if( u == +1.0 ) {\\n u = v_segment.y + v * w * tan(angle/2.0);\\n if( v == -1.0 ) {\\n position += 2.0 * w * t1 / sin(angle);\\n u += 2.0 * w / sin(angle);\\n }\\n } else {\\n u = v_segment.x - v * w * tan(angle/2.0);\\n if( v == -1.0 ) {\\n position -= 2.0 * w * t2 / sin(angle);\\n u -= 2.0*w / sin(angle);\\n }\\n }\\n }\\n // Continuous angle\\n // ------------------------------------------------------------\\n } else {\\n position += v * w * o / cos(angle/2.0);\\n if( u == +1.0 ) u = v_segment.y;\\n else u = v_segment.x;\\n }\\n }\\n\\n // Solid line\\n // --------------------------------------------------------------------\\n else\\n {\\n position.xy += v * w * o / cos(angle/2.0);\\n if( angle < 0.0 ) {\\n if( u == +1.0 ) {\\n u = v_segment.y + v * w * tan(angle/2.0);\\n } else {\\n u = v_segment.x - v * w * tan(angle/2.0);\\n }\\n } else {\\n if( u == +1.0 ) {\\n u = v_segment.y + v * w * tan(angle/2.0);\\n } else {\\n u = v_segment.x - v * w * tan(angle/2.0);\\n }\\n }\\n }\\n\\n // This is a line start or end (t1 == t2)\\n // ------------------------------------------------------------------------\\n } else {\\n position += v * w * o1;\\n if( u == -1.0 ) {\\n u = v_segment.x - w;\\n position -= w * t1;\\n } else {\\n u = v_segment.y + w;\\n position += w * t2;\\n }\\n }\\n\\n // Miter distance\\n // ------------------------------------------------------------------------\\n vec2 t;\\n vec2 curr = a_position * abs_scale;\\n if( a_texcoord.x < 0.0 ) {\\n vec2 next = curr + t2*(v_segment.y-v_segment.x);\\n\\n rotate( t1, +v_angles.x/2.0, t);\\n v_miter.x = signed_distance(curr, curr+t, position);\\n\\n rotate( t2, +v_angles.y/2.0, t);\\n v_miter.y = signed_distance(next, next+t, position);\\n } else {\\n vec2 prev = curr - t1*(v_segment.y-v_segment.x);\\n\\n rotate( t1, -v_angles.x/2.0,t);\\n v_miter.x = signed_distance(prev, prev+t, position);\\n\\n rotate( t2, -v_angles.y/2.0,t);\\n v_miter.y = signed_distance(curr, curr+t, position);\\n }\\n\\n if (!closed && v_segment.x <= 0.0) {\\n v_miter.x = 1e10;\\n }\\n if (!closed && v_segment.y >= v_length)\\n {\\n v_miter.y = 1e10;\\n }\\n\\n v_texcoord = vec2( u, v*w );\\n\\n // Calculate position in device coordinates. Note that we\\n // already scaled with abs scale above.\\n vec2 normpos = position * sign(u_scale_aspect);\\n normpos += 0.5; // make up for Bokeh's offset\\n normpos /= u_canvas_size / u_pixel_ratio; // in 0..1\\n gl_Position = vec4(normpos*2.0-1.0, 0.0, 1.0);\\n gl_Position.y *= -1.0;\\n}\\n\"},\n function _(n,t,e){Object.defineProperty(e,\"__esModule\",{value:!0}),e.fragment_shader=\"\\nprecision mediump float;\\n\\nconst float PI = 3.14159265358979323846264;\\nconst float THETA = 15.0 * 3.14159265358979323846264/180.0;\\n\\nuniform sampler2D u_dash_atlas;\\n\\nuniform vec2 u_linecaps;\\nuniform float u_miter_limit;\\nuniform float u_linejoin;\\nuniform float u_antialias;\\nuniform float u_dash_phase;\\nuniform float u_dash_period;\\nuniform float u_dash_index;\\nuniform vec2 u_dash_caps;\\nuniform float u_closed;\\n\\nvarying vec4 v_color;\\nvarying vec2 v_segment;\\nvarying vec2 v_angles;\\nvarying vec2 v_texcoord;\\nvarying vec2 v_miter;\\nvarying float v_length;\\nvarying float v_linewidth;\\n\\n// Compute distance to cap ----------------------------------------------------\\nfloat cap( int type, float dx, float dy, float t, float linewidth )\\n{\\n float d = 0.0;\\n dx = abs(dx);\\n dy = abs(dy);\\n if (type == 0) discard; // None\\n else if (type == 1) d = sqrt(dx*dx+dy*dy); // Round\\n else if (type == 3) d = (dx+abs(dy)); // Triangle in\\n else if (type == 2) d = max(abs(dy),(t+dx-abs(dy))); // Triangle out\\n else if (type == 4) d = max(dx,dy); // Square\\n else if (type == 5) d = max(dx+t,dy); // Butt\\n return d;\\n}\\n\\n// Compute distance to join -------------------------------------------------\\nfloat join( in int type, in float d, in vec2 segment, in vec2 texcoord, in vec2 miter,\\n in float linewidth )\\n{\\n // texcoord.x is distance from start\\n // texcoord.y is distance from centerline\\n // segment.x and y indicate the limits (as for texcoord.x) for this segment\\n\\n float dx = texcoord.x;\\n\\n // Round join\\n if( type == 1 ) {\\n if (dx < segment.x) {\\n d = max(d,length( texcoord - vec2(segment.x,0.0)));\\n //d = length( texcoord - vec2(segment.x,0.0));\\n } else if (dx > segment.y) {\\n d = max(d,length( texcoord - vec2(segment.y,0.0)));\\n //d = length( texcoord - vec2(segment.y,0.0));\\n }\\n }\\n // Bevel join\\n else if ( type == 2 ) {\\n if (dx < segment.x) {\\n vec2 x = texcoord - vec2(segment.x,0.0);\\n d = max(d, max(abs(x.x), abs(x.y)));\\n\\n } else if (dx > segment.y) {\\n vec2 x = texcoord - vec2(segment.y,0.0);\\n d = max(d, max(abs(x.x), abs(x.y)));\\n }\\n /* Original code for bevel which does not work for us\\n if( (dx < segment.x) || (dx > segment.y) )\\n d = max(d, min(abs(x.x),abs(x.y)));\\n */\\n }\\n\\n return d;\\n}\\n\\nvoid main()\\n{\\n // If color is fully transparent we just discard the fragment\\n if( v_color.a <= 0.0 ) {\\n discard;\\n }\\n\\n // Test if dash pattern is the solid one (0)\\n bool solid = (u_dash_index == 0.0);\\n\\n // Test if path is closed\\n bool closed = (u_closed > 0.0);\\n\\n vec4 color = v_color;\\n float dx = v_texcoord.x;\\n float dy = v_texcoord.y;\\n float t = v_linewidth/2.0-u_antialias;\\n float width = 1.0; //v_linewidth; original code had dashes scale with line width, we do not\\n float d = 0.0;\\n\\n vec2 linecaps = u_linecaps;\\n vec2 dash_caps = u_dash_caps;\\n float line_start = 0.0;\\n float line_stop = v_length;\\n\\n // Apply miter limit; fragments too far into the miter are simply discarded\\n if( (dx < v_segment.x) || (dx > v_segment.y) ) {\\n float into_miter = max(v_segment.x - dx, dx - v_segment.y);\\n if (into_miter > u_miter_limit*v_linewidth/2.0)\\n discard;\\n }\\n\\n // Solid line --------------------------------------------------------------\\n if( solid ) {\\n d = abs(dy);\\n if( (!closed) && (dx < line_start) ) {\\n d = cap( int(u_linecaps.x), abs(dx), abs(dy), t, v_linewidth );\\n }\\n else if( (!closed) && (dx > line_stop) ) {\\n d = cap( int(u_linecaps.y), abs(dx)-line_stop, abs(dy), t, v_linewidth );\\n }\\n else {\\n d = join( int(u_linejoin), abs(dy), v_segment, v_texcoord, v_miter, v_linewidth );\\n }\\n\\n // Dash line --------------------------------------------------------------\\n } else {\\n float segment_start = v_segment.x;\\n float segment_stop = v_segment.y;\\n float segment_center= (segment_start+segment_stop)/2.0;\\n float freq = u_dash_period*width;\\n float u = mod( dx + u_dash_phase*width, freq);\\n vec4 tex = texture2D(u_dash_atlas, vec2(u/freq, u_dash_index)) * 255.0 -10.0; // conversion to int-like\\n float dash_center= tex.x * width;\\n float dash_type = tex.y;\\n float _start = tex.z * width;\\n float _stop = tex.a * width;\\n float dash_start = dx - u + _start;\\n float dash_stop = dx - u + _stop;\\n\\n // Compute extents of the first dash (the one relative to v_segment.x)\\n // Note: this could be computed in the vertex shader\\n if( (dash_stop < segment_start) && (dash_caps.x != 5.0) ) {\\n float u = mod(segment_start + u_dash_phase*width, freq);\\n vec4 tex = texture2D(u_dash_atlas, vec2(u/freq, u_dash_index)) * 255.0 -10.0; // conversion to int-like\\n dash_center= tex.x * width;\\n //dash_type = tex.y;\\n float _start = tex.z * width;\\n float _stop = tex.a * width;\\n dash_start = segment_start - u + _start;\\n dash_stop = segment_start - u + _stop;\\n }\\n\\n // Compute extents of the last dash (the one relatives to v_segment.y)\\n // Note: This could be computed in the vertex shader\\n else if( (dash_start > segment_stop) && (dash_caps.y != 5.0) ) {\\n float u = mod(segment_stop + u_dash_phase*width, freq);\\n vec4 tex = texture2D(u_dash_atlas, vec2(u/freq, u_dash_index)) * 255.0 -10.0; // conversion to int-like\\n dash_center= tex.x * width;\\n //dash_type = tex.y;\\n float _start = tex.z * width;\\n float _stop = tex.a * width;\\n dash_start = segment_stop - u + _start;\\n dash_stop = segment_stop - u + _stop;\\n }\\n\\n // This test if the we are dealing with a discontinuous angle\\n bool discontinuous = ((dx < segment_center) && abs(v_angles.x) > THETA) ||\\n ((dx >= segment_center) && abs(v_angles.y) > THETA);\\n //if( dx < line_start) discontinuous = false;\\n //if( dx > line_stop) discontinuous = false;\\n\\n float d_join = join( int(u_linejoin), abs(dy),\\n v_segment, v_texcoord, v_miter, v_linewidth );\\n\\n // When path is closed, we do not have room for linecaps, so we make room\\n // by shortening the total length\\n if (closed) {\\n line_start += v_linewidth/2.0;\\n line_stop -= v_linewidth/2.0;\\n }\\n\\n // We also need to take antialias area into account\\n //line_start += u_antialias;\\n //line_stop -= u_antialias;\\n\\n // Check is dash stop is before line start\\n if( dash_stop <= line_start ) {\\n discard;\\n }\\n // Check is dash start is beyond line stop\\n if( dash_start >= line_stop ) {\\n discard;\\n }\\n\\n // Check if current dash start is beyond segment stop\\n if( discontinuous ) {\\n // Dash start is beyond segment, we discard\\n if( (dash_start > segment_stop) ) {\\n discard;\\n //gl_FragColor = vec4(1.0,0.0,0.0,.25); return;\\n }\\n\\n // Dash stop is before segment, we discard\\n if( (dash_stop < segment_start) ) {\\n discard; //gl_FragColor = vec4(0.0,1.0,0.0,.25); return;\\n }\\n\\n // Special case for round caps (nicer with this)\\n if( dash_caps.x == 1.0 ) {\\n if( (u > _stop) && (dash_stop > segment_stop ) && (abs(v_angles.y) < PI/2.0)) {\\n discard;\\n }\\n }\\n\\n // Special case for round caps (nicer with this)\\n if( dash_caps.y == 1.0 ) {\\n if( (u < _start) && (dash_start < segment_start ) && (abs(v_angles.x) < PI/2.0)) {\\n discard;\\n }\\n }\\n\\n // Special case for triangle caps (in & out) and square\\n // We make sure the cap stop at crossing frontier\\n if( (dash_caps.x != 1.0) && (dash_caps.x != 5.0) ) {\\n if( (dash_start < segment_start ) && (abs(v_angles.x) < PI/2.0) ) {\\n float a = v_angles.x/2.0;\\n float x = (segment_start-dx)*cos(a) - dy*sin(a);\\n float y = (segment_start-dx)*sin(a) + dy*cos(a);\\n if( x > 0.0 ) discard;\\n // We transform the cap into square to avoid holes\\n dash_caps.x = 4.0;\\n }\\n }\\n\\n // Special case for triangle caps (in & out) and square\\n // We make sure the cap stop at crossing frontier\\n if( (dash_caps.y != 1.0) && (dash_caps.y != 5.0) ) {\\n if( (dash_stop > segment_stop ) && (abs(v_angles.y) < PI/2.0) ) {\\n float a = v_angles.y/2.0;\\n float x = (dx-segment_stop)*cos(a) - dy*sin(a);\\n float y = (dx-segment_stop)*sin(a) + dy*cos(a);\\n if( x > 0.0 ) discard;\\n // We transform the caps into square to avoid holes\\n dash_caps.y = 4.0;\\n }\\n }\\n }\\n\\n // Line cap at start\\n if( (dx < line_start) && (dash_start < line_start) && (dash_stop > line_start) ) {\\n d = cap( int(linecaps.x), dx-line_start, dy, t, v_linewidth);\\n }\\n // Line cap at stop\\n else if( (dx > line_stop) && (dash_stop > line_stop) && (dash_start < line_stop) ) {\\n d = cap( int(linecaps.y), dx-line_stop, dy, t, v_linewidth);\\n }\\n // Dash cap left - dash_type = -1, 0 or 1, but there may be roundoff errors\\n else if( dash_type < -0.5 ) {\\n d = cap( int(dash_caps.y), abs(u-dash_center), dy, t, v_linewidth);\\n if( (dx > line_start) && (dx < line_stop) )\\n d = max(d,d_join);\\n }\\n // Dash cap right\\n else if( dash_type > 0.5 ) {\\n d = cap( int(dash_caps.x), abs(dash_center-u), dy, t, v_linewidth);\\n if( (dx > line_start) && (dx < line_stop) )\\n d = max(d,d_join);\\n }\\n // Dash body (plain)\\n else {// if( dash_type > -0.5 && dash_type < 0.5) {\\n d = abs(dy);\\n }\\n\\n // Line join\\n if( (dx > line_start) && (dx < line_stop)) {\\n if( (dx <= segment_start) && (dash_start <= segment_start)\\n && (dash_stop >= segment_start) ) {\\n d = d_join;\\n // Antialias at outer border\\n float angle = PI/2.+v_angles.x;\\n float f = abs( (segment_start - dx)*cos(angle) - dy*sin(angle));\\n d = max(f,d);\\n }\\n else if( (dx > segment_stop) && (dash_start <= segment_stop)\\n && (dash_stop >= segment_stop) ) {\\n d = d_join;\\n // Antialias at outer border\\n float angle = PI/2.+v_angles.y;\\n float f = abs((dx - segment_stop)*cos(angle) - dy*sin(angle));\\n d = max(f,d);\\n }\\n else if( dx < (segment_start - v_linewidth/2.)) {\\n discard;\\n }\\n else if( dx > (segment_stop + v_linewidth/2.)) {\\n discard;\\n }\\n }\\n else if( dx < (segment_start - v_linewidth/2.)) {\\n discard;\\n }\\n else if( dx > (segment_stop + v_linewidth/2.)) {\\n discard;\\n }\\n }\\n\\n // Distance to border ------------------------------------------------------\\n d = d - t;\\n if( d < 0.0 ) {\\n gl_FragColor = color;\\n } else {\\n d /= u_antialias;\\n gl_FragColor = vec4(color.rgb, exp(-d*d)*color.a);\\n }\\n}\\n\"},\n function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=e(1),l=e(93),_=e(100),n=s.__importStar(e(101)),o=s.__importStar(e(28)),a=e(88);class h extends l.XYGlyphView{_inner_loop(e,i,t,s,l){for(const _ of i)0!=_?isNaN(t[_]+s[_])?(e.closePath(),l.apply(e),e.beginPath()):e.lineTo(t[_],s[_]):(e.beginPath(),e.moveTo(t[_],s[_]));e.closePath(),l.call(e)}_render(e,i,{sx:t,sy:s}){this.visuals.fill.doit&&(this.visuals.fill.set_value(e),this._inner_loop(e,i,t,s,e.fill)),this.visuals.hatch.doit2(e,0,()=>this._inner_loop(e,i,t,s,e.fill),()=>this.renderer.request_render()),this.visuals.line.doit&&(this.visuals.line.set_value(e),this._inner_loop(e,i,t,s,e.stroke))}draw_legend_for_index(e,i,t){_.generic_area_legend(this.visuals,e,i,t)}_hit_point(e){const i=new a.Selection;return n.point_in_poly(e.sx,e.sy,this.sx,this.sy)&&(i.add_to_selected_glyphs(this.model),i.view=this),i}}t.PatchView=h,h.__name__=\"PatchView\";class r extends l.XYGlyph{constructor(e){super(e)}static init_Patch(){this.prototype.default_view=h,this.mixins([o.Line,o.Fill,o.Hatch])}}t.Patch=r,r.__name__=\"Patch\",r.init_Patch()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),r=e(24),n=e(112),a=i.__importStar(e(101)),_=i.__importStar(e(18)),h=e(88);class l extends n.AreaView{_index_data(e){const{min:t,max:s}=Math,{data_size:i}=this;for(let r=0;r=0;t--)e.lineTo(s[t],i[t]);e.closePath(),r.call(e)}_render(e,t,{sx1:s,sx2:i,sy:r}){this.visuals.fill.doit&&(this.visuals.fill.set_value(e),this._inner(e,s,i,r,e.fill)),this.visuals.hatch.doit2(e,0,()=>this._inner(e,s,i,r,e.fill),()=>this.renderer.request_render())}_hit_point(e){const t=this.sy.length,s=new r.NumberArray(2*t),i=new r.NumberArray(2*t);for(let e=0,r=t;e=0;s--)e.lineTo(t[s],i[s]);e.closePath(),r.call(e)}_render(e,t,{sx:s,sy1:i,sy2:r}){this.visuals.fill.doit&&(this.visuals.fill.set_value(e),this._inner(e,s,i,r,e.fill)),this.visuals.hatch.doit2(e,0,()=>this._inner(e,s,i,r,e.fill),()=>this.renderer.request_render())}scenterxy(e){return[this.sx[e],(this.sy1[e]+this.sy2[e])/2]}_hit_point(e){const t=this.sx.length,s=new r.NumberArray(2*t),i=new r.NumberArray(2*t);for(let e=0,r=t;ethis.compute_indices());const i=()=>{const i=()=>this.compute_indices();null!=this.source&&(this.connect(this.source.change,i),this.source instanceof _.ColumnarDataSource&&(this.connect(this.source.streaming,i),this.connect(this.source.patching,i)))};let e=null!=this.source;e?i():this.connect(this.properties.source.change,()=>{e||(i(),e=!0)})}compute_indices(){var i;const{source:e}=this;if(null==e)return;const s=null!==(i=e.get_length())&&void 0!==i?i:1,t=r.Indices.all_set(s);for(const i of this.filters)t.intersect(i.compute_indices(e));this.indices=t,this._indices=[...t],this.indices_map_to_subset()}indices_map_to_subset(){this.indices_map={};for(let i=0;ithis._indices[i]);return new o.Selection(Object.assign(Object.assign({},i.attributes),{indices:e}))}convert_selection_to_subset(i){const e=i.indices.map(i=>this.indices_map[i]);return new o.Selection(Object.assign(Object.assign({},i.attributes),{indices:e}))}convert_indices_from_subset(i){return i.map(i=>this._indices[i])}}s.CDSView=a,a.__name__=\"CDSView\",a.init_CDSView()},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=e(9);async function i(e,n,t){const o=new e(Object.assign(Object.assign({},t),{model:n}));return o.initialize(),await o.lazy_initialize(),o}t.build_view=async function(e,n={parent:null},t=(e=>e.default_view)){const o=await i(t(e),e,n);return o.connect_signals(),o},t.build_views=async function(e,n,t={parent:null},s=(e=>e.default_view)){const c=o.difference([...e.keys()],n);for(const n of c)e.get(n).remove(),e.delete(n);const a=[],f=n.filter(n=>!e.has(n));for(const n of f){const o=await i(s(n),n,t);e.set(n,o),a.push(o)}for(const e of a)e.connect_signals();return a},t.remove_views=function(e){for(const[n,t]of e)t.remove(),e.delete(n)}},\n function _(e,r,n){Object.defineProperty(n,\"__esModule\",{value:!0});const t=e(1),i=e(91),s=e(117),a=t.__importStar(e(18)),o=e(115),_=e(11);class l extends i.DataRendererView{async lazy_initialize(){await super.lazy_initialize();const e=this.model;let r=null,n=null;const t={v_compute(n){_.assert(null==r);const[t]=r=e.layout_provider.get_edge_coordinates(n);return t}},i={v_compute(e){_.assert(null!=r);const[,n]=r;return r=null,n}},s={v_compute(r){_.assert(null==n);const[t]=n=e.layout_provider.get_node_coordinates(r);return t}},a={v_compute(e){_.assert(null!=n);const[,r]=n;return n=null,r}},{edge_renderer:l,node_renderer:d}=this.model;l.glyph.properties.xs.internal=!0,l.glyph.properties.ys.internal=!0,d.glyph.properties.x.internal=!0,d.glyph.properties.y.internal=!0,l.glyph.xs={expr:t},l.glyph.ys={expr:i},d.glyph.x={expr:s},d.glyph.y={expr:a};const{parent:p}=this;this.edge_view=await o.build_view(l,{parent:p}),this.node_view=await o.build_view(d,{parent:p})}connect_signals(){super.connect_signals(),this.connect(this.model.layout_provider.change,()=>{this.edge_view.set_data(!1),this.node_view.set_data(!1),this.request_render()})}remove(){this.edge_view.remove(),this.node_view.remove(),super.remove()}_render(){this.edge_view.render(),this.node_view.render()}}n.GraphRendererView=l,l.__name__=\"GraphRendererView\";class d extends i.DataRenderer{constructor(e){super(e)}static init_GraphRenderer(){this.prototype.default_view=l,this.define({layout_provider:[a.Instance],node_renderer:[a.Instance],edge_renderer:[a.Instance],selection_policy:[a.Instance,()=>new s.NodesOnly],inspection_policy:[a.Instance,()=>new s.NodesOnly]})}get_selection_manager(){return this.node_renderer.data_source.selection_manager}}n.GraphRenderer=d,d.__name__=\"GraphRenderer\",d.init_GraphRenderer()},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const d=e(81),s=e(12),o=e(9),_=e(88);class i extends d.Model{constructor(e){super(e)}_hit_test_nodes(e,t){if(!t.model.visible)return null;const n=t.node_view.glyph.hit_test(e);return null==n?null:t.node_view.model.view.convert_selection_from_subset(n)}_hit_test_edges(e,t){if(!t.model.visible)return null;const n=t.edge_view.glyph.hit_test(e);return null==n?null:t.edge_view.model.view.convert_selection_from_subset(n)}}n.GraphHitTestPolicy=i,i.__name__=\"GraphHitTestPolicy\";class r extends i{constructor(e){super(e)}hit_test(e,t){return this._hit_test_nodes(e,t)}do_selection(e,t,n,d){if(null==e)return!1;const s=t.node_renderer.data_source.selected;return s.update(e,n,d),t.node_renderer.data_source._select.emit(),!s.is_empty()}do_inspection(e,t,n,d,s){if(null==e)return!1;const o=n.model.get_selection_manager().get_or_create_inspector(n.node_view.model);return o.update(e,d,s),n.node_view.model.data_source.setv({inspected:o},{silent:!0}),n.node_view.model.data_source.inspect.emit([n.node_view,{geometry:t}]),!o.is_empty()}}n.NodesOnly=r,r.__name__=\"NodesOnly\";class c extends i{constructor(e){super(e)}hit_test(e,t){return this._hit_test_nodes(e,t)}get_linked_edges(e,t,n){let d=[];\"selection\"==n?d=e.selected.indices.map(t=>e.data.index[t]):\"inspection\"==n&&(d=e.inspected.indices.map(t=>e.data.index[t]));const s=[];for(let e=0;es.indexOf(e.data.index,t));return new _.Selection({indices:r})}do_selection(e,t,n,d){if(null==e)return!1;const s=t.edge_renderer.data_source.selected;s.update(e,n,d);const o=t.node_renderer.data_source.selected,_=this.get_linked_nodes(t.node_renderer.data_source,t.edge_renderer.data_source,\"selection\");return o.update(_,n,d),t.edge_renderer.data_source._select.emit(),!s.is_empty()}do_inspection(e,t,n,d,s){if(null==e)return!1;const o=n.edge_view.model.data_source.selection_manager.get_or_create_inspector(n.edge_view.model);o.update(e,d,s),n.edge_view.model.data_source.setv({inspected:o},{silent:!0});const _=n.node_view.model.data_source.selection_manager.get_or_create_inspector(n.node_view.model),i=this.get_linked_nodes(n.node_view.model.data_source,n.edge_view.model.data_source,\"inspection\");return _.update(i,d,s),n.node_view.model.data_source.setv({inspected:_},{silent:!0}),n.edge_view.model.data_source.inspect.emit([n.edge_view,{geometry:t}]),!o.is_empty()}}n.EdgesAndLinkedNodes=a,a.__name__=\"EdgesAndLinkedNodes\"},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const s=e(81);class o extends s.Model{do_selection(e,t,n,s){return null!==e&&(t.selected.update(e,n,s),t._select.emit(),!t.selected.is_empty())}}n.SelectionPolicy=o,o.__name__=\"SelectionPolicy\";class r extends o{hit_test(e,t){const n=[];for(const s of t){const t=s.hit_test(e);null!==t&&n.push(t)}if(n.length>0){const e=n[0];for(const t of n)e.update_through_intersection(t);return e}return null}}n.IntersectRenderers=r,r.__name__=\"IntersectRenderers\";class c extends o{hit_test(e,t){const n=[];for(const s of t){const t=s.hit_test(e);null!==t&&n.push(t)}if(n.length>0){const e=n[0];for(const t of n)e.update_through_union(t);return e}return null}}n.UnionRenderers=c,c.__name__=\"UnionRenderers\"},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0}),n.concat=function(t,...e){let n=t.length;for(const t of e)n+=t.length;const o=new t.constructor(n);o.set(t,0);let c=t.length;for(const t of e)o.set(t,c),c+=t.length;return o}},\n function _(n,o,e){function t(...n){const o=new Set;for(const e of n)for(const n of e)o.add(n);return o}Object.defineProperty(e,\"__esModule\",{value:!0}),e.union=t,e.intersection=function(n,...o){const e=new Set;n:for(const t of n){for(const n of o)if(!n.has(t))continue n;e.add(t)}return e},e.difference=function(n,...o){const e=new Set(n);for(const n of t(...o))e.delete(n);return e}},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=e(14);class o{constructor(e){this.document=e}}s.DocumentEvent=o,o.__name__=\"DocumentEvent\";class r extends o{constructor(e,t,s){super(e),this.events=t,this.setter_id=s}}s.DocumentEventBatch=r,r.__name__=\"DocumentEventBatch\";class d extends o{}s.DocumentChangedEvent=d,d.__name__=\"DocumentChangedEvent\";class _ extends d{constructor(e,t,s){super(e),this.msg_type=t,this.msg_data=s}json(e){const t=this.msg_data,s=n.HasProps._value_to_json(t),o=new Set;return n.HasProps._value_record_references(t,o,{recursive:!0}),{kind:\"MessageSent\",msg_type:this.msg_type,msg_data:s}}}s.MessageSentEvent=_,_.__name__=\"MessageSentEvent\";class i extends d{constructor(e,t,s,n,o,r,d){super(e),this.model=t,this.attr=s,this.old=n,this.new_=o,this.setter_id=r,this.hint=d}json(e){if(\"id\"===this.attr)throw new Error(\"'id' field should never change, whatever code just set it is wrong\");if(null!=this.hint)return this.hint.json(e);const t=this.new_,s=n.HasProps._value_to_json(t),o=new Set;n.HasProps._value_record_references(t,o,{recursive:!0}),o.has(this.model)&&this.model!==t&&o.delete(this.model);for(const t of o)e.add(t);return{kind:\"ModelChanged\",model:this.model.ref(),attr:this.attr,new:s}}}s.ModelChangedEvent=i,i.__name__=\"ModelChangedEvent\";class a extends d{constructor(e,t,s){super(e),this.column_source=t,this.patches=s}json(e){return{kind:\"ColumnsPatched\",column_source:this.column_source,patches:this.patches}}}s.ColumnsPatchedEvent=a,a.__name__=\"ColumnsPatchedEvent\";class c extends d{constructor(e,t,s,n){super(e),this.column_source=t,this.data=s,this.rollover=n}json(e){return{kind:\"ColumnsStreamed\",column_source:this.column_source,data:this.data,rollover:this.rollover}}}s.ColumnsStreamedEvent=c,c.__name__=\"ColumnsStreamedEvent\";class h extends d{constructor(e,t,s){super(e),this.title=t,this.setter_id=s}json(e){return{kind:\"TitleChanged\",title:this.title}}}s.TitleChangedEvent=h,h.__name__=\"TitleChangedEvent\";class u extends d{constructor(e,t,s){super(e),this.model=t,this.setter_id=s}json(e){return n.HasProps._value_record_references(this.model,e,{recursive:!0}),{kind:\"RootAdded\",model:this.model.ref()}}}s.RootAddedEvent=u,u.__name__=\"RootAddedEvent\";class l extends d{constructor(e,t,s){super(e),this.model=t,this.setter_id=s}json(e){return{kind:\"RootRemoved\",model:this.model.ref()}}}s.RootRemovedEvent=l,l.__name__=\"RootRemovedEvent\"},\n function _(e,s,t){Object.defineProperty(t,\"__esModule\",{value:!0});const i=e(1),l=e(123),_=i.__importStar(e(28));class o extends l.UpperLowerView{connect_signals(){super.connect_signals();const e=()=>this.set_data(this.model.source);this.connect(this.model.change,e),this.connect(this.model.source.streaming,e),this.connect(this.model.source.patching,e),this.connect(this.model.source.change,e)}_render(){this._map_data();const{ctx:e}=this.layer;e.beginPath(),e.moveTo(this._lower_sx[0],this._lower_sy[0]);for(let s=0,t=this._lower_sx.length;s=0;s--)e.lineTo(this._upper_sx[s],this._upper_sy[s]);e.closePath(),this.visuals.fill.doit&&(this.visuals.fill.set_value(e),e.fill()),e.beginPath(),e.moveTo(this._lower_sx[0],this._lower_sy[0]);for(let s=0,t=this._lower_sx.length;snew r.ColumnDataSource]})}}i.UpperLower=a,a.__name__=\"UpperLower\",a.init_UpperLower()},\n function _(t,i,s){Object.defineProperty(s,\"__esModule\",{value:!0});const e=t(1),o=t(36),n=t(15),l=e.__importStar(t(28)),a=e.__importStar(t(18)),h=t(79);s.EDGE_TOLERANCE=2.5;class r extends o.AnnotationView{connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.plot_view.request_paint(this)),this.connect(this.model.data_update,()=>this.plot_view.request_paint(this))}_render(){if(null==this.model.left&&null==this.model.right&&null==this.model.top&&null==this.model.bottom)return;const{frame:t}=this.plot_view,i=this.coordinates.x_scale,s=this.coordinates.y_scale,e=(t,i,s,e,o)=>{let n;return n=null!=t?this.model.screen?t:\"data\"==i?s.compute(t):e.compute(t):o,n};this.sleft=e(this.model.left,this.model.left_units,i,t.xview,t.bbox.left),this.sright=e(this.model.right,this.model.right_units,i,t.xview,t.bbox.right),this.stop=e(this.model.top,this.model.top_units,s,t.yview,t.bbox.top),this.sbottom=e(this.model.bottom,this.model.bottom_units,s,t.yview,t.bbox.bottom),this._paint_box(this.sleft,this.sright,this.sbottom,this.stop)}_paint_box(t,i,s,e){const{ctx:o}=this.layer;o.save(),o.beginPath(),o.rect(t,e,i-t,s-e),this.visuals.fill.doit&&(this.visuals.fill.set_value(o),o.fill()),this.visuals.line.doit&&(this.visuals.line.set_value(o),o.stroke()),o.restore()}interactive_bbox(){const t=this.model.properties.line_width.value()+s.EDGE_TOLERANCE;return new h.BBox({x0:this.sleft-t,y0:this.stop-t,x1:this.sright+t,y1:this.sbottom+t})}interactive_hit(t,i){if(null==this.model.in_cursor)return!1;return this.interactive_bbox().contains(t,i)}cursor(t,i){return Math.abs(t-this.sleft)<3||Math.abs(t-this.sright)<3?this.model.ew_cursor:Math.abs(i-this.sbottom)<3||Math.abs(i-this.stop)<3?this.model.ns_cursor:t>this.sleft&&tthis.stop&&ithis.plot_view.request_render()),this.connect(this.model.formatter.change,()=>this.plot_view.request_render()),null!=this.model.color_mapper&&this.connect(this.model.color_mapper.change,()=>{this._set_canvas_image(),this.plot_view.request_render()})}_get_size(){if(null==this.model.color_mapper)return{width:0,height:0};{const{width:t,height:e}=this.compute_legend_dimensions();return{width:t,height:e}}}_set_canvas_image(){if(null==this.model.color_mapper)return;let t,e,{palette:i}=this.model.color_mapper;switch(\"vertical\"==this.model.orientation&&(i=g.reversed(i)),this.model.orientation){case\"vertical\":[t,e]=[1,i.length];break;case\"horizontal\":[t,e]=[i.length,1]}const o=document.createElement(\"canvas\");o.width=t,o.height=e;const a=o.getContext(\"2d\"),s=a.getImageData(0,0,t,e),r=new n.LinearColorMapper({palette:i}).rgba_mapper.v_compute(g.range(0,i.length));s.data.set(r),a.putImageData(s,0,0),this.image=o}compute_legend_dimensions(){const t=this._computed_image_dimensions(),[e,i]=[t.height,t.width],o=this._get_label_extent(),a=this._title_extent(),s=this._tick_extent(),{padding:r}=this.model;let n,l;switch(this.model.orientation){case\"vertical\":n=e+a+2*r,l=i+s+o+2*r;break;case\"horizontal\":n=e+a+s+o+2*r,l=i+2*r}return{width:l,height:n}}compute_legend_location(){const t=this.compute_legend_dimensions(),[e,i]=[t.height,t.width],o=this.model.margin,a=null!=this.panel?this.panel:this.plot_view.frame,[s,r]=a.bbox.ranges,{location:n}=this.model;let l,_;if(f.isString(n))switch(n){case\"top_left\":l=s.start+o,_=r.start+o;break;case\"top_center\":l=(s.end+s.start)/2-i/2,_=r.start+o;break;case\"top_right\":l=s.end-o-i,_=r.start+o;break;case\"bottom_right\":l=s.end-o-i,_=r.end-o-e;break;case\"bottom_center\":l=(s.end+s.start)/2-i/2,_=r.end-o-e;break;case\"bottom_left\":l=s.start+o,_=r.end-o-e;break;case\"center_left\":l=s.start+o,_=(r.end+r.start)/2-e/2;break;case\"center\":l=(s.end+s.start)/2-i/2,_=(r.end+r.start)/2-e/2;break;case\"center_right\":l=s.end-o-i,_=(r.end+r.start)/2-e/2}else if(f.isArray(n)&&2==n.length){const[t,i]=n;l=a.xview.compute(t),_=a.yview.compute(i)-e}else b.unreachable();return{sx:l,sy:_}}_render(){if(null==this.model.color_mapper)return;const{ctx:t}=this.layer;t.save();const{sx:e,sy:i}=this.compute_legend_location();t.translate(e,i),this._draw_bbox(t);const o=this._get_image_offset();t.translate(o.x,o.y),this._draw_image(t);const a=this.tick_info();this._draw_major_ticks(t,a),this._draw_minor_ticks(t,a),this._draw_major_labels(t,a),this.model.title&&this._draw_title(t),t.restore()}_draw_bbox(t){const e=this.compute_legend_dimensions();t.save(),this.visuals.background_fill.doit&&(this.visuals.background_fill.set_value(t),t.fillRect(0,0,e.width,e.height)),this.visuals.border_line.doit&&(this.visuals.border_line.set_value(t),t.strokeRect(0,0,e.width,e.height)),t.restore()}_draw_image(t){const e=this._computed_image_dimensions();t.save(),t.setImageSmoothingEnabled(!1),t.globalAlpha=this.model.scale_alpha,t.drawImage(this.image,0,0,e.width,e.height),this.visuals.bar_line.doit&&(this.visuals.bar_line.set_value(t),t.strokeRect(0,0,e.width,e.height)),t.restore()}_draw_major_ticks(t,e){if(!this.visuals.major_tick_line.doit)return;const[i,o]=this._normals(),a=this._computed_image_dimensions(),[s,r]=[a.width*i,a.height*o],[n,l]=e.coords.major,_=this.model.major_tick_in,h=this.model.major_tick_out;t.save(),t.translate(s,r),this.visuals.major_tick_line.set_value(t);for(let e=0,a=n.length;ei.measureText(t.toString()).width));break;case\"horizontal\":e=u.measure_font(this.visuals.major_label_text.font_value()).height}e+=this.model.label_standoff,i.restore()}return e}_get_image_offset(){return{x:this.model.padding,y:this.model.padding+this._title_extent()}}_normals(){return\"vertical\"==this.model.orientation?[1,0]:[0,1]}_title_extent(){const t=this.model.title_text_font+\" \"+this.model.title_text_font_size+\" \"+this.model.title_text_font_style;return this.model.title?u.measure_font(t).height+this.model.title_standoff:0}_tick_extent(){return g.max([this.model.major_tick_out,this.model.minor_tick_out])}_computed_image_dimensions(){const t=this.plot_view.frame.bbox.height,e=this.plot_view.frame.bbox.width,i=this._title_extent();let o,a;switch(this.model.orientation){case\"vertical\":\"auto\"==this.model.height?null!=this.panel?o=t-2*this.model.padding-i:(o=g.max([25*this.model.color_mapper.palette.length,.3*t]),o=g.min([o,.8*t-2*this.model.padding-i])):o=this.model.height,a=\"auto\"==this.model.width?25:this.model.width;break;case\"horizontal\":o=\"auto\"==this.model.height?25:this.model.height,\"auto\"==this.model.width?null!=this.panel?a=e-2*this.model.padding:(a=g.max([25*this.model.color_mapper.palette.length,.3*e]),a=g.min([a,.8*e-2*this.model.padding])):a=this.model.width}return{width:a,height:o}}_tick_coordinate_scale(t){const e={source_range:new m.Range1d({start:this.model.color_mapper.metrics.min,end:this.model.color_mapper.metrics.max}),target_range:new m.Range1d({start:0,end:t})},{color_mapper:i}=this.model;if(i instanceof n.LinearColorMapper)return new l.LinearScale(e);if(i instanceof n.LogColorMapper)return new h.LogScale(e);if(i instanceof n.ScanningColorMapper){const{binning:t}=i.metrics;return new _.LinearInterpolationScale(Object.assign(Object.assign({},e),{binning:t}))}b.unreachable()}_format_major_labels(t,e){const i=this.model.formatter.doFormat(t,null);for(let t=0,o=e.length;tr||(h[o].push(l[t]),h[a].push(0));for(let t=0,e=_.length;tr||(m[o].push(_[t]),m[a].push(0));const d={major:this._format_major_labels(h[o],l)},c={major:[[],[]],minor:[[],[]]};return c.major[o]=i.v_compute(h[o]),c.minor[o]=i.v_compute(m[o]),c.major[a]=h[a],c.minor[a]=m[a],\"vertical\"==this.model.orientation&&(c.major[o]=p.map(c.major[o],t=>e-t),c.minor[o]=p.map(c.minor[o],t=>e-t)),{coords:c,labels:d}}}i.ColorBarView=v,v.__name__=\"ColorBarView\";class w extends a.Annotation{constructor(t){super(t)}static init_ColorBar(){this.prototype.default_view=v,this.mixins([[\"major_label_\",d.Text],[\"title_\",d.Text],[\"major_tick_\",d.Line],[\"minor_tick_\",d.Line],[\"border_\",d.Line],[\"bar_\",d.Line],[\"background_\",d.Fill]]),this.define({location:[c.Any,\"top_right\"],orientation:[c.Orientation,\"vertical\"],title:[c.String],title_standoff:[c.Number,2],width:[c.Any,\"auto\"],height:[c.Any,\"auto\"],scale_alpha:[c.Number,1],ticker:[c.Instance,()=>new s.BasicTicker],formatter:[c.Instance,()=>new r.BasicTickFormatter],major_label_overrides:[c.Any,{}],color_mapper:[c.Instance],label_standoff:[c.Number,5],margin:[c.Number,30],padding:[c.Number,10],major_tick_in:[c.Number,5],major_tick_out:[c.Number,0],minor_tick_in:[c.Number,0],minor_tick_out:[c.Number,0]}),this.override({background_fill_color:\"#ffffff\",background_fill_alpha:.95,bar_line_color:null,border_line_color:null,major_label_text_align:\"center\",major_label_text_baseline:\"middle\",major_label_text_font_size:\"11px\",major_tick_line_color:\"#ffffff\",minor_tick_line_color:null,title_text_font_size:\"13px\",title_text_font_style:\"italic\"})}}i.ColorBar=w,w.__name__=\"ColorBar\",w.init_ColorBar()},\n function _(e,c,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(127);class r extends i.AdaptiveTicker{constructor(e){super(e)}}s.BasicTicker=r,r.__name__=\"BasicTicker\"},\n function _(t,i,e){Object.defineProperty(e,\"__esModule\",{value:!0});const a=t(1),s=t(128),n=t(9),r=a.__importStar(t(18));class _ extends s.ContinuousTicker{constructor(t){super(t)}static init_AdaptiveTicker(){this.define({base:[r.Number,10],mantissas:[r.Array,[1,2,5]],min_interval:[r.Number,0],max_interval:[r.Number]})}initialize(){super.initialize();const t=n.nth(this.mantissas,-1)/this.base,i=n.nth(this.mantissas,0)*this.base;this.extended_mantissas=[t,...this.mantissas,i],this.base_factor=0===this.get_min_interval()?1:this.get_min_interval()}get_interval(t,i,e){const a=i-t,s=this.get_ideal_interval(t,i,e),r=Math.floor(function(t,i=Math.E){return Math.log(t)/Math.log(i)}(s/this.base_factor,this.base)),_=this.base**r*this.base_factor,h=this.extended_mantissas,m=h.map(t=>Math.abs(e-a/(t*_))),o=h[n.argmin(m)];return c=o*_,l=this.get_min_interval(),u=this.get_max_interval(),Math.max(l,Math.min(u,c));var c,l,u}}e.AdaptiveTicker=_,_.__name__=\"AdaptiveTicker\",_.init_AdaptiveTicker()},\n function _(t,i,e){Object.defineProperty(e,\"__esModule\",{value:!0});const n=t(1),r=t(129),s=n.__importStar(t(18)),o=t(9);class _ extends r.Ticker{constructor(t){super(t)}static init_ContinuousTicker(){this.define({num_minor_ticks:[s.Number,5],desired_num_ticks:[s.Number,6]})}get_ticks(t,i,e,n,r){return this.get_ticks_no_defaults(t,i,n,this.desired_num_ticks)}get_ticks_no_defaults(t,i,e,n){const r=this.get_interval(t,i,n),s=Math.floor(t/r),_=Math.ceil(i/r);let c;c=isFinite(s)&&isFinite(_)?o.range(s,_+1):[];const u=c.map(t=>t*r).filter(e=>t<=e&&e<=i),a=this.num_minor_ticks,l=[];if(a>0&&u.length>0){const e=r/a,n=o.range(0,a).map(t=>t*e);for(const e of n.slice(1)){const n=u[0]-e;t<=n&&n<=i&&l.push(n)}for(const e of u)for(const r of n){const n=e+r;t<=n&&n<=i&&l.push(n)}}return{major:u,minor:l}}get_min_interval(){return this.min_interval}get_max_interval(){return null!=this.max_interval?this.max_interval:1/0}get_ideal_interval(t,i,e){return(i-t)/e}}e.ContinuousTicker=_,_.__name__=\"ContinuousTicker\",_.init_ContinuousTicker()},\n function _(e,c,n){Object.defineProperty(n,\"__esModule\",{value:!0});const o=e(81);class r extends o.Model{constructor(e){super(e)}}n.Ticker=r,r.__name__=\"Ticker\"},\n function _(i,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const r=i(1),s=i(131),n=r.__importStar(i(18));class o extends s.TickFormatter{constructor(i){super(i),this.last_precision=3}static init_BasicTickFormatter(){this.define({precision:[n.Any,\"auto\"],use_scientific:[n.Boolean,!0],power_limit_high:[n.Number,5],power_limit_low:[n.Number,-3]})}get scientific_limit_low(){return 10**this.power_limit_low}get scientific_limit_high(){return 10**this.power_limit_high}_need_sci(i){if(!this.use_scientific)return!1;const{scientific_limit_high:t}=this,{scientific_limit_low:e}=this,r=i.length<2?0:Math.abs(i[1]-i[0])/1e4;for(const s of i){const i=Math.abs(s);if(!(i<=r)&&(i>=t||i<=e))return!0}return!1}_format_with_precision(i,t,e){const r=new Array(i.length);if(t)for(let t=0,s=i.length;t=1;r?s++:s--){if(t){e[0]=i[0].toExponential(s);for(let t=1;tu(e,d))),s=g<0||g>=t.length?r:t[g],c[_]=s}}},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const n=t(1),o=t(136),_=n.__importStar(t(18)),i=t(8),l=t(22),c=t(32);function a(t){return i.isNumber(t)?t:(\"#\"!=t[0]&&(t=l.color2hex(t)),9!=t.length&&(t+=\"ff\"),parseInt(t.slice(1),16))}function s(t){const e=new Uint32Array(t.length);for(let r=0,n=t.length;rt)),e}get rgba_mapper(){const t=this,e=s(this.palette),r=this._colors(a);return{v_compute(n){const o=new Uint32Array(n.length);return t._v_compute(n,o,e,r),p(o)}}}_colors(t){return{nan_color:t(this.nan_color)}}}r.ColorMapper=u,u.__name__=\"ColorMapper\",u.init_ColorMapper()},\n function _(e,r,n){Object.defineProperty(n,\"__esModule\",{value:!0});const o=e(137);class s extends o.Transform{constructor(e){super(e)}compute(e){throw new Error(\"mapping single values is not supported\")}}n.Mapper=s,s.__name__=\"Mapper\"},\n function _(e,n,o){Object.defineProperty(o,\"__esModule\",{value:!0});const r=e(81);class s extends r.Model{constructor(e){super(e)}}o.Transform=s,s.__name__=\"Transform\"},\n function _(r,e,a){Object.defineProperty(a,\"__esModule\",{value:!0});const t=r(1),s=r(134),i=r(136),c=t.__importStar(r(18));class n extends i.Mapper{constructor(r){super(r)}static init_CategoricalMarkerMapper(){this.define({factors:[c.Array],markers:[c.Array],start:[c.Number,0],end:[c.Number],default_value:[c.MarkerType,\"circle\"]})}v_compute(r){const e=new Array(r.length);return s.cat_v_compute(r,this.factors,this.markers,e,this.start,this.end,this.default_value),e}}a.CategoricalMarkerMapper=n,n.__name__=\"CategoricalMarkerMapper\",n.init_CategoricalMarkerMapper()},\n function _(t,e,a){Object.defineProperty(a,\"__esModule\",{value:!0});const r=t(1),n=t(134),s=t(136),i=r.__importStar(t(18));class c extends s.Mapper{constructor(t){super(t)}static init_CategoricalPatternMapper(){this.define({factors:[i.Array],patterns:[i.Array],start:[i.Number,0],end:[i.Number],default_value:[i.HatchPatternType,\" \"]})}v_compute(t){const e=new Array(t.length);return n.cat_v_compute(t,this.factors,this.patterns,e,this.start,this.end,this.default_value),e}}a.CategoricalPatternMapper=c,c.__name__=\"CategoricalPatternMapper\",c.init_CategoricalPatternMapper()},\n function _(t,o,e){Object.defineProperty(e,\"__esModule\",{value:!0});const n=t(135),s=t(90),l=t(9),i=t(8);class c extends n.ColorMapper{constructor(t){super(t),this._scan_data=null}static init_ContinuousColorMapper(){this.define(({Number:t,String:o,Null:e,Ref:n,Color:l,Or:i,Tuple:c,Array:a})=>({high:[i(t,e),null],low:[i(t,e),null],high_color:[i(l,e),null],low_color:[i(l,e),null],domain:[a(c(n(s.GlyphRenderer),i(o,a(o)))),[]]}))}connect_signals(){super.connect_signals();const t=()=>{for(const[t]of this.domain)this.connect(t.view.change,()=>this.update_data()),this.connect(t.data_source.selected.change,()=>this.update_data())};this.connect(this.properties.domain.change,()=>t()),t()}update_data(){const{domain:t,palette:o}=this,e=[...this._collect(t)];this._scan_data=this.scan(e,o.length),this.change.emit()}get metrics(){return null==this._scan_data&&this.update_data(),this._scan_data}*_collect(t){for(const[o,e]of t)for(const t of i.isArray(e)?e:[e]){let e=o.data_source.get_column(t);e=o.view.indices.select(e);const n=o.view.masked,s=o.data_source.selected.indices;let c;if(null!=n&&s.length>0?c=l.intersection([...n],s):null!=n?c=[...n]:s.length>0&&(c=s),null!=c&&(e=l.map(c,t=>e[t])),e.length>0&&!i.isNumber(e[0]))for(const t of e)yield*t;else yield*e}}_v_compute(t,o,e,n){const{nan_color:s}=n;let{low_color:i,high_color:c}=n;null==i&&(i=e[0]),null==c&&(c=e[e.length-1]);const{domain:a}=this,r=l.is_empty(a)?t:[...this._collect(a)];this._scan_data=this.scan(r,e.length);for(let n=0,l=t.length;na?e:r[l]}}o.LinearColorMapper=a,a.__name__=\"LinearColorMapper\"},\n function _(o,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const e=o(140),r=o(12);class l extends e.ContinuousColorMapper{constructor(o){super(o)}scan(o,t){const n=null!=this.low?this.low:r.min(o),e=null!=this.high?this.high:r.max(o);return{max:e,min:n,scale:t/(Math.log(e)-Math.log(n))}}cmap(o,t,n,e,r){const l=t.length-1;if(o>r.max)return e;if(o==r.max)return t[l];if(ol&&(s=l),t[s]}}n.LogColorMapper=l,l.__name__=\"LogColorMapper\"},\n function _(n,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=n(140),o=n(12);class t extends i.ContinuousColorMapper{constructor(n){super(n)}cmap(n,e,r,i,t){if(nt.binning[t.binning.length-1])return i;return e[o.left_edge_index(n,t.binning)]}}r.ScanningColorMapper=t,t.__name__=\"ScanningColorMapper\"},\n function _(n,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const i=n(1),o=n(143),r=n(12),s=n(9),a=i.__importStar(n(18)),l=n(19);class p extends o.ScanningColorMapper{constructor(n){super(n)}static init_EqHistColorMapper(){this.define({bins:[a.Int,65536]})}scan(n,t){const e=null!=this.low?this.low:r.min(n),i=null!=this.high?this.high:r.max(n),o=this.bins,a=s.linspace(e,i,o+1),p=r.bin_counts(n,a),c=new Array(o);for(let n=0,t=a.length;nn/u);let m=t-1,_=[],M=0,f=2*t;for(;m!=t&&M<4&&0!=m;){const n=f/m;if(n>1e3)break;f=Math.round(Math.max(t*n,t));const e=s.range(0,f),i=r.map(g,n=>n*(f-1));_=r.interpolate(e,i,c);m=s.uniq(_).length-1,M++}if(0==m){_=[e,i];for(let n=0;nthis._sorted_dirty=!0)}v_compute(t){const e=new i.NumberArray(t.length);for(let r=0;rs*(e[t]-e[r])),this._x_sorted=new i.NumberArray(n),this._y_sorted=new i.NumberArray(n);for(let t=0;tthis._x_sorted[this._x_sorted.length-1])return NaN}else{if(tthis._x_sorted[this._x_sorted.length-1])return this._y_sorted[this._y_sorted.length-1]}if(t==this._x_sorted[0])return this._y_sorted[0];const s=_.find_last_index(this._x_sorted,s=>sthis._x_sorted[this._x_sorted.length-1])return NaN}else{if(tthis._x_sorted[this._x_sorted.length-1])return this._y_sorted[this._y_sorted.length-1]}let e;switch(this.mode){case\"after\":e=i.find_last_index(this._x_sorted,e=>t>=e);break;case\"before\":e=i.find_index(this._x_sorted,e=>t<=e);break;case\"center\":{const r=this._x_sorted.map(e=>Math.abs(e-t)),s=i.min(r);e=i.find_index(r,t=>s===t);break}default:throw new Error(\"unknown mode: \"+this.mode)}return-1!=e?this._y_sorted[e]:NaN}}r.StepInterpolator=n,n.__name__=\"StepInterpolator\",n.init_StepInterpolator()},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const r=e(1),a=e(147),i=e(24),s=e(9),o=e(12),c=r.__importStar(e(18));class _ extends a.Scale{constructor(e){super(e)}static init_LinearInterpolationScale(){this.internal({binning:[c.Array]})}compute(e){return e}v_compute(e){const t=o.norm(e,this.source_range.start,this.source_range.end),n=s.linspace(0,1,this.binning.length),r=o.interpolate(t,n,this.binning),a=o.norm(r,this.source_range.start,this.source_range.end),c=this.target_range.end-this.target_range.start,_=o.map(a,e=>this.target_range.start+e*c);return new i.NumberArray(_)}invert(e){return e}v_invert(e){return new i.NumberArray(e)}}n.LinearInterpolationScale=_,_.__name__=\"LinearInterpolationScale\",_.init_LinearInterpolationScale()},\n function _(t,e,o){Object.defineProperty(o,\"__esModule\",{value:!0});const a=t(146),r=t(24);class s extends a.ContinuousScale{constructor(t){super(t)}compute(t){const[e,o,a,r]=this._compute_state();let s;if(0==a)s=0;else{const n=(Math.log(t)-r)/a;s=isFinite(n)?n*e+o:NaN}return s}v_compute(t){const[e,o,a,s]=this._compute_state(),n=new r.NumberArray(t.length);if(0==a)for(let e=0;ethis.render()):this.connect(this.model.change,()=>this.plot_view.request_render())}render(){this.model.visible||\"css\"!=this.model.render_mode||a.undisplay(this.el),super.render()}_calculate_text_dimensions(e,t){const{width:s}=e.measureText(t),{height:i}=o.measure_font(this.visuals.text.font_value());return[s,i]}_calculate_bounding_box_dimensions(e,t){const[s,i]=this._calculate_text_dimensions(e,t);let l,a;switch(e.textAlign){case\"left\":l=0;break;case\"center\":l=-s/2;break;case\"right\":l=-s;break;default:r.unreachable()}switch(e.textBaseline){case\"top\":a=0;break;case\"middle\":a=-.5*i;break;case\"bottom\":a=-1*i;break;case\"alphabetic\":a=-.8*i;break;case\"hanging\":a=-.17*i;break;case\"ideographic\":a=-.83*i;break;default:r.unreachable()}return[l,a,s,i]}_canvas_text(e,t,s,i,l){this.visuals.text.set_value(e);const a=this._calculate_bounding_box_dimensions(e,t);e.save(),e.beginPath(),e.translate(s,i),l&&e.rotate(l),e.rect(a[0],a[1],a[2],a[3]),this.visuals.background_fill.doit&&(this.visuals.background_fill.set_value(e),e.fill()),this.visuals.border_line.doit&&(this.visuals.border_line.set_value(e),e.stroke()),this.visuals.text.doit&&(this.visuals.text.set_value(e),e.fillText(t,0,0)),e.restore()}_css_text(e,t,s,i,l){const{el:n}=this;r.assert(null!=n),a.undisplay(n),this.visuals.text.set_value(e);const o=this._calculate_bounding_box_dimensions(e,t),_=this.visuals.border_line.line_dash.value().length<2?\"solid\":\"dashed\";this.visuals.border_line.set_value(e),this.visuals.background_fill.set_value(e),n.style.position=\"absolute\",n.style.left=s+o[0]+\"px\",n.style.top=i+o[1]+\"px\",n.style.color=\"\"+this.visuals.text.text_color.value(),n.style.opacity=\"\"+this.visuals.text.text_alpha.value(),n.style.font=\"\"+this.visuals.text.font_value(),n.style.lineHeight=\"normal\",l&&(n.style.transform=`rotate(${l}rad)`),this.visuals.background_fill.doit&&(n.style.backgroundColor=\"\"+this.visuals.background_fill.color_value()),this.visuals.border_line.doit&&(n.style.borderStyle=\"\"+_,n.style.borderWidth=this.visuals.border_line.line_width.value()+\"px\",n.style.borderColor=\"\"+this.visuals.border_line.color_value()),n.textContent=t,a.display(n)}}s.TextAnnotationView=_,_.__name__=\"TextAnnotationView\";class u extends l.Annotation{constructor(e){super(e)}static init_TextAnnotation(){this.define({render_mode:[n.RenderMode,\"canvas\"]})}}s.TextAnnotation=u,u.__name__=\"TextAnnotation\",u.init_TextAnnotation()},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=t(1),o=t(161),l=t(85),a=i.__importStar(t(28)),n=t(72),r=i.__importStar(t(18));class _ extends o.TextAnnotationView{initialize(){if(super.initialize(),this.set_data(this.model.source),\"css\"==this.model.render_mode)for(let t=0,e=this._text.length;t{this.set_data(this.model.source),this.render()}),this.connect(this.model.source.streaming,()=>{this.set_data(this.model.source),this.render()}),this.connect(this.model.source.patching,()=>{this.set_data(this.model.source),this.render()}),this.connect(this.model.source.change,()=>{this.set_data(this.model.source),this.render()})):(this.connect(this.model.change,()=>{this.set_data(this.model.source),this.plot_view.request_render()}),this.connect(this.model.source.streaming,()=>{this.set_data(this.model.source),this.plot_view.request_render()}),this.connect(this.model.source.patching,()=>{this.set_data(this.model.source),this.plot_view.request_render()}),this.connect(this.model.source.change,()=>{this.set_data(this.model.source),this.plot_view.request_render()}))}set_data(t){super.set_data(t),this.visuals.warm_cache(t)}_map_data(){const t=this.coordinates.x_scale,e=this.coordinates.y_scale,s=null!=this.panel?this.panel:this.plot_view.frame;return[\"data\"==this.model.x_units?t.v_compute(this._x):s.xview.v_compute(this._x),\"data\"==this.model.y_units?e.v_compute(this._y):s.yview.v_compute(this._y)]}_render(){const t=\"canvas\"==this.model.render_mode?this._v_canvas_text.bind(this):this._v_css_text.bind(this),{ctx:e}=this.layer,[s,i]=this._map_data();for(let o=0,l=this._text.length;onew l.ColumnDataSource]}),this.override({background_fill_color:null,border_line_color:null})}}s.LabelSet=h,h.__name__=\"LabelSet\",h.init_LabelSet()},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),l=t(36),n=s.__importStar(t(28)),h=s.__importStar(t(18)),a=t(15),_=t(159),o=t(79),r=t(9),d=t(8),c=t(11);class g extends l.AnnotationView{cursor(t,e){return\"none\"==this.model.click_policy?null:\"pointer\"}get legend_padding(){return null!=this.visuals.border_line.line_color.value()?this.model.padding:0}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.plot_view.request_render()),this.connect(this.model.item_change,()=>this.plot_view.request_render())}compute_legend_bbox(){const t=this.model.get_legend_names(),{glyph_height:e,glyph_width:i}=this.model,{label_height:s,label_width:l}=this.model;this.max_label_height=r.max([_.measure_font(this.visuals.label_text.font_value()).height,s,e]);const{ctx:n}=this.layer;n.save(),this.visuals.label_text.set_value(n),this.text_widths=new Map;for(const e of t)this.text_widths.set(e,r.max([n.measureText(e).width,l]));this.visuals.title_text.set_value(n),this.title_height=this.model.title?_.measure_font(this.visuals.title_text.font_value()).height+this.model.title_standoff:0,this.title_width=this.model.title?n.measureText(this.model.title).width:0,n.restore();const h=Math.max(r.max([...this.text_widths.values()]),0),a=this.model.margin,{legend_padding:g}=this,m=this.model.spacing,{label_standoff:b}=this.model;let u,f;if(\"vertical\"==this.model.orientation)u=t.length*this.max_label_height+Math.max(t.length-1,0)*m+2*g+this.title_height,f=r.max([h+i+b+2*g,this.title_width+2*g]);else{let e=2*g+Math.max(t.length-1,0)*m;for(const[,t]of this.text_widths)e+=r.max([t,l])+i+b;f=r.max([this.title_width+2*g,e]),u=this.max_label_height+this.title_height+2*g}const x=null!=this.panel?this.panel:this.plot_view.frame,[p,w]=x.bbox.ranges,{location:v}=this.model;let y,k;if(d.isString(v))switch(v){case\"top_left\":y=p.start+a,k=w.start+a;break;case\"top_center\":y=(p.end+p.start)/2-f/2,k=w.start+a;break;case\"top_right\":y=p.end-a-f,k=w.start+a;break;case\"bottom_right\":y=p.end-a-f,k=w.end-a-u;break;case\"bottom_center\":y=(p.end+p.start)/2-f/2,k=w.end-a-u;break;case\"bottom_left\":y=p.start+a,k=w.end-a-u;break;case\"center_left\":y=p.start+a,k=(w.end+w.start)/2-u/2;break;case\"center\":y=(p.end+p.start)/2-f/2,k=(w.end+w.start)/2-u/2;break;case\"center_right\":y=p.end-a-f,k=(w.end+w.start)/2-u/2}else if(d.isArray(v)&&2==v.length){const[t,e]=v;y=x.xview.compute(t),k=x.yview.compute(e)-u}else c.unreachable();return new o.BBox({left:y,top:k,width:f,height:u})}interactive_bbox(){return this.compute_legend_bbox()}interactive_hit(t,e){return this.interactive_bbox().contains(t,e)}on_hit(t,e){let i;const{glyph_width:s}=this.model,{legend_padding:l}=this,n=this.model.spacing,{label_standoff:h}=this.model;let a=i=l;const _=this.compute_legend_bbox(),r=\"vertical\"==this.model.orientation;for(const d of this.model.items){const c=d.get_labels_list_from_label_prop();for(const g of c){const c=_.x+a,m=_.y+i+this.title_height;let b,u;[b,u]=r?[_.width-2*l,this.max_label_height]:[this.text_widths.get(g)+s+h,this.max_label_height];if(new o.BBox({left:c,top:m,width:b,height:u}).contains(t,e)){switch(this.model.click_policy){case\"hide\":for(const t of d.renderers)t.visible=!t.visible;break;case\"mute\":for(const t of d.renderers)t.muted=!t.muted}return!0}r?i+=this.max_label_height+n:a+=this.text_widths.get(g)+s+h+n}}return!1}_render(){if(0==this.model.items.length)return;for(const t of this.model.items)t.legend=this.model;const{ctx:t}=this.layer,e=this.compute_legend_bbox();t.save(),this._draw_legend_box(t,e),this._draw_legend_items(t,e),this.model.title&&this._draw_title(t,e),t.restore()}_draw_legend_box(t,e){t.beginPath(),t.rect(e.x,e.y,e.width,e.height),this.visuals.background_fill.set_value(t),t.fill(),this.visuals.border_line.doit&&(this.visuals.border_line.set_value(t),t.stroke())}_draw_legend_items(t,e){const{glyph_width:i,glyph_height:s}=this.model,{legend_padding:l}=this,n=this.model.spacing,{label_standoff:h}=this.model;let a=l,_=l;const o=\"vertical\"==this.model.orientation;for(const d of this.model.items){const c=d.get_labels_list_from_label_prop(),g=d.get_field_from_label_prop();if(0==c.length)continue;const m=(()=>{switch(this.model.click_policy){case\"none\":return!0;case\"hide\":return r.every(d.renderers,t=>t.visible);case\"mute\":return r.every(d.renderers,t=>!t.muted)}})();for(const r of c){const c=e.x+a,b=e.y+_+this.title_height,u=c+i,f=b+s;o?_+=this.max_label_height+n:a+=this.text_widths.get(r)+i+h+n,this.visuals.label_text.set_value(t),t.fillText(r,u+h,b+this.max_label_height/2);for(const e of d.renderers){this.plot_view.renderer_views.get(e).draw_legend(t,c,u,b,f,g,r,d.index)}if(!m){let s,n;[s,n]=o?[e.width-2*l,this.max_label_height]:[this.text_widths.get(r)+i+h,this.max_label_height],t.beginPath(),t.rect(c,b,s,n),this.visuals.inactive_fill.set_value(t),t.fill()}}}}_draw_title(t,e){this.visuals.title_text.doit&&(t.save(),t.translate(e.x0,e.y0+this.title_height),this.visuals.title_text.set_value(t),t.fillText(this.model.title,this.legend_padding,this.legend_padding-this.model.title_standoff),t.restore())}_get_size(){const{width:t,height:e}=this.compute_legend_bbox();return{width:t+2*this.model.margin,height:e+2*this.model.margin}}}i.LegendView=g,g.__name__=\"LegendView\";class m extends l.Annotation{constructor(t){super(t)}initialize(){super.initialize(),this.item_change=new a.Signal0(this,\"item_change\")}static init_Legend(){this.prototype.default_view=g,this.mixins([[\"label_\",n.Text],[\"title_\",n.Text],[\"inactive_\",n.Fill],[\"border_\",n.Line],[\"background_\",n.Fill]]),this.define({orientation:[h.Orientation,\"vertical\"],location:[h.Any,\"top_right\"],title:[h.String],title_standoff:[h.Number,5],label_standoff:[h.Number,5],glyph_height:[h.Number,20],glyph_width:[h.Number,20],label_height:[h.Number,20],label_width:[h.Number,20],margin:[h.Number,10],padding:[h.Number,10],spacing:[h.Number,3],items:[h.Array,[]],click_policy:[h.Any,\"none\"]}),this.override({border_line_color:\"#e5e5e5\",border_line_alpha:.5,border_line_width:1,background_fill_color:\"#ffffff\",background_fill_alpha:.95,inactive_fill_color:\"white\",inactive_fill_alpha:.7,label_text_font_size:\"13px\",label_text_baseline:\"middle\",title_text_font_size:\"13px\",title_text_font_style:\"italic\"})}get_legend_names(){const t=[];for(const e of this.items){const i=e.get_labels_list_from_label_prop();t.push(...i)}return t}}i.Legend=m,m.__name__=\"Legend\",m.init_Legend()},\n function _(e,r,n){Object.defineProperty(n,\"__esModule\",{value:!0});const t=e(1),l=e(81),i=e(86),s=e(165),o=t.__importStar(e(18)),_=e(19),a=e(9);class u extends l.Model{constructor(e){super(e)}static init_LegendItem(){this.define({label:[o.StringSpec,null],renderers:[o.Array,[]],index:[o.Number,null]})}_check_data_sources_on_renderers(){if(null!=this.get_field_from_label_prop()){if(this.renderers.length<1)return!1;const e=this.renderers[0].data_source;if(null!=e)for(const r of this.renderers)if(r.data_source!=e)return!1}return!0}_check_field_label_on_data_source(){const e=this.get_field_from_label_prop();if(null!=e){if(this.renderers.length<1)return!1;const r=this.renderers[0].data_source;if(null!=r&&!a.includes(r.columns(),e))return!1}return!0}initialize(){super.initialize(),this.legend=null,this.connect(this.change,()=>{var e;return null===(e=this.legend)||void 0===e?void 0:e.item_change.emit()});this._check_data_sources_on_renderers()||_.logger.error(\"Non matching data sources on legend item renderers\");this._check_field_label_on_data_source()||_.logger.error(\"Bad column name on label: \"+this.label)}get_field_from_label_prop(){const{label:e}=this;return s.isField(e)?e.field:null}get_labels_list_from_label_prop(){if(s.isValue(this.label)){const{value:e}=this.label;return null!=e?[e]:[]}const e=this.get_field_from_label_prop();if(null!=e){let r;if(!this.renderers[0]||null==this.renderers[0].data_source)return[\"No source found\"];if(r=this.renderers[0].data_source,r instanceof i.ColumnarDataSource){const n=r.get_column(e);return null!=n?a.uniq(Array.from(n)):[\"Invalid field\"]}}return[]}}n.LegendItem=u,u.__name__=\"LegendItem\",u.init_LegendItem()},\n function _(e,i,n){Object.defineProperty(n,\"__esModule\",{value:!0});const t=e(8);n.isValue=function(e){return t.isPlainObject(e)&&\"value\"in e},n.isField=function(e){return t.isPlainObject(e)&&\"field\"in e}},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=t(1),s=t(36),o=n.__importStar(t(28)),l=t(15),a=n.__importStar(t(18));class r extends s.AnnotationView{connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.plot_view.request_render()),this.connect(this.model.data_update,()=>this.plot_view.request_render())}_render(){const{xs:t,ys:e}=this.model;if(t.length!=e.length)return;if(t.length<3||e.length<3)return;const{frame:i}=this.plot_view,{ctx:n}=this.layer;for(let s=0,o=t.length;sthis.plot_view.request_render())}_render(){const e=this.model.gradient,t=this.model.y_intercept;if(null==e||null==t)return;const{frame:i}=this.plot_view,n=this.coordinates.x_scale,o=this.coordinates.y_scale,s=i.bbox.top,l=s+i.bbox.height,r=(o.invert(s)-t)/e,_=(o.invert(l)-t)/e,a=n.compute(r),c=n.compute(_),{ctx:p}=this.layer;p.save(),p.beginPath(),this.visuals.line.set_value(p),p.moveTo(a,s),p.lineTo(c,l),p.stroke(),p.restore()}}i.SlopeView=r,r.__name__=\"SlopeView\";class _ extends o.Annotation{constructor(e){super(e)}static init_Slope(){this.prototype.default_view=r,this.mixins(s.Line),this.define({gradient:[l.Number,null],y_intercept:[l.Number,null]}),this.override({line_color:\"black\"})}}i.Slope=_,_.__name__=\"Slope\",_.init_Slope()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),o=e(36),s=n.__importStar(e(28)),a=n.__importStar(e(18));class l extends o.AnnotationView{connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.plot_view.request_paint(this))}_render(){const{location:e}=this.model;if(null==e)return;const{frame:t}=this.plot_view,i=this.coordinates.x_scale,n=this.coordinates.y_scale,o=(t,i)=>\"data\"==this.model.location_units?t.compute(e):this.model.for_hover?e:i.compute(e);let s,a,l,r;\"width\"==this.model.dimension?(l=o(n,t.yview),a=t.bbox.left,r=t.bbox.width,s=this.model.properties.line_width.value()):(l=t.bbox.top,a=o(i,t.xview),r=this.model.properties.line_width.value(),s=t.bbox.height);const{ctx:_}=this.layer;_.save(),_.beginPath(),this.visuals.line.set_value(_),_.moveTo(a,l),\"width\"==this.model.dimension?_.lineTo(a+r,l):_.lineTo(a,l+s),_.stroke(),_.restore()}}i.SpanView=l,l.__name__=\"SpanView\";class r extends o.Annotation{constructor(e){super(e)}static init_Span(){this.prototype.default_view=l,this.mixins(s.Line),this.define({render_mode:[a.RenderMode,\"canvas\"],location:[a.Number,null],location_units:[a.SpatialUnits,\"data\"],dimension:[a.Dimension,\"width\"]}),this.override({line_color:\"black\"}),this.internal({for_hover:[a.Boolean,!1]})}}i.Span=r,r.__name__=\"Span\",r.init_Span()},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const l=t(1),s=t(161),a=t(74),n=l.__importStar(t(28)),o=l.__importStar(t(18));class r extends s.TextAnnotationView{initialize(){super.initialize(),this.visuals.text=new a.Text(this.model)}_get_location(){const t=this.panel,e=this.model.offset;let i,l;const{bbox:s}=t;switch(t.side){case\"above\":case\"below\":switch(this.model.vertical_align){case\"top\":l=s.top+5;break;case\"middle\":l=s.vcenter;break;case\"bottom\":l=s.bottom-5}switch(this.model.align){case\"left\":i=s.left+e;break;case\"center\":i=s.hcenter;break;case\"right\":i=s.right-e}break;case\"left\":switch(this.model.vertical_align){case\"top\":i=s.left-5;break;case\"middle\":i=s.hcenter;break;case\"bottom\":i=s.right+5}switch(this.model.align){case\"left\":l=s.bottom-e;break;case\"center\":l=s.vcenter;break;case\"right\":l=s.top+e}break;case\"right\":switch(this.model.vertical_align){case\"top\":i=s.right-5;break;case\"middle\":i=s.hcenter;break;case\"bottom\":i=s.left+5}switch(this.model.align){case\"left\":l=s.top+e;break;case\"center\":l=s.vcenter;break;case\"right\":l=s.bottom-e}}return[i,l]}_render(){const{text:t}=this.model;if(null==t||0==t.length)return;this.model.text_baseline=this.model.vertical_align,this.model.text_align=this.model.align;const[e,i]=this._get_location(),l=this.panel.get_label_angle_heuristic(\"parallel\");(\"canvas\"==this.model.render_mode?this._canvas_text.bind(this):this._css_text.bind(this))(this.layer.ctx,t,e,i,l)}_get_size(){const{text:t}=this.model;if(null==t||0==t.length)return{width:0,height:0};{this.visuals.text.set_value(this.layer.ctx);const{width:e,ascent:i}=this.layer.ctx.measureText(t);return{width:e,height:i*this.visuals.text.text_line_height.value()+10}}}}i.TitleView=r,r.__name__=\"TitleView\";class c extends s.TextAnnotation{constructor(t){super(t)}static init_Title(){this.prototype.default_view=r,this.mixins([[\"border_\",n.Line],[\"background_\",n.Fill]]),this.define({text:[o.String],text_font:[o.Font,\"helvetica\"],text_font_size:[o.StringSpec,\"13px\"],text_font_style:[o.FontStyle,\"bold\"],text_color:[o.ColorSpec,\"#444444\"],text_alpha:[o.NumberSpec,1],text_line_height:[o.Number,1],vertical_align:[o.VerticalAlign,\"bottom\"],align:[o.TextAlign,\"left\"],offset:[o.Number,0]}),this.override({background_fill_color:null,border_line_color:null}),this.internal({text_align:[o.TextAlign,\"left\"],text_baseline:[o.TextBaseline,\"bottom\"]})}}i.Title=c,c.__name__=\"Title\",c.init_Title()},\n function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=e(1),l=e(36),s=e(115),a=e(72),n=e(79),r=o.__importStar(e(18));class _ extends l.AnnotationView{constructor(){super(...arguments),this.rotate=!0,this._invalidate_toolbar=!0,this._previous_bbox=new n.BBox}initialize(){super.initialize(),this.el=a.div(),this.plot_view.canvas_view.add_event(this.el)}async lazy_initialize(){this._toolbar_view=await s.build_view(this.model.toolbar,{parent:this}),this.plot_view.visibility_callbacks.push(e=>this._toolbar_view.set_visibility(e))}remove(){this._toolbar_view.remove(),a.remove(this.el),super.remove()}render(){this.model.visible||a.undisplay(this.el),super.render()}_render(){const{bbox:e}=this.panel;this._previous_bbox.equals(e)||(a.position(this.el,e),this._previous_bbox=e),this._invalidate_toolbar&&(this.el.style.position=\"absolute\",this.el.style.overflow=\"hidden\",this._toolbar_view.render(),a.empty(this.el),this.el.appendChild(this._toolbar_view.el),this._invalidate_toolbar=!1),a.display(this.el)}_get_size(){const{tools:e,logo:i}=this.model.toolbar;return{width:30*e.length+(null!=i?25:0),height:30}}}t.ToolbarPanelView=_,_.__name__=\"ToolbarPanelView\";class h extends l.Annotation{constructor(e){super(e)}static init_ToolbarPanel(){this.prototype.default_view=_,this.define({toolbar:[r.Instance]})}}t.ToolbarPanel=h,h.__name__=\"ToolbarPanel\",h.init_ToolbarPanel()},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),l=t(36),o=t(72),n=s.__importStar(t(18)),a=t(172),h=t(173),r=s.__importDefault(t(174));class c extends l.AnnotationView{initialize(){super.initialize(),this.el=o.div({class:a.bk_tooltip}),o.undisplay(this.el),this.plot_view.canvas_view.add_overlay(this.el)}remove(){o.remove(this.el),super.remove()}connect_signals(){super.connect_signals(),this.connect(this.model.properties.content.change,()=>this.render()),this.connect(this.model.properties.position.change,()=>this._reposition())}styles(){return[...super.styles(),r.default]}render(){this.model.visible||o.undisplay(this.el),super.render()}_render(){const{content:t}=this.model;null!=t?(o.empty(this.el),o.classes(this.el).toggle(a.bk_tooltip_custom,this.model.custom),this.el.appendChild(t),this.model.show_arrow&&this.el.classList.add(a.bk_tooltip_arrow)):o.undisplay(this.el)}_reposition(){const{position:t}=this.model;if(null==t)return void o.undisplay(this.el);const[e,i]=t,s=(()=>{const t=this.parent.layout.bbox.relativize(),{attachment:s}=this.model;switch(s){case\"horizontal\":return eo.div()],custom:[n.Any]})}clear(){this.position=null}}i.Tooltip=d,d.__name__=\"Tooltip\",d.init_Tooltip()},\n function _(o,t,l){Object.defineProperty(l,\"__esModule\",{value:!0}),l.bk_tooltip=\"bk-tooltip\",l.bk_tooltip_arrow=\"bk-tooltip-arrow\",l.bk_tooltip_custom=\"bk-tooltip-custom\",l.bk_tooltip_row_label=\"bk-tooltip-row-label\",l.bk_tooltip_row_value=\"bk-tooltip-row-value\",l.bk_tooltip_color_block=\"bk-tooltip-color-block\"},\n function _(e,b,k){Object.defineProperty(k,\"__esModule\",{value:!0}),k.bk_active=\"bk-active\",k.bk_inline=\"bk-inline\",k.bk_left=\"bk-left\",k.bk_right=\"bk-right\",k.bk_above=\"bk-above\",k.bk_below=\"bk-below\",k.bk_up=\"bk-up\",k.bk_down=\"bk-down\",k.bk_side=function(e){switch(e){case\"above\":return k.bk_above;case\"below\":return k.bk_below;case\"left\":return k.bk_left;case\"right\":return k.bk_right}}},\n function _(o,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});t.default='\\n.bk-root {\\n /* Same border color used everywhere */\\n /* Gray of icons */\\n}\\n.bk-root .bk-tooltip {\\n font-weight: 300;\\n font-size: 12px;\\n position: absolute;\\n padding: 5px;\\n border: 1px solid #e5e5e5;\\n color: #2f2f2f;\\n background-color: white;\\n pointer-events: none;\\n opacity: 0.95;\\n z-index: 100;\\n}\\n.bk-root .bk-tooltip > div:not(:first-child) {\\n /* gives space when multiple elements are being hovered over */\\n margin-top: 5px;\\n border-top: #e5e5e5 1px dashed;\\n}\\n.bk-root .bk-tooltip.bk-left.bk-tooltip-arrow::before {\\n position: absolute;\\n margin: -7px 0 0 0;\\n top: 50%;\\n width: 0;\\n height: 0;\\n border-style: solid;\\n border-width: 7px 0 7px 0;\\n border-color: transparent;\\n content: \" \";\\n display: block;\\n left: -10px;\\n border-right-width: 10px;\\n border-right-color: #909599;\\n}\\n.bk-root .bk-tooltip.bk-left::before {\\n left: -10px;\\n border-right-width: 10px;\\n border-right-color: #909599;\\n}\\n.bk-root .bk-tooltip.bk-right.bk-tooltip-arrow::after {\\n position: absolute;\\n margin: -7px 0 0 0;\\n top: 50%;\\n width: 0;\\n height: 0;\\n border-style: solid;\\n border-width: 7px 0 7px 0;\\n border-color: transparent;\\n content: \" \";\\n display: block;\\n right: -10px;\\n border-left-width: 10px;\\n border-left-color: #909599;\\n}\\n.bk-root .bk-tooltip.bk-right::after {\\n right: -10px;\\n border-left-width: 10px;\\n border-left-color: #909599;\\n}\\n.bk-root .bk-tooltip.bk-above::before {\\n position: absolute;\\n margin: 0 0 0 -7px;\\n left: 50%;\\n width: 0;\\n height: 0;\\n border-style: solid;\\n border-width: 0 7px 0 7px;\\n border-color: transparent;\\n content: \" \";\\n display: block;\\n top: -10px;\\n border-bottom-width: 10px;\\n border-bottom-color: #909599;\\n}\\n.bk-root .bk-tooltip.bk-below::after {\\n position: absolute;\\n margin: 0 0 0 -7px;\\n left: 50%;\\n width: 0;\\n height: 0;\\n border-style: solid;\\n border-width: 0 7px 0 7px;\\n border-color: transparent;\\n content: \" \";\\n display: block;\\n bottom: -10px;\\n border-top-width: 10px;\\n border-top-color: #909599;\\n}\\n.bk-root .bk-tooltip-row-label {\\n text-align: right;\\n color: #26aae1;\\n /* blue from toolbar highlighting */\\n}\\n.bk-root .bk-tooltip-row-value {\\n color: default;\\n /* seems to be necessary for notebook */\\n}\\n.bk-root .bk-tooltip-color-block {\\n width: 12px;\\n height: 12px;\\n margin-left: 5px;\\n margin-right: 5px;\\n outline: #dddddd solid 1px;\\n display: inline-block;\\n}\\n'},\n function _(e,s,t){Object.defineProperty(t,\"__esModule\",{value:!0});const i=e(1),r=e(123),o=e(84),h=e(28),n=i.__importStar(e(18));class l extends r.UpperLowerView{connect_signals(){super.connect_signals(),this.connect(this.model.source.streaming,()=>this.set_data(this.model.source)),this.connect(this.model.source.patching,()=>this.set_data(this.model.source)),this.connect(this.model.source.change,()=>this.set_data(this.model.source))}_render(){this._map_data();const{ctx:e}=this.layer;if(this.visuals.line.doit)for(let s=0,t=this._lower_sx.length;snew o.TeeHead({level:\"underlay\",size:10})],upper_head:[n.Instance,()=>new o.TeeHead({level:\"underlay\",size:10})]}),this.override({level:\"underlay\"})}}t.Whisker=_,_.__name__=\"Whisker\",_.init_Whisker()},\n function _(i,a,e){Object.defineProperty(e,\"__esModule\",{value:!0});var r=i(177);e.Axis=r.Axis;var s=i(179);e.CategoricalAxis=s.CategoricalAxis;var x=i(182);e.ContinuousAxis=x.ContinuousAxis;var A=i(183);e.DatetimeAxis=A.DatetimeAxis;var o=i(184);e.LinearAxis=o.LinearAxis;var t=i(197);e.LogAxis=t.LogAxis;var n=i(200);e.MercatorAxis=n.MercatorAxis},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),a=t(178),l=s.__importStar(t(28)),n=s.__importStar(t(18)),o=t(9),r=t(8),_=t(98),{abs:h,min:c,max:d}=Math;class m extends a.GuideRendererView{constructor(){super(...arguments),this.rotate=!0}get panel(){return this.layout}get is_renderable(){const[t,e]=this.ranges;return t.is_valid&&e.is_valid}_render(){var t;if(!this.is_renderable)return;const e={tick:this._tick_extent(),tick_label:this._tick_label_extents(),axis_label:this._axis_label_extent()},{tick_coords:i}=this,s=this.layer.ctx;s.save(),this._draw_rule(s,e),this._draw_major_ticks(s,e,i),this._draw_minor_ticks(s,e,i),this._draw_major_labels(s,e,i),this._draw_axis_label(s,e,i),null===(t=this._paint)||void 0===t||t.call(this,s,e,i),s.restore()}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.plot_view.request_layout())}get_size(){if(this.model.visible&&null==this.model.fixed_location&&this.is_renderable){const t=this._get_size();return{width:0,height:Math.round(t)}}return{width:0,height:0}}_get_size(){return this._tick_extent()+this._tick_label_extent()+this._axis_label_extent()}get needs_clip(){return null!=this.model.fixed_location}_draw_rule(t,e){if(!this.visuals.axis_line.doit)return;const[i,s]=this.rule_coords,[a,l]=this.coordinates.map_to_screen(i,s),[n,o]=this.normals,[r,_]=this.offsets;this.visuals.axis_line.set_value(t),t.beginPath(),t.moveTo(Math.round(a[0]+n*r),Math.round(l[0]+o*_));for(let e=1;ec&&(c=o)}return c>0&&(c+=s),c}get normals(){return this.panel.normals}get dimension(){return this.panel.dimension}compute_labels(t){const e=this.model.formatter.doFormat(t,this);for(let i=0;ih(n-o)?(t=d(c(a,l),n),s=c(d(a,l),o)):(t=c(a,l),s=d(a,l)),[t,s]}}get rule_coords(){const t=this.dimension,e=(t+1)%2,[i]=this.ranges,[s,a]=this.computed_bounds,l=[new Array(2),new Array(2)];return l[t][0]=Math.max(s,i.min),l[t][1]=Math.min(a,i.max),l[t][0]>l[t][1]&&(l[t][0]=l[t][1]=NaN),l[e][0]=this.loc,l[e][1]=this.loc,l}get tick_coords(){const t=this.dimension,e=(t+1)%2,[i]=this.ranges,[s,a]=this.computed_bounds,l=this.model.ticker.get_ticks(s,a,i,this.loc,{}),n=l.major,o=l.minor,r=[[],[]],_=[[],[]],[h,c]=[i.min,i.max];for(let i=0;ic||(r[t].push(n[i]),r[e].push(this.loc));for(let i=0;ic||(_[t].push(o[i]),_[e].push(this.loc));return{major:r,minor:_}}get loc(){const{fixed_location:t}=this.model;if(null!=t){if(r.isNumber(t))return t;const[,e]=this.ranges;if(e instanceof _.FactorRange)return e.synthetic(t);throw new Error(\"unexpected\")}const[,e]=this.ranges;switch(this.panel.side){case\"left\":case\"below\":return e.start;case\"right\":case\"above\":return e.end}}serializable_state(){return Object.assign(Object.assign({},super.serializable_state()),{bbox:this.layout.bbox.box})}}i.AxisView=m,m.__name__=\"AxisView\";class b extends a.GuideRenderer{constructor(t){super(t)}static init_Axis(){this.prototype.default_view=m,this.mixins([[\"axis_\",l.Line],[\"major_tick_\",l.Line],[\"minor_tick_\",l.Line],[\"major_label_\",l.Text],[\"axis_label_\",l.Text]]),this.define({bounds:[n.Any,\"auto\"],ticker:[n.Instance],formatter:[n.Instance],axis_label:[n.String,\"\"],axis_label_standoff:[n.Int,5],major_label_standoff:[n.Int,5],major_label_orientation:[n.Any,\"horizontal\"],major_label_overrides:[n.Any,{}],major_tick_in:[n.Number,2],major_tick_out:[n.Number,6],minor_tick_in:[n.Number,0],minor_tick_out:[n.Number,4],fixed_location:[n.Any,null]}),this.override({axis_line_color:\"black\",major_tick_line_color:\"black\",minor_tick_line_color:\"black\",major_label_text_font_size:\"11px\",major_label_text_align:\"center\",major_label_text_baseline:\"alphabetic\",axis_label_text_font_size:\"13px\",axis_label_text_font_style:\"italic\"})}}i.Axis=b,b.__name__=\"Axis\",b.init_Axis()},\n function _(e,r,d){Object.defineProperty(d,\"__esModule\",{value:!0});const i=e(70);class n extends i.RendererView{}d.GuideRendererView=n,n.__name__=\"GuideRendererView\";class t extends i.Renderer{constructor(e){super(e)}static init_GuideRenderer(){this.override({level:\"guide\"})}}d.GuideRenderer=t,t.__name__=\"GuideRenderer\",t.init_GuideRenderer()},\n function _(t,s,o){Object.defineProperty(o,\"__esModule\",{value:!0});const e=t(1),i=t(177),r=t(180),a=t(181),l=e.__importStar(t(28)),_=e.__importStar(t(18));class n extends i.AxisView{_paint(t,s,o){this._draw_group_separators(t,s,o)}_draw_group_separators(t,s,o){const[e]=this.ranges,[i,r]=this.computed_bounds;if(!e.tops||e.tops.length<2||!this.visuals.separator_line.doit)return;const a=this.dimension,l=(a+1)%2,_=[[],[]];let n=0;for(let t=0;ti&&ht[1]),s=this.model.formatter.doFormat(t,this);a.push([s,r.major,this.model.major_label_orientation,this.visuals.major_label_text]),a.push([i.tops,r.tops,this.model.group_label_orientation,this.visuals.group_text])}else if(3==t.levels){const t=i.major.map(t=>t[2]),s=this.model.formatter.doFormat(t,this),o=i.mids.map(t=>t[1]);a.push([s,r.major,this.model.major_label_orientation,this.visuals.major_label_text]),a.push([o,r.mids,this.model.subgroup_label_orientation,this.visuals.subgroup_text]),a.push([i.tops,r.tops,this.model.group_label_orientation,this.visuals.group_text])}return a}get tick_coords(){const t=this.dimension,s=(t+1)%2,[o]=this.ranges,[e,i]=this.computed_bounds,r=this.model.ticker.get_ticks(e,i,o,this.loc,{}),a={major:[[],[]],mids:[[],[]],tops:[[],[]],minor:[[],[]]};return a.major[t]=r.major,a.major[s]=r.major.map(t=>this.loc),3==o.levels&&(a.mids[t]=r.mids,a.mids[s]=r.mids.map(t=>this.loc)),o.levels>1&&(a.tops[t]=r.tops,a.tops[s]=r.tops.map(t=>this.loc)),a}}o.CategoricalAxisView=n,n.__name__=\"CategoricalAxisView\";class h extends i.Axis{constructor(t){super(t)}static init_CategoricalAxis(){this.prototype.default_view=n,this.mixins([[\"separator_\",l.Line],[\"group_\",l.Text],[\"subgroup_\",l.Text]]),this.define({group_label_orientation:[_.Any,\"parallel\"],subgroup_label_orientation:[_.Any,\"parallel\"]}),this.override({ticker:()=>new r.CategoricalTicker,formatter:()=>new a.CategoricalTickFormatter,separator_line_color:\"lightgrey\",separator_line_width:2,group_text_font_style:\"bold\",group_text_font_size:\"11px\",group_text_color:\"grey\",subgroup_text_font_style:\"bold\",subgroup_text_font_size:\"11px\"})}}o.CategoricalAxis=h,h.__name__=\"CategoricalAxis\",h.init_CategoricalAxis()},\n function _(t,c,e){Object.defineProperty(e,\"__esModule\",{value:!0});const o=t(129);class s extends o.Ticker{constructor(t){super(t)}get_ticks(t,c,e,o,s){return{major:this._collect(e.factors,e,t,c),minor:[],tops:this._collect(e.tops||[],e,t,c),mids:this._collect(e.mids||[],e,t,c)}}_collect(t,c,e,o){const s=[];for(const r of t){const t=c.synthetic(r);t>e&&tnew r.DatetimeTicker,formatter:()=>new a.DatetimeTickFormatter})}}i.DatetimeAxis=_,_.__name__=\"DatetimeAxis\",_.init_DatetimeAxis()},\n function _(e,i,s){Object.defineProperty(s,\"__esModule\",{value:!0});const t=e(177),n=e(182),r=e(130),a=e(126);class _ extends t.AxisView{}s.LinearAxisView=_,_.__name__=\"LinearAxisView\";class c extends n.ContinuousAxis{constructor(e){super(e)}static init_LinearAxis(){this.prototype.default_view=_,this.override({ticker:()=>new a.BasicTicker,formatter:()=>new r.BasicTickFormatter})}}s.LinearAxis=c,c.__name__=\"LinearAxis\",c.init_LinearAxis()},\n function _(t,s,e){Object.defineProperty(e,\"__esModule\",{value:!0});const r=t(1),i=r.__importDefault(t(186)),n=t(131),o=t(19),a=r.__importStar(t(18)),c=t(187),m=t(9),u=t(8);function h(t){return i.default(t,\"%Y %m %d %H %M %S\").split(/\\s+/).map(t=>parseInt(t,10))}function d(t,s){if(u.isFunction(s))return s(t);{const e=c.sprintf(\"$1%06d\",function(t){return Math.round(t/1e3%1*1e6)}(t));return-1==(s=s.replace(/((^|[^%])(%%)*)%f/,e)).indexOf(\"%\")?s:i.default(t,s)}}const l=[\"microseconds\",\"milliseconds\",\"seconds\",\"minsec\",\"minutes\",\"hourmin\",\"hours\",\"days\",\"months\",\"years\"];class _ extends n.TickFormatter{constructor(t){super(t),this.strip_leading_zeros=!0}static init_DatetimeTickFormatter(){this.define({microseconds:[a.Array,[\"%fus\"]],milliseconds:[a.Array,[\"%3Nms\",\"%S.%3Ns\"]],seconds:[a.Array,[\"%Ss\"]],minsec:[a.Array,[\":%M:%S\"]],minutes:[a.Array,[\":%M\",\"%Mm\"]],hourmin:[a.Array,[\"%H:%M\"]],hours:[a.Array,[\"%Hh\",\"%H:%M\"]],days:[a.Array,[\"%m/%d\",\"%a%d\"]],months:[a.Array,[\"%m/%Y\",\"%b %Y\"]],years:[a.Array,[\"%Y\"]]})}initialize(){super.initialize(),this._update_width_formats()}_update_width_formats(){const t=+i.default(new Date),s=function(s){const e=s.map(s=>d(t,s).length),r=m.sort_by(m.zip(e,s),([t])=>t);return m.unzip(r)};this._width_formats={microseconds:s(this.microseconds),milliseconds:s(this.milliseconds),seconds:s(this.seconds),minsec:s(this.minsec),minutes:s(this.minutes),hourmin:s(this.hourmin),hours:s(this.hours),days:s(this.days),months:s(this.months),years:s(this.years)}}_get_resolution_str(t,s){const e=1.1*t;switch(!1){case!(e<.001):return\"microseconds\";case!(e<1):return\"milliseconds\";case!(e<60):return s>=60?\"minsec\":\"seconds\";case!(e<3600):return s>=3600?\"hourmin\":\"minutes\";case!(e<86400):return\"hours\";case!(e<2678400):return\"days\";case!(e<31536e3):return\"months\";default:return\"years\"}}doFormat(t,s){if(0==t.length)return[];const e=Math.abs(t[t.length-1]-t[0])/1e3,r=e/(t.length-1),i=this._get_resolution_str(r,e),[,[n]]=this._width_formats[i],a=[],c=l.indexOf(i),m={};for(const t of l)m[t]=0;m.seconds=5,m.minsec=4,m.minutes=4,m.hourmin=3,m.hours=3;for(const s of t){let t,e;try{e=h(s),t=d(s,n)}catch(t){o.logger.warn(\"unable to format tick for timestamp value \"+s),o.logger.warn(\" - \"+t),a.push(\"ERR\");continue}let r=!1,u=c;for(;0==e[m[l[u]]];){let n;if(u+=1,u==l.length)break;if((\"minsec\"==i||\"hourmin\"==i)&&!r){if(\"minsec\"==i&&0==e[4]&&0!=e[5]||\"hourmin\"==i&&0==e[3]&&0!=e[4]){n=this._width_formats[l[c-1]][1][0],t=d(s,n);break}r=!0}n=this._width_formats[l[u]][1][0],t=d(s,n)}if(this.strip_leading_zeros){let s=t.replace(/^0+/g,\"\");s!=t&&isNaN(parseInt(s))&&(s=\"0\"+s),a.push(s)}else a.push(t)}return a}}e.DatetimeTickFormatter=_,_.__name__=\"DatetimeTickFormatter\",_.init_DatetimeTickFormatter()},\n function _(e,t,n){!function(e){\"object\"==typeof t&&t.exports?t.exports=e():\"function\"==typeof define?define(e):this.tz=e()}((function(){function e(e,t,n){var r,o=t.day[1];do{r=new Date(Date.UTC(n,t.month,Math.abs(o++)))}while(t.day[0]<7&&r.getUTCDay()!=t.day[0]);return(r={clock:t.clock,sort:r.getTime(),rule:t,save:6e4*t.save,offset:e.offset})[r.clock]=r.sort+6e4*t.time,r.posix?r.wallclock=r[r.clock]+(e.offset+t.saved):r.posix=r[r.clock]-(e.offset+t.saved),r}function t(t,n,r){var o,a,u,i,l,s,c,f=t[t.zone],h=[],T=new Date(r).getUTCFullYear(),g=1;for(o=1,a=f.length;o=T-g;--c)for(o=0,a=s.length;o=h[o][n]&&h[o][h[o].clock]>u[h[o].clock]&&(i=h[o])}return i&&((l=/^(.*)\\/(.*)$/.exec(u.format))?i.abbrev=l[i.save?2:1]:i.abbrev=u.format.replace(/%s/,i.rule.letter)),i||u}function n(e,n){return\"UTC\"==e.zone?n:(e.entry=t(e,\"posix\",n),n+e.entry.offset+e.entry.save)}function r(e,n){return\"UTC\"==e.zone?n:(e.entry=r=t(e,\"wallclock\",n),0<(o=n-r.wallclock)&&o9)t+=s*l[c-10];else{if(a=new Date(n(e,t)),c<7)for(;s;)a.setUTCDate(a.getUTCDate()+i),a.getUTCDay()==c&&(s-=i);else 7==c?a.setUTCFullYear(a.getUTCFullYear()+s):8==c?a.setUTCMonth(a.getUTCMonth()+s):a.setUTCDate(a.getUTCDate()+s);null==(t=r(e,a.getTime()))&&(t=r(e,a.getTime()+864e5*i)-864e5*i)}return t}var a={clock:function(){return+new Date},zone:\"UTC\",entry:{abbrev:\"UTC\",offset:0,save:0},UTC:1,z:function(e,t,n,r){var o,a,u=this.entry.offset+this.entry.save,i=Math.abs(u/1e3),l=[],s=3600;for(o=0;o<3;o++)l.push((\"0\"+Math.floor(i/s)).slice(-2)),i%=s,s/=60;return\"^\"!=n||u?(\"^\"==n&&(r=3),3==r?(a=(a=l.join(\":\")).replace(/:00$/,\"\"),\"^\"!=n&&(a=a.replace(/:00$/,\"\"))):r?(a=l.slice(0,r+1).join(\":\"),\"^\"==n&&(a=a.replace(/:00$/,\"\"))):a=l.slice(0,2).join(\"\"),a=(a=(u<0?\"-\":\"+\")+a).replace(/([-+])(0)/,{_:\" $1\",\"-\":\"$1\"}[n]||\"$1$2\")):\"Z\"},\"%\":function(e){return\"%\"},n:function(e){return\"\\n\"},t:function(e){return\"\\t\"},U:function(e){return s(e,0)},W:function(e){return s(e,1)},V:function(e){return c(e)[0]},G:function(e){return c(e)[1]},g:function(e){return c(e)[1]%100},j:function(e){return Math.floor((e.getTime()-Date.UTC(e.getUTCFullYear(),0))/864e5)+1},s:function(e){return Math.floor(e.getTime()/1e3)},C:function(e){return Math.floor(e.getUTCFullYear()/100)},N:function(e){return e.getTime()%1e3*1e6},m:function(e){return e.getUTCMonth()+1},Y:function(e){return e.getUTCFullYear()},y:function(e){return e.getUTCFullYear()%100},H:function(e){return e.getUTCHours()},M:function(e){return e.getUTCMinutes()},S:function(e){return e.getUTCSeconds()},e:function(e){return e.getUTCDate()},d:function(e){return e.getUTCDate()},u:function(e){return e.getUTCDay()||7},w:function(e){return e.getUTCDay()},l:function(e){return e.getUTCHours()%12||12},I:function(e){return e.getUTCHours()%12||12},k:function(e){return e.getUTCHours()},Z:function(e){return this.entry.abbrev},a:function(e){return this[this.locale].day.abbrev[e.getUTCDay()]},A:function(e){return this[this.locale].day.full[e.getUTCDay()]},h:function(e){return this[this.locale].month.abbrev[e.getUTCMonth()]},b:function(e){return this[this.locale].month.abbrev[e.getUTCMonth()]},B:function(e){return this[this.locale].month.full[e.getUTCMonth()]},P:function(e){return this[this.locale].meridiem[Math.floor(e.getUTCHours()/12)].toLowerCase()},p:function(e){return this[this.locale].meridiem[Math.floor(e.getUTCHours()/12)]},R:function(e,t){return this.convert([t,\"%H:%M\"])},T:function(e,t){return this.convert([t,\"%H:%M:%S\"])},D:function(e,t){return this.convert([t,\"%m/%d/%y\"])},F:function(e,t){return this.convert([t,\"%Y-%m-%d\"])},x:function(e,t){return this.convert([t,this[this.locale].date])},r:function(e,t){return this.convert([t,this[this.locale].time12||\"%I:%M:%S\"])},X:function(e,t){return this.convert([t,this[this.locale].time24])},c:function(e,t){return this.convert([t,this[this.locale].dateTime])},convert:function(e){if(!e.length)return\"1.0.23\";var t,a,u,l,s,c=Object.create(this),f=[];for(t=0;t=o?Math.floor((n-o)/7)+1:0}function c(e){var t,n,r;return n=e.getUTCFullYear(),t=new Date(Date.UTC(n,0)).getUTCDay(),(r=s(e,1)+(t>1&&t<=4?1:0))?53!=r||4==t||3==t&&29==new Date(n,1,29).getDate()?[r,e.getUTCFullYear()]:[1,e.getUTCFullYear()+1]:(n=e.getUTCFullYear()-1,[r=4==(t=new Date(Date.UTC(n,0)).getUTCDay())||3==t&&29==new Date(n,1,29).getDate()?53:52,e.getUTCFullYear()-1])}return u=u.toLowerCase().split(\"|\"),\"delmHMSUWVgCIky\".replace(/./g,(function(e){a[e].pad=2})),a.N.pad=9,a.j.pad=3,a.k.style=\"_\",a.l.style=\"_\",a.e.style=\"_\",function(){return a.convert(arguments)}}))},\n function _(r,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=r(1),i=n.__importStar(r(188)),u=r(189),a=n.__importDefault(r(186)),f=r(29),o=r(8);function l(r,...e){return u.sprintf(r,...e)}function s(r,e,t){if(o.isNumber(r)){return l((()=>{switch(!1){case Math.floor(r)!=r:return\"%d\";case!(Math.abs(r)>.1&&Math.abs(r)<1e3):return\"%0.3f\";default:return\"%0.3e\"}})(),r)}return\"\"+r}function c(r,e,n){if(null==e)return s;if(null!=n&&r in n){const e=n[r];if(o.isString(e)){if(e in t.DEFAULT_FORMATTERS)return t.DEFAULT_FORMATTERS[e];throw new Error(`Unknown tooltip field formatter type '${e}'`)}return function(r,t,n){return e.format(r,t,n)}}return t.DEFAULT_FORMATTERS.numeral}function m(r,e,t,n){if(\"$\"==r[0]){return function(r,e){if(r in e)return e[r];throw new Error(`Unknown special variable '$${r}'`)}(r.substring(1),n)}return function(r,e,t){const n=e.get_column(r);if(null==n)return null;if(o.isNumber(t))return n[t];const i=n[t.index];if(o.isTypedArray(i)||o.isArray(i)){if(o.isArray(i[0])){return i[t.dim2][t.dim1]}return i[t.flat_index]}return i}(r.substring(1).replace(/[{}]/g,\"\"),e,t)}t.DEFAULT_FORMATTERS={numeral:(r,e,t)=>i.format(r,e),datetime:(r,e,t)=>a.default(r,e),printf:(r,e,t)=>l(e,r)},t.sprintf=l,t.basic_formatter=s,t.get_formatter=c,t.get_value=m,t.replace_placeholders=function(r,e,t,n,i={}){let u,a;if(o.isString(r)?(u=r,a=!1):(u=r.html,a=!0),u=u.replace(/@\\$name/g,r=>`@{${i.name}}`),u=u.replace(/((?:\\$\\w+)|(?:@\\w+)|(?:@{(?:[^{}]+)}))(?:{([^{}]+)})?/g,(r,u,o)=>{const l=m(u,e,t,i);if(null==l)return\"\"+f.escape(\"???\");if(\"safe\"==o)return a=!0,\"\"+l;const s=c(u,o,n);return\"\"+f.escape(s(l,o,i))}),a){return[...(new DOMParser).parseFromString(u,\"text/html\").body.childNodes]}return u}},\n function _(e,n,t){\n /*!\n * numbro.js\n * version : 1.6.2\n * author : FΓΆretagsplatsen AB\n * license : MIT\n * http://www.foretagsplatsen.se\n */\n var r,i={},a=i,o=\"en-US\",l=null,u=\"0,0\";void 0!==n&&n.exports;function c(e){this._value=e}function s(e){var n,t=\"\";for(n=0;n-1?function(e,n){var t,r,i,a;return t=(a=e.toString()).split(\"e\")[0],i=a.split(\"e\")[1],a=t.split(\".\")[0]+(r=t.split(\".\")[1]||\"\")+s(i-r.length),n>0&&(a+=\".\"+s(n)),a}(e,n):(t(e*o)/o).toFixed(n),r&&(i=new RegExp(\"0{1,\"+r+\"}$\"),a=a.replace(i,\"\")),a}function d(e,n,t){return n.indexOf(\"$\")>-1?function(e,n,t){var r,a,l=n,u=l.indexOf(\"$\"),c=l.indexOf(\"(\"),s=l.indexOf(\"+\"),f=l.indexOf(\"-\"),d=\"\",p=\"\";-1===l.indexOf(\"$\")?\"infix\"===i[o].currency.position?(p=i[o].currency.symbol,i[o].currency.spaceSeparated&&(p=\" \"+p+\" \")):i[o].currency.spaceSeparated&&(d=\" \"):l.indexOf(\" $\")>-1?(d=\" \",l=l.replace(\" $\",\"\")):l.indexOf(\"$ \")>-1?(d=\" \",l=l.replace(\"$ \",\"\")):l=l.replace(\"$\",\"\");if(a=h(e,l,t,p),-1===n.indexOf(\"$\"))switch(i[o].currency.position){case\"postfix\":a.indexOf(\")\")>-1?((a=a.split(\"\")).splice(-1,0,d+i[o].currency.symbol),a=a.join(\"\")):a=a+d+i[o].currency.symbol;break;case\"infix\":break;case\"prefix\":a.indexOf(\"(\")>-1||a.indexOf(\"-\")>-1?(a=a.split(\"\"),r=Math.max(c,f)+1,a.splice(r,0,i[o].currency.symbol+d),a=a.join(\"\")):a=i[o].currency.symbol+d+a;break;default:throw Error('Currency position should be among [\"prefix\", \"infix\", \"postfix\"]')}else u<=1?a.indexOf(\"(\")>-1||a.indexOf(\"+\")>-1||a.indexOf(\"-\")>-1?(a=a.split(\"\"),r=1,(u-1?((a=a.split(\"\")).splice(-1,0,d+i[o].currency.symbol),a=a.join(\"\")):a=a+d+i[o].currency.symbol;return a}(e,n,t):n.indexOf(\"%\")>-1?function(e,n,t){var r,i=\"\";e*=100,n.indexOf(\" %\")>-1?(i=\" \",n=n.replace(\" %\",\"\")):n=n.replace(\"%\",\"\");(r=h(e,n,t)).indexOf(\")\")>-1?((r=r.split(\"\")).splice(-1,0,i+\"%\"),r=r.join(\"\")):r=r+i+\"%\";return r}(e,n,t):n.indexOf(\":\")>-1?function(e){var n=Math.floor(e/60/60),t=Math.floor((e-60*n*60)/60),r=Math.round(e-60*n*60-60*t);return n+\":\"+(t<10?\"0\"+t:t)+\":\"+(r<10?\"0\"+r:r)}(e):h(e,n,t)}function h(e,n,t,r){var a,u,c,s,d,h,p,m,x,g,O,b,w,y,M,v,$,B=!1,E=!1,F=!1,k=\"\",U=!1,N=!1,S=!1,j=!1,D=!1,C=\"\",L=\"\",T=Math.abs(e),K=[\"B\",\"KiB\",\"MiB\",\"GiB\",\"TiB\",\"PiB\",\"EiB\",\"ZiB\",\"YiB\"],G=[\"B\",\"KB\",\"MB\",\"GB\",\"TB\",\"PB\",\"EB\",\"ZB\",\"YB\"],I=\"\",P=!1,R=!1;if(0===e&&null!==l)return l;if(!isFinite(e))return\"\"+e;if(0===n.indexOf(\"{\")){var W=n.indexOf(\"}\");if(-1===W)throw Error('Format should also contain a \"}\"');b=n.slice(1,W),n=n.slice(W+1)}else b=\"\";if(n.indexOf(\"}\")===n.length-1){var Y=n.indexOf(\"{\");if(-1===Y)throw Error('Format should also contain a \"{\"');w=n.slice(Y+1,-1),n=n.slice(0,Y+1)}else w=\"\";if(v=null===($=-1===n.indexOf(\".\")?n.match(/([0-9]+).*/):n.match(/([0-9]+)\\..*/))?-1:$[1].length,-1!==n.indexOf(\"-\")&&(P=!0),n.indexOf(\"(\")>-1?(B=!0,n=n.slice(1,-1)):n.indexOf(\"+\")>-1&&(E=!0,n=n.replace(/\\+/g,\"\")),n.indexOf(\"a\")>-1){if(g=n.split(\".\")[0].match(/[0-9]+/g)||[\"0\"],g=parseInt(g[0],10),U=n.indexOf(\"aK\")>=0,N=n.indexOf(\"aM\")>=0,S=n.indexOf(\"aB\")>=0,j=n.indexOf(\"aT\")>=0,D=U||N||S||j,n.indexOf(\" a\")>-1?(k=\" \",n=n.replace(\" a\",\"\")):n=n.replace(\"a\",\"\"),p=0===(p=(d=Math.floor(Math.log(T)/Math.LN10)+1)%3)?3:p,g&&0!==T&&(h=Math.floor(Math.log(T)/Math.LN10)+1-g,m=3*~~((Math.min(g,d)-p)/3),T/=Math.pow(10,m),-1===n.indexOf(\".\")&&g>3))for(n+=\"[.]\",M=(M=0===h?0:3*~~(h/3)-h)<0?M+3:M,a=0;a=Math.pow(10,12)&&!D||j?(k+=i[o].abbreviations.trillion,e/=Math.pow(10,12)):T=Math.pow(10,9)&&!D||S?(k+=i[o].abbreviations.billion,e/=Math.pow(10,9)):T=Math.pow(10,6)&&!D||N?(k+=i[o].abbreviations.million,e/=Math.pow(10,6)):(T=Math.pow(10,3)&&!D||U)&&(k+=i[o].abbreviations.thousand,e/=Math.pow(10,3)))}if(n.indexOf(\"b\")>-1)for(n.indexOf(\" b\")>-1?(C=\" \",n=n.replace(\" b\",\"\")):n=n.replace(\"b\",\"\"),s=0;s<=K.length;s++)if(u=Math.pow(1024,s),c=Math.pow(1024,s+1),e>=u&&e0&&(e/=u);break}if(n.indexOf(\"d\")>-1)for(n.indexOf(\" d\")>-1?(C=\" \",n=n.replace(\" d\",\"\")):n=n.replace(\"d\",\"\"),s=0;s<=G.length;s++)if(u=Math.pow(1e3,s),c=Math.pow(1e3,s+1),e>=u&&e0&&(e/=u);break}if(n.indexOf(\"o\")>-1&&(n.indexOf(\" o\")>-1?(L=\" \",n=n.replace(\" o\",\"\")):n=n.replace(\"o\",\"\"),i[o].ordinal&&(L+=i[o].ordinal(e))),n.indexOf(\"[.]\")>-1&&(F=!0,n=n.replace(\"[.]\",\".\")),x=e.toString().split(\".\")[0],O=n.split(\".\")[1],y=n.indexOf(\",\"),O){if(x=(I=-1!==O.indexOf(\"*\")?f(e,e.toString().split(\".\")[1].length,t):O.indexOf(\"[\")>-1?f(e,(O=(O=O.replace(\"]\",\"\")).split(\"[\"))[0].length+O[1].length,t,O[1].length):f(e,O.length,t)).split(\".\")[0],I.split(\".\")[1].length)I=(r?k+r:i[o].delimiters.decimal)+I.split(\".\")[1];else I=\"\";F&&0===Number(I.slice(1))&&(I=\"\")}else x=f(e,null,t);return x.indexOf(\"-\")>-1&&(x=x.slice(1),R=!0),x.length-1&&(x=x.toString().replace(/(\\d)(?=(\\d{3})+(?!\\d))/g,\"$1\"+i[o].delimiters.thousands)),0===n.indexOf(\".\")&&(x=\"\"),b+(n.indexOf(\"(\")2)&&(o.length<2?!!o[0].match(/^\\d+.*\\d$/)&&!o[0].match(u):1===o[0].length?!!o[0].match(/^\\d+$/)&&!o[0].match(u)&&!!o[1].match(/^\\d+$/):!!o[0].match(/^\\d+.*\\d$/)&&!o[0].match(u)&&!!o[1].match(/^\\d+$/)))))},n.exports={format:function(e,n,t,i){return null!=t&&t!==r.culture()&&r.setCulture(t),d(Number(e),null!=n?n:u,null==i?Math.round:i)}}},\n function _(e,n,t){!function(){\"use strict\";var e={not_string:/[^s]/,not_bool:/[^t]/,not_type:/[^T]/,not_primitive:/[^v]/,number:/[diefg]/,numeric_arg:/[bcdiefguxX]/,json:/[j]/,not_json:/[^j]/,text:/^[^\\x25]+/,modulo:/^\\x25{2}/,placeholder:/^\\x25(?:([1-9]\\d*)\\$|\\(([^)]+)\\))?(\\+)?(0|'[^$])?(-)?(\\d+)?(?:\\.(\\d+))?([b-gijostTuvxX])/,key:/^([a-z_][a-z_\\d]*)/i,key_access:/^\\.([a-z_][a-z_\\d]*)/i,index_access:/^\\[(\\d+)\\]/,sign:/^[+-]/};function n(e){return i(a(e),arguments)}function r(e,t){return n.apply(null,[e].concat(t||[]))}function i(t,r){var i,s,a,o,p,c,l,u,f,d=1,g=t.length,y=\"\";for(s=0;s=0),o.type){case\"b\":i=parseInt(i,10).toString(2);break;case\"c\":i=String.fromCharCode(parseInt(i,10));break;case\"d\":case\"i\":i=parseInt(i,10);break;case\"j\":i=JSON.stringify(i,null,o.width?parseInt(o.width):0);break;case\"e\":i=o.precision?parseFloat(i).toExponential(o.precision):parseFloat(i).toExponential();break;case\"f\":i=o.precision?parseFloat(i).toFixed(o.precision):parseFloat(i);break;case\"g\":i=o.precision?String(Number(i.toPrecision(o.precision))):parseFloat(i);break;case\"o\":i=(parseInt(i,10)>>>0).toString(8);break;case\"s\":i=String(i),i=o.precision?i.substring(0,o.precision):i;break;case\"t\":i=String(!!i),i=o.precision?i.substring(0,o.precision):i;break;case\"T\":i=Object.prototype.toString.call(i).slice(8,-1).toLowerCase(),i=o.precision?i.substring(0,o.precision):i;break;case\"u\":i=parseInt(i,10)>>>0;break;case\"v\":i=i.valueOf(),i=o.precision?i.substring(0,o.precision):i;break;case\"x\":i=(parseInt(i,10)>>>0).toString(16);break;case\"X\":i=(parseInt(i,10)>>>0).toString(16).toUpperCase()}e.json.test(o.type)?y+=i:(!e.number.test(o.type)||u&&!o.sign?f=\"\":(f=u?\"+\":\"-\",i=i.toString().replace(e.sign,\"\")),c=o.pad_char?\"0\"===o.pad_char?\"0\":o.pad_char.charAt(1):\" \",l=o.width-(f+i).length,p=o.width&&l>0?c.repeat(l):\"\",y+=o.align?f+i+p:\"0\"===c?f+p+i:p+f+i)}return y}var s=Object.create(null);function a(n){if(s[n])return s[n];for(var t,r=n,i=[],a=0;r;){if(null!==(t=e.text.exec(r)))i.push(t[0]);else if(null!==(t=e.modulo.exec(r)))i.push(\"%\");else{if(null===(t=e.placeholder.exec(r)))throw new SyntaxError(\"[sprintf] unexpected placeholder\");if(t[2]){a|=1;var o=[],p=t[2],c=[];if(null===(c=e.key.exec(p)))throw new SyntaxError(\"[sprintf] failed to parse named argument key\");for(o.push(c[1]);\"\"!==(p=p.substring(c[0].length));)if(null!==(c=e.key_access.exec(p)))o.push(c[1]);else{if(null===(c=e.index_access.exec(p)))throw new SyntaxError(\"[sprintf] failed to parse named argument key\");o.push(c[1])}t[2]=o}else a|=2;if(3===a)throw new Error(\"[sprintf] mixing positional and named placeholders is not (yet) supported\");i.push({placeholder:t[0],param_no:t[1],keys:t[2],sign:t[3],pad_char:t[4],align:t[5],width:t[6],precision:t[7],type:t[8]})}r=r.substring(t[0].length)}return s[n]=i}void 0!==t&&(t.sprintf=n,t.vsprintf=r),\"undefined\"!=typeof window&&(window.sprintf=n,window.vsprintf=r,\"function\"==typeof define&&define.amd&&define((function(){return{sprintf:n,vsprintf:r}})))}()},\n function _(e,i,n){Object.defineProperty(n,\"__esModule\",{value:!0});const t=e(9),a=e(127),s=e(191),r=e(192),c=e(195),_=e(196),m=e(194);class k extends s.CompositeTicker{constructor(e){super(e)}static init_DatetimeTicker(){this.override({num_minor_ticks:0,tickers:()=>[new a.AdaptiveTicker({mantissas:[1,2,5],base:10,min_interval:0,max_interval:500*m.ONE_MILLI,num_minor_ticks:0}),new a.AdaptiveTicker({mantissas:[1,2,5,10,15,20,30],base:60,min_interval:m.ONE_SECOND,max_interval:30*m.ONE_MINUTE,num_minor_ticks:0}),new a.AdaptiveTicker({mantissas:[1,2,4,6,8,12],base:24,min_interval:m.ONE_HOUR,max_interval:12*m.ONE_HOUR,num_minor_ticks:0}),new r.DaysTicker({days:t.range(1,32)}),new r.DaysTicker({days:t.range(1,31,3)}),new r.DaysTicker({days:[1,8,15,22]}),new r.DaysTicker({days:[1,15]}),new c.MonthsTicker({months:t.range(0,12,1)}),new c.MonthsTicker({months:t.range(0,12,2)}),new c.MonthsTicker({months:t.range(0,12,4)}),new c.MonthsTicker({months:t.range(0,12,6)}),new _.YearsTicker({})]})}}n.DatetimeTicker=k,k.__name__=\"DatetimeTicker\",k.init_DatetimeTicker()},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const r=t(1),s=t(128),n=r.__importStar(t(18)),_=t(9);class a extends s.ContinuousTicker{constructor(t){super(t)}static init_CompositeTicker(){this.define({tickers:[n.Array,[]]})}get min_intervals(){return this.tickers.map(t=>t.get_min_interval())}get max_intervals(){return this.tickers.map(t=>t.get_max_interval())}get min_interval(){return this.min_intervals[0]}get max_interval(){return this.max_intervals[0]}get_best_ticker(t,e,i){const r=e-t,s=this.get_ideal_interval(t,e,i),n=[_.sorted_index(this.min_intervals,s)-1,_.sorted_index(this.max_intervals,s)],a=[this.min_intervals[n[0]],this.max_intervals[n[1]]].map(t=>Math.abs(i-r/t));let c;if(_.is_empty(a.filter(t=>!isNaN(t))))c=this.tickers[0];else{const t=n[_.argmin(a)];c=this.tickers[t]}return c}get_interval(t,e,i){return this.get_best_ticker(t,e,i).get_interval(t,e,i)}get_ticks_no_defaults(t,e,i,r){return this.get_best_ticker(t,e,r).get_ticks_no_defaults(t,e,i,r)}}i.CompositeTicker=a,a.__name__=\"CompositeTicker\",a.init_CompositeTicker()},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const i=t(1),s=t(193),a=t(194),o=i.__importStar(t(18)),r=t(9);class _ extends s.SingleIntervalTicker{constructor(t){super(t)}static init_DaysTicker(){this.define({days:[o.Array,[]]}),this.override({num_minor_ticks:0})}initialize(){super.initialize();const t=this.days;t.length>1?this.interval=(t[1]-t[0])*a.ONE_DAY:this.interval=31*a.ONE_DAY}get_ticks_no_defaults(t,e,n,i){const s=function(t,e){const n=a.last_month_no_later_than(new Date(t)),i=a.last_month_no_later_than(new Date(e));i.setUTCMonth(i.getUTCMonth()+1);const s=[],o=n;for(;s.push(a.copy_date(o)),o.setUTCMonth(o.getUTCMonth()+1),!(o>i););return s}(t,e),o=this.days,_=this.interval;return{major:r.concat(s.map(t=>((t,e)=>{const n=t.getUTCMonth(),i=[];for(const s of o){const o=a.copy_date(t);o.setUTCDate(s);new Date(o.getTime()+e/2).getUTCMonth()==n&&i.push(o)}return i})(t,_))).map(t=>t.getTime()).filter(n=>t<=n&&n<=e),minor:[]}}}n.DaysTicker=_,_.__name__=\"DaysTicker\",_.init_DaysTicker()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),r=e(128),l=n.__importStar(e(18));class a extends r.ContinuousTicker{constructor(e){super(e)}static init_SingleIntervalTicker(){this.define({interval:[l.Number]})}get_interval(e,t,i){return this.interval}get min_interval(){return this.interval}get max_interval(){return this.interval}}i.SingleIntervalTicker=a,a.__name__=\"SingleIntervalTicker\",a.init_SingleIntervalTicker()},\n function _(t,e,n){function _(t){return new Date(t.getTime())}function O(t){const e=_(t);return e.setUTCDate(1),e.setUTCHours(0),e.setUTCMinutes(0),e.setUTCSeconds(0),e.setUTCMilliseconds(0),e}Object.defineProperty(n,\"__esModule\",{value:!0}),n.ONE_MILLI=1,n.ONE_SECOND=1e3,n.ONE_MINUTE=60*n.ONE_SECOND,n.ONE_HOUR=60*n.ONE_MINUTE,n.ONE_DAY=24*n.ONE_HOUR,n.ONE_MONTH=30*n.ONE_DAY,n.ONE_YEAR=365*n.ONE_DAY,n.copy_date=_,n.last_month_no_later_than=O,n.last_year_no_later_than=function(t){const e=O(t);return e.setUTCMonth(0),e}},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const r=t(1),i=t(193),s=t(194),a=r.__importStar(t(18)),o=t(9);class _ extends i.SingleIntervalTicker{constructor(t){super(t)}static init_MonthsTicker(){this.define({months:[a.Array,[]]})}initialize(){super.initialize();const t=this.months;t.length>1?this.interval=(t[1]-t[0])*s.ONE_MONTH:this.interval=12*s.ONE_MONTH}get_ticks_no_defaults(t,e,n,r){const i=function(t,e){const n=s.last_year_no_later_than(new Date(t)),r=s.last_year_no_later_than(new Date(e));r.setUTCFullYear(r.getUTCFullYear()+1);const i=[],a=n;for(;i.push(s.copy_date(a)),a.setUTCFullYear(a.getUTCFullYear()+1),!(a>r););return i}(t,e),a=this.months;return{major:o.concat(i.map(t=>a.map(e=>{const n=s.copy_date(t);return n.setUTCMonth(e),n}))).map(t=>t.getTime()).filter(n=>t<=n&&n<=e),minor:[]}}}n.MonthsTicker=_,_.__name__=\"MonthsTicker\",_.init_MonthsTicker()},\n function _(e,t,a){Object.defineProperty(a,\"__esModule\",{value:!0});const i=e(126),r=e(193),n=e(194);class _ extends r.SingleIntervalTicker{constructor(e){super(e)}initialize(){super.initialize(),this.interval=n.ONE_YEAR,this.basic_ticker=new i.BasicTicker({num_minor_ticks:0})}get_ticks_no_defaults(e,t,a,i){const r=n.last_year_no_later_than(new Date(e)).getUTCFullYear(),_=n.last_year_no_later_than(new Date(t)).getUTCFullYear();return{major:this.basic_ticker.get_ticks_no_defaults(r,_,a,i).major.map(e=>Date.UTC(e,0,1)).filter(a=>e<=a&&a<=t),minor:[]}}}a.YearsTicker=_,_.__name__=\"YearsTicker\"},\n function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=e(177),o=e(182),n=e(198),r=e(199);class _ extends s.AxisView{}t.LogAxisView=_,_.__name__=\"LogAxisView\";class c extends o.ContinuousAxis{constructor(e){super(e)}static init_LogAxis(){this.prototype.default_view=_,this.override({ticker:()=>new r.LogTicker,formatter:()=>new n.LogTickFormatter})}}t.LogAxis=c,c.__name__=\"LogAxis\",c.init_LogAxis()},\n function _(t,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=t(1),o=t(131),a=t(130),n=i.__importStar(t(18));class c extends o.TickFormatter{constructor(t){super(t)}static init_LogTickFormatter(){this.define({ticker:[n.Instance,null]})}initialize(){super.initialize(),this.basic_formatter=new a.BasicTickFormatter}doFormat(t,e){if(0==t.length)return[];const r=null!=this.ticker?this.ticker.base:10;let i=!1;const o=new Array(t.length);for(let e=0,a=t.length;e0&&o[e]==o[e-1]){i=!0;break}return i?this.basic_formatter.doFormat(t,e):o}}r.LogTickFormatter=c,c.__name__=\"LogTickFormatter\",c.init_LogTickFormatter()},\n function _(t,o,e){Object.defineProperty(e,\"__esModule\",{value:!0});const i=t(127),s=t(9);class n extends i.AdaptiveTicker{constructor(t){super(t)}static init_LogTicker(){this.override({mantissas:[1,5]})}get_ticks_no_defaults(t,o,e,i){const n=this.num_minor_ticks,r=[],c=this.base,a=Math.log(t)/Math.log(c),f=Math.log(o)/Math.log(c),l=f-a;let h;if(isFinite(l))if(l<2){const e=this.get_interval(t,o,i),c=Math.floor(t/e),a=Math.ceil(o/e);if(h=s.range(c,a+1).filter(t=>0!=t).map(t=>t*e).filter(e=>t<=e&&e<=o),n>0&&h.length>0){const t=e/n,o=s.range(0,n).map(o=>o*t);for(const t of o.slice(1))r.push(h[0]-t);for(const t of h)for(const e of o)r.push(t+e)}}else{const t=Math.ceil(.999999*a),o=Math.floor(1.000001*f),e=Math.ceil((o-t)/9);if(h=s.range(t-1,o+1,e).map(t=>c**t),n>0&&h.length>0){const t=c**e/n,o=s.range(1,n+1).map(o=>o*t);for(const t of o)r.push(h[0]/t);r.push(h[0]);for(const t of h)for(const e of o)r.push(t*e)}}else h=[];return{major:h.filter(e=>t<=e&&e<=o),minor:r.filter(e=>t<=e&&e<=o)}}}e.LogTicker=n,n.__name__=\"LogTicker\",n.init_LogTicker()},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=e(177),s=e(184),o=e(201),a=e(202);class c extends i.AxisView{}r.MercatorAxisView=c,c.__name__=\"MercatorAxisView\";class n extends s.LinearAxis{constructor(e){super(e)}static init_MercatorAxis(){this.prototype.default_view=c,this.override({ticker:()=>new a.MercatorTicker({dimension:\"lat\"}),formatter:()=>new o.MercatorTickFormatter({dimension:\"lat\"})})}}r.MercatorAxis=n,n.__name__=\"MercatorAxis\",n.init_MercatorAxis()},\n function _(r,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const o=r(1),n=r(130),i=o.__importStar(r(18)),c=r(37);class a extends n.BasicTickFormatter{constructor(r){super(r)}static init_MercatorTickFormatter(){this.define({dimension:[i.LatLon]})}doFormat(r,t){if(null==this.dimension)throw new Error(\"MercatorTickFormatter.dimension not configured\");if(0==r.length)return[];const e=r.length,o=new Array(e);if(\"lon\"==this.dimension)for(let n=0;n{const n=s.replace_placeholders(this.url,t,e);if(!r.isString(n))throw new Error(\"HTML output is not supported in this context\");this.same_tab?window.location.href=n:window.open(n)},{selected:o}=t;for(const e of o.indices)n(e);for(const e of o.line_indices)n(e)}}n.OpenURL=a,a.__name__=\"OpenURL\",a.init_OpenURL()},\n function _(a,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});var n=a(77);r.Canvas=n.Canvas;var s=a(208);r.CartesianFrame=s.CartesianFrame},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const a=e(209),_=e(146),n=e(157),r=e(158),i=e(210),g=e(98),c=e(212),o=e(13),l=e(11);class h extends c.LayoutItem{constructor(e,t,s,a,_={},n={}){super(),this.in_x_scale=e,this.in_y_scale=t,this.x_range=s,this.y_range=a,this.extra_x_ranges=_,this.extra_y_ranges=n,l.assert(null==e.source_range&&null==e.target_range),l.assert(null==t.source_range&&null==t.target_range),this._configure_scales()}_get_ranges(e,t){return new Map(o.entries(Object.assign(Object.assign({},t),{default:e})))}_get_scales(e,t,s){const c=new Map;for(const[o,l]of t){if((l instanceof i.DataRange1d||l instanceof r.Range1d)&&!(e instanceof _.ContinuousScale))throw new Error(`Range ${l.type} is incompatible is Scale ${e.type}`);if(l instanceof g.FactorRange&&!(e instanceof a.CategoricalScale))throw new Error(`Range ${l.type} is incompatible is Scale ${e.type}`);e instanceof n.LogScale&&l instanceof i.DataRange1d&&(l.scale_hint=\"log\");const t=e.clone();t.setv({source_range:l,target_range:s}),c.set(o,t)}return c}_configure_frame_ranges(){const{bbox:e}=this;this._x_target=new r.Range1d({start:e.left,end:e.right}),this._y_target=new r.Range1d({start:e.bottom,end:e.top})}_configure_scales(){this._configure_frame_ranges(),this._x_ranges=this._get_ranges(this.x_range,this.extra_x_ranges),this._y_ranges=this._get_ranges(this.y_range,this.extra_y_ranges),this._x_scales=this._get_scales(this.in_x_scale,this._x_ranges,this._x_target),this._y_scales=this._get_scales(this.in_y_scale,this._y_ranges,this._y_target)}_update_scales(){this._configure_frame_ranges();for(const[,e]of this._x_scales)e.target_range=this._x_target;for(const[,e]of this._y_scales)e.target_range=this._y_target}_set_geometry(e,t){super._set_geometry(e,t),this._update_scales()}get x_ranges(){return this._x_ranges}get y_ranges(){return this._y_ranges}get x_scales(){return this._x_scales}get y_scales(){return this._y_scales}get x_scale(){return this._x_scales.get(\"default\")}get y_scale(){return this._y_scales.get(\"default\")}get xscales(){return o.to_object(this.x_scales)}get yscales(){return o.to_object(this.y_scales)}}s.CartesianFrame=h,h.__name__=\"CartesianFrame\"},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(147);class _ extends n.Scale{constructor(e){super(e)}compute(e){return super._linear_compute(this.source_range.synthetic(e))}v_compute(e){return super._linear_v_compute(this.source_range.v_synthetic(e))}invert(e){return this._linear_invert(e)}v_invert(e){return this._linear_v_invert(e)}}t.CategoricalScale=_,_.__name__=\"CategoricalScale\"},\n function _(t,i,n){Object.defineProperty(n,\"__esModule\",{value:!0});const e=t(1),a=t(211),s=t(90),l=t(19),_=e.__importStar(t(18)),o=e.__importStar(t(79)),r=t(9);class h extends a.DataRange{constructor(t){super(t),this.have_updated_interactively=!1}static init_DataRange1d(){this.define({start:[_.Number],end:[_.Number],range_padding:[_.Number,.1],range_padding_units:[_.PaddingUnits,\"percent\"],flipped:[_.Boolean,!1],follow:[_.StartEnd],follow_interval:[_.Number],default_span:[_.Number,2],only_visible:[_.Boolean,!1]}),this.internal({scale_hint:[_.String,\"auto\"]})}initialize(){super.initialize(),this._initial_start=this.start,this._initial_end=this.end,this._initial_range_padding=this.range_padding,this._initial_range_padding_units=this.range_padding_units,this._initial_follow=this.follow,this._initial_follow_interval=this.follow_interval,this._initial_default_span=this.default_span,this._plot_bounds=new Map}get min(){return Math.min(this.start,this.end)}get max(){return Math.max(this.start,this.end)}computed_renderers(){const t=this.names;let i=this.renderers;if(0==i.length)for(const t of this.plots){const n=t.renderers.filter(t=>t instanceof s.GlyphRenderer);i=i.concat(n)}t.length>0&&(i=i.filter(i=>r.includes(t,i.name))),l.logger.debug(`computed ${i.length} renderers for ${this}`);for(const t of i)l.logger.trace(\" - \"+t);return i}_compute_plot_bounds(t,i){let n=o.empty();for(const e of t){const t=i.get(e);null==t||!e.visible&&this.only_visible||(n=o.union(n,t))}return n}adjust_bounds_for_aspect(t,i){const n=o.empty();let e=t.x1-t.x0;e<=0&&(e=1);let a=t.y1-t.y0;a<=0&&(a=1);const s=.5*(t.x1+t.x0),l=.5*(t.y1+t.y0);return e_&&(\"start\"==this.follow?a=e+s*_:\"end\"==this.follow&&(e=a-s*_)),[e,a]}update(t,i,n,e){if(this.have_updated_interactively)return;const a=this.computed_renderers();let s=this._compute_plot_bounds(a,t);null!=e&&(s=this.adjust_bounds_for_aspect(s,e)),this._plot_bounds.set(n,s);const[l,_]=this._compute_min_max(this._plot_bounds.values(),i);let[o,r]=this._compute_range(l,_);null!=this._initial_start&&(\"log\"==this.scale_hint?this._initial_start>0&&(o=this._initial_start):o=this._initial_start),null!=this._initial_end&&(\"log\"==this.scale_hint?this._initial_end>0&&(r=this._initial_end):r=this._initial_end);const[h,d]=[this.start,this.end];if(o!=h||r!=d){const t={};o!=h&&(t.start=o),r!=d&&(t.end=r),this.setv(t)}\"auto\"==this.bounds&&this.setv({bounds:[o,r]},{silent:!0}),this.change.emit()}reset(){this.have_updated_interactively=!1,this.setv({range_padding:this._initial_range_padding,range_padding_units:this._initial_range_padding_units,follow:this._initial_follow,follow_interval:this._initial_follow_interval,default_span:this._initial_default_span},{silent:!0}),this.change.emit()}}n.DataRange1d=h,h.__name__=\"DataRange1d\",h.init_DataRange1d()},\n function _(e,a,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1),r=e(99),s=n.__importStar(e(18));class _ extends r.Range{constructor(e){super(e)}static init_DataRange(){this.define({names:[s.Array,[]],renderers:[s.Array,[]]})}}t.DataRange=_,_.__name__=\"DataRange\",_.init_DataRange()},\n function _(a,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});var e=a(213);t.Sizeable=e.Sizeable,t.SizingPolicy=e.SizingPolicy;var i=a(214);t.Layoutable=i.Layoutable,t.LayoutItem=i.LayoutItem;var n=a(215);t.HStack=n.HStack,t.VStack=n.VStack,t.AnchorLayout=n.AnchorLayout;var r=a(216);t.Grid=r.Grid,t.Row=r.Row,t.Column=r.Column;var c=a(217);t.ContentBox=c.ContentBox,t.VariadicBox=c.VariadicBox},\n function _(t,h,i){Object.defineProperty(i,\"__esModule\",{value:!0});const e=t(21),{min:d,max:n}=Math;class w{constructor(t={}){this.width=null!=t.width?t.width:0,this.height=null!=t.height?t.height:0}bounded_to({width:t,height:h}){return new w({width:this.width==1/0&&null!=t?t:this.width,height:this.height==1/0&&null!=h?h:this.height})}expanded_to({width:t,height:h}){return new w({width:t!=1/0?n(this.width,t):this.width,height:h!=1/0?n(this.height,h):this.height})}expand_to({width:t,height:h}){this.width=n(this.width,t),this.height=n(this.height,h)}narrowed_to({width:t,height:h}){return new w({width:d(this.width,t),height:d(this.height,h)})}narrow_to({width:t,height:h}){this.width=d(this.width,t),this.height=d(this.height,h)}grow_by({left:t,right:h,top:i,bottom:e}){const d=this.width+t+h,n=this.height+i+e;return new w({width:d,height:n})}shrink_by({left:t,right:h,top:i,bottom:e}){const d=n(this.width-t-h,0),s=n(this.height-i-e,0);return new w({width:d,height:s})}map(t,h){return new w({width:t(this.width),height:(null!=h?h:t)(this.height)})}}i.Sizeable=w,w.__name__=\"Sizeable\",i.SizingPolicy=e.Enum(\"fixed\",\"fit\",\"min\",\"max\")},\n function _(i,t,h){Object.defineProperty(h,\"__esModule\",{value:!0});const e=i(213),s=i(79),{min:n,max:g,round:a}=Math;class l{constructor(){this._bbox=new s.BBox,this._inner_bbox=new s.BBox}get bbox(){return this._bbox}get inner_bbox(){return this._inner_bbox}get sizing(){return this._sizing}set_sizing(i){const t=i.width_policy||\"fit\",h=i.width,e=null!=i.min_width?i.min_width:0,s=null!=i.max_width?i.max_width:1/0,n=i.height_policy||\"fit\",g=i.height,a=null!=i.min_height?i.min_height:0,l=null!=i.max_height?i.max_height:1/0,_=i.aspect,d=i.margin||{top:0,right:0,bottom:0,left:0},r=!1!==i.visible,w=i.halign||\"start\",o=i.valign||\"start\";this._sizing={width_policy:t,min_width:e,width:h,max_width:s,height_policy:n,min_height:a,height:g,max_height:l,aspect:_,margin:d,visible:r,halign:w,valign:o,size:{width:h,height:g},min_size:{width:e,height:a},max_size:{width:s,height:l}},this._init()}_init(){}_set_geometry(i,t){this._bbox=i,this._inner_bbox=t}set_geometry(i,t){this._set_geometry(i,t||i)}is_width_expanding(){return\"max\"==this.sizing.width_policy}is_height_expanding(){return\"max\"==this.sizing.height_policy}apply_aspect(i,{width:t,height:h}){const{aspect:e}=this.sizing;if(null!=e){const{width_policy:s,height_policy:n}=this.sizing,g=(i,t)=>{const h={max:4,fit:3,min:2,fixed:1};return h[i]>h[t]};if(\"fixed\"!=s&&\"fixed\"!=n)if(s==n){const s=t,n=a(t/e),g=a(h*e),l=h;Math.abs(i.width-s)+Math.abs(i.height-n)<=Math.abs(i.width-g)+Math.abs(i.height-l)?(t=s,h=n):(t=g,h=l)}else g(s,n)?h=a(t/e):t=a(h*e);else\"fixed\"==s?h=a(t/e):\"fixed\"==n&&(t=a(h*e))}return{width:t,height:h}}measure(i){if(!this.sizing.visible)return{width:0,height:0};const t=i=>\"fixed\"==this.sizing.width_policy&&null!=this.sizing.width?this.sizing.width:i,h=i=>\"fixed\"==this.sizing.height_policy&&null!=this.sizing.height?this.sizing.height:i,s=new e.Sizeable(i).shrink_by(this.sizing.margin).map(t,h),n=this._measure(s),g=this.clip_size(n),a=t(g.width),l=h(g.height),_=this.apply_aspect(s,{width:a,height:l});return Object.assign(Object.assign({},n),_)}compute(i={}){const t=this.measure({width:null!=i.width&&this.is_width_expanding()?i.width:1/0,height:null!=i.height&&this.is_height_expanding()?i.height:1/0}),{width:h,height:e}=t,n=new s.BBox({left:0,top:0,width:h,height:e});let g=void 0;if(null!=t.inner){const{left:i,top:n,right:a,bottom:l}=t.inner;g=new s.BBox({left:i,top:n,right:h-a,bottom:e-l})}this.set_geometry(n,g)}get xview(){return this.bbox.xview}get yview(){return this.bbox.yview}clip_width(i){return g(this.sizing.min_width,n(i,this.sizing.max_width))}clip_height(i){return g(this.sizing.min_height,n(i,this.sizing.max_height))}clip_size({width:i,height:t}){return{width:this.clip_width(i),height:this.clip_height(t)}}}h.Layoutable=l,l.__name__=\"Layoutable\";class _ extends l{_measure(i){const{width_policy:t,height_policy:h}=this.sizing;let e,s;if(i.width==1/0)e=null!=this.sizing.width?this.sizing.width:0;else switch(t){case\"fixed\":e=null!=this.sizing.width?this.sizing.width:0;break;case\"min\":e=null!=this.sizing.width?n(i.width,this.sizing.width):0;break;case\"fit\":e=null!=this.sizing.width?n(i.width,this.sizing.width):i.width;break;case\"max\":e=null!=this.sizing.width?g(i.width,this.sizing.width):i.width}if(i.height==1/0)s=null!=this.sizing.height?this.sizing.height:0;else switch(h){case\"fixed\":s=null!=this.sizing.height?this.sizing.height:0;break;case\"min\":s=null!=this.sizing.height?n(i.height,this.sizing.height):0;break;case\"fit\":s=null!=this.sizing.height?n(i.height,this.sizing.height):i.height;break;case\"max\":s=null!=this.sizing.height?g(i.height,this.sizing.height):i.height}return{width:e,height:s}}}h.LayoutItem=_,_.__name__=\"LayoutItem\";class d extends l{_measure(i){const t=this._content_size(),h=i.bounded_to(this.sizing.size).bounded_to(t);return{width:(()=>{switch(this.sizing.width_policy){case\"fixed\":return null!=this.sizing.width?this.sizing.width:t.width;case\"min\":return t.width;case\"fit\":return h.width;case\"max\":return Math.max(t.width,h.width)}})(),height:(()=>{switch(this.sizing.height_policy){case\"fixed\":return null!=this.sizing.height?this.sizing.height:t.height;case\"min\":return t.height;case\"fit\":return h.height;case\"max\":return Math.max(t.height,h.height)}})()}}}h.ContentLayoutable=d,d.__name__=\"ContentLayoutable\"},\n function _(t,e,h){Object.defineProperty(h,\"__esModule\",{value:!0});const o=t(214),r=t(79);class i extends o.Layoutable{constructor(){super(...arguments),this.children=[]}}h.Stack=i,i.__name__=\"Stack\";class s extends i{_measure(t){let e=0,h=0;for(const t of this.children){const o=t.measure({width:0,height:0});e+=o.width,h=Math.max(h,o.height)}return{width:e,height:h}}_set_geometry(t,e){super._set_geometry(t,e);const{top:h,bottom:o}=t;let{left:i}=t;for(const t of this.children){const{width:e}=t.measure({width:0,height:0});t.set_geometry(new r.BBox({left:i,width:e,top:h,bottom:o})),i+=e}}}h.HStack=s,s.__name__=\"HStack\";class n extends i{_measure(t){let e=0,h=0;for(const t of this.children){const o=t.measure({width:0,height:0});e=Math.max(e,o.width),h+=o.height}return{width:e,height:h}}_set_geometry(t,e){super._set_geometry(t,e);const{left:h,right:o}=t;let{top:i}=t;for(const t of this.children){const{height:e}=t.measure({width:0,height:0});t.set_geometry(new r.BBox({top:i,height:e,left:h,right:o})),i+=e}}}h.VStack=n,n.__name__=\"VStack\";class c extends o.Layoutable{constructor(){super(...arguments),this.children=[]}_measure(t){let e=0,h=0;for(const{layout:o}of this.children){const r=o.measure(t);e=Math.max(e,r.width),h=Math.max(h,r.height)}return{width:e,height:h}}_set_geometry(t,e){super._set_geometry(t,e);for(const{layout:e,anchor:h,margin:o}of this.children){const{left:i,right:s,top:n,bottom:c,hcenter:a,vcenter:_}=t,{width:g,height:d}=e.measure(t);let m;switch(h){case\"top_left\":m=new r.BBox({left:i+o,top:n+o,width:g,height:d});break;case\"top_center\":m=new r.BBox({hcenter:a,top:n+o,width:g,height:d});break;case\"top_right\":m=new r.BBox({right:s-o,top:n+o,width:g,height:d});break;case\"bottom_right\":m=new r.BBox({right:s-o,bottom:c-o,width:g,height:d});break;case\"bottom_center\":m=new r.BBox({hcenter:a,bottom:c-o,width:g,height:d});break;case\"bottom_left\":m=new r.BBox({left:i+o,bottom:c-o,width:g,height:d});break;case\"center_left\":m=new r.BBox({left:i+o,vcenter:_,width:g,height:d});break;case\"center\":m=new r.BBox({hcenter:a,vcenter:_,width:g,height:d});break;case\"center_right\":m=new r.BBox({right:s-o,vcenter:_,width:g,height:d})}e.set_geometry(m)}}}h.AnchorLayout=c,c.__name__=\"AnchorLayout\"},\n function _(t,i,s){Object.defineProperty(s,\"__esModule\",{value:!0});const e=t(213),o=t(214),n=t(8),r=t(79),h=t(9),{max:l,round:c}=Math;class a{constructor(t){this.def=t,this._map=new Map}get(t){let i=this._map.get(t);return void 0===i&&(i=this.def(),this._map.set(t,i)),i}apply(t,i){const s=this.get(t);this._map.set(t,i(s))}}a.__name__=\"DefaultMap\";class g{constructor(){this._items=[],this._nrows=0,this._ncols=0}get nrows(){return this._nrows}get ncols(){return this._ncols}add(t,i){const{r1:s,c1:e}=t;this._nrows=l(this._nrows,s+1),this._ncols=l(this._ncols,e+1),this._items.push({span:t,data:i})}at(t,i){return this._items.filter(({span:s})=>s.r0<=t&&t<=s.r1&&s.c0<=i&&i<=s.c1).map(({data:t})=>t)}row(t){return this._items.filter(({span:i})=>i.r0<=t&&t<=i.r1).map(({data:t})=>t)}col(t){return this._items.filter(({span:i})=>i.c0<=t&&t<=i.c1).map(({data:t})=>t)}foreach(t){for(const{span:i,data:s}of this._items)t(i,s)}map(t){const i=new g;for(const{span:s,data:e}of this._items)i.add(s,t(s,e));return i}}g.__name__=\"Container\";class p extends o.Layoutable{constructor(t=[]){super(),this.items=t,this.rows=\"auto\",this.cols=\"auto\",this.spacing=0,this.absolute=!1}is_width_expanding(){if(super.is_width_expanding())return!0;if(\"fixed\"==this.sizing.width_policy)return!1;const{cols:t}=this._state;return h.some(t,t=>\"max\"==t.policy)}is_height_expanding(){if(super.is_height_expanding())return!0;if(\"fixed\"==this.sizing.height_policy)return!1;const{rows:t}=this._state;return h.some(t,t=>\"max\"==t.policy)}_init(){super._init();const t=new g;for(const{layout:i,row:s,col:e,row_span:o,col_span:n}of this.items)if(i.sizing.visible){const r=s,h=e,l=s+(null!=o?o:1)-1,c=e+(null!=n?n:1)-1;t.add({r0:r,c0:h,r1:l,c1:c},i)}const{nrows:i,ncols:s}=t,e=new Array(i);for(let s=0;s{const t=n.isPlainObject(this.rows)?this.rows[s]||this.rows[\"*\"]:this.rows;return null==t?{policy:\"auto\"}:n.isNumber(t)?{policy:\"fixed\",height:t}:n.isString(t)?{policy:t}:t})(),o=i.align||\"auto\";if(\"fixed\"==i.policy)e[s]={policy:\"fixed\",height:i.height,align:o};else if(\"min\"==i.policy)e[s]={policy:\"min\",align:o};else if(\"fit\"==i.policy||\"max\"==i.policy)e[s]={policy:i.policy,flex:i.flex||1,align:o};else{if(\"auto\"!=i.policy)throw new Error(\"unrechable\");h.some(t.row(s),t=>t.is_height_expanding())?e[s]={policy:\"max\",flex:1,align:o}:e[s]={policy:\"min\",align:o}}}const o=new Array(s);for(let i=0;i{const t=n.isPlainObject(this.cols)?this.cols[i]||this.cols[\"*\"]:this.cols;return null==t?{policy:\"auto\"}:n.isNumber(t)?{policy:\"fixed\",width:t}:n.isString(t)?{policy:t}:t})(),e=s.align||\"auto\";if(\"fixed\"==s.policy)o[i]={policy:\"fixed\",width:s.width,align:e};else if(\"min\"==s.policy)o[i]={policy:\"min\",align:e};else if(\"fit\"==s.policy||\"max\"==s.policy)o[i]={policy:s.policy,flex:s.flex||1,align:e};else{if(\"auto\"!=s.policy)throw new Error(\"unrechable\");h.some(t.col(i),t=>t.is_width_expanding())?o[i]={policy:\"max\",flex:1,align:e}:o[i]={policy:\"min\",align:e}}}const[r,l]=n.isNumber(this.spacing)?[this.spacing,this.spacing]:this.spacing;this._state={items:t,nrows:i,ncols:s,rows:e,cols:o,rspacing:r,cspacing:l}}_measure_totals(t,i){const{nrows:s,ncols:e,rspacing:o,cspacing:n}=this._state;return{height:h.sum(t)+(s-1)*o,width:h.sum(i)+(e-1)*n}}_measure_cells(t){const{items:i,nrows:s,ncols:o,rows:n,cols:r,rspacing:h,cspacing:a}=this._state,p=new Array(s);for(let t=0;t{const{r0:o,c0:g,r1:d,c1:w}=i,u=(d-o)*h,m=(w-g)*a;let y=0;for(let i=o;i<=d;i++)y+=t(i,g).height;y+=u;let x=0;for(let i=g;i<=w;i++)x+=t(o,i).width;x+=m;const b=s.measure({width:x,height:y});f.add(i,{layout:s,size_hint:b});const z=new e.Sizeable(b).grow_by(s.sizing.margin);z.height-=u,z.width-=m;const j=[];for(let t=o;t<=d;t++){const i=n[t];\"fixed\"==i.policy?z.height-=i.height:j.push(t)}if(z.height>0){const t=c(z.height/j.length);for(const i of j)p[i]=l(p[i],t)}const O=[];for(let t=g;t<=w;t++){const i=r[t];\"fixed\"==i.policy?z.width-=i.width:O.push(t)}if(z.width>0){const t=c(z.width/O.length);for(const i of O)_[i]=l(_[i],t)}});return{size:this._measure_totals(p,_),row_heights:p,col_widths:_,size_hints:f}}_measure_grid(t){const{nrows:i,ncols:s,rows:e,cols:o,rspacing:n,cspacing:r}=this._state,h=this._measure_cells((t,i)=>{const s=e[t],n=o[i];return{width:\"fixed\"==n.policy?n.width:1/0,height:\"fixed\"==s.policy?s.height:1/0}});let a;a=\"fixed\"==this.sizing.height_policy&&null!=this.sizing.height?this.sizing.height:t.height!=1/0&&this.is_height_expanding()?t.height:h.size.height;let g,p=0;for(let t=0;t0)for(let t=0;ti?i:e,t--}}}g=\"fixed\"==this.sizing.width_policy&&null!=this.sizing.width?this.sizing.width:t.width!=1/0&&this.is_width_expanding()?t.width:h.size.width;let _=0;for(let t=0;t0)for(let t=0;ts?s:o,t--}}}const{row_heights:f,col_widths:d,size_hints:w}=this._measure_cells((t,i)=>({width:h.col_widths[i],height:h.row_heights[t]}));return{size:this._measure_totals(f,d),row_heights:f,col_widths:d,size_hints:w}}_measure(t){const{size:i}=this._measure_grid(t);return i}_set_geometry(t,i){super._set_geometry(t,i);const{nrows:s,ncols:e,rspacing:o,cspacing:n}=this._state,{row_heights:h,col_widths:g,size_hints:p}=this._measure_grid(t),_=this._state.rows.map((t,i)=>Object.assign(Object.assign({},t),{top:0,height:h[i],get bottom(){return this.top+this.height}})),f=this._state.cols.map((t,i)=>Object.assign(Object.assign({},t),{left:0,width:g[i],get right(){return this.left+this.width}})),d=p.map((t,i)=>Object.assign(Object.assign({},i),{outer:new r.BBox,inner:new r.BBox}));for(let i=0,e=this.absolute?t.top:0;i{const{layout:l,size_hint:a}=h,{sizing:g}=l,{width:p,height:d}=a,w=function(t,i){let s=(i-t)*n;for(let e=t;e<=i;e++)s+=f[e].width;return s}(i,e),u=function(t,i){let s=(i-t)*o;for(let e=t;e<=i;e++)s+=_[e].height;return s}(t,s),m=i==e&&\"auto\"!=f[i].align?f[i].align:g.halign,y=t==s&&\"auto\"!=_[t].align?_[t].align:g.valign;let x=f[i].left;\"start\"==m?x+=g.margin.left:\"center\"==m?x+=c((w-p)/2):\"end\"==m&&(x+=w-g.margin.right-p);let b=_[t].top;\"start\"==y?b+=g.margin.top:\"center\"==y?b+=c((u-d)/2):\"end\"==y&&(b+=u-g.margin.bottom-d),h.outer=new r.BBox({left:x,top:b,width:p,height:d})});const w=_.map(()=>({start:new a(()=>0),end:new a(()=>0)})),u=f.map(()=>({start:new a(()=>0),end:new a(()=>0)}));d.foreach(({r0:t,c0:i,r1:s,c1:e},{size_hint:o,outer:n})=>{const{inner:r}=o;null!=r&&(w[t].start.apply(n.top,t=>l(t,r.top)),w[s].end.apply(_[s].bottom-n.bottom,t=>l(t,r.bottom)),u[i].start.apply(n.left,t=>l(t,r.left)),u[e].end.apply(f[e].right-n.right,t=>l(t,r.right)))}),d.foreach(({r0:t,c0:i,r1:s,c1:e},o)=>{const{size_hint:n,outer:h}=o;function l({left:t,right:i,top:s,bottom:e}){const o=h.width-t-i,n=h.height-s-e;return new r.BBox({left:t,top:s,width:o,height:n})}if(null!=n.inner){let r=l(n.inner);if(!1!==n.align){const o=w[t].start.get(h.top),n=w[s].end.get(_[s].bottom-h.bottom),c=u[i].start.get(h.left),a=u[e].end.get(f[e].right-h.right);try{r=l({top:o,bottom:n,left:c,right:a})}catch(t){}}o.inner=r}else o.inner=h}),d.foreach((t,{layout:i,outer:s,inner:e})=>{i.set_geometry(s,e)})}}s.Grid=p,p.__name__=\"Grid\";class _ extends p{constructor(t){super(),this.items=t.map((t,i)=>({layout:t,row:0,col:i})),this.rows=\"fit\"}}s.Row=_,_.__name__=\"Row\";class f extends p{constructor(t){super(),this.items=t.map((t,i)=>({layout:t,row:i,col:0})),this.cols=\"fit\"}}s.Column=f,f.__name__=\"Column\"},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=e(214),i=e(213),a=e(72);class c extends n.ContentLayoutable{constructor(e){super(),this.content_size=a.unsized(e,()=>new i.Sizeable(a.size(e)))}_content_size(){return this.content_size}}s.ContentBox=c,c.__name__=\"ContentBox\";class o extends n.Layoutable{constructor(e){super(),this.el=e}_measure(e){const t=new i.Sizeable(e).bounded_to(this.sizing.size);return a.sized(this.el,t,()=>{const e=new i.Sizeable(a.content_size(this.el)),{border:t,padding:s}=a.extents(this.el);return e.grow_by(t).grow_by(s).map(Math.ceil)})}}s.VariadicBox=o,o.__name__=\"VariadicBox\";class r extends o{constructor(e){super(e),this._cache=new Map}_measure(e){const{width:t,height:s}=e,n=`${t},${s}`;let i=this._cache.get(n);return null==i&&(i=super._measure(e),this._cache.set(n,i)),i}invalidate_cache(){this._cache.clear()}}s.CachedVariadicBox=r,r.__name__=\"CachedVariadicBox\"},\n function _(e,r,u){Object.defineProperty(u,\"__esModule\",{value:!0});var a=e(219);u.Expression=a.Expression;var n=e(220);u.Stack=n.Stack;var o=e(221);u.CumSum=o.CumSum},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=e(81);class i extends n.Model{constructor(e){super(e)}initialize(){super.initialize(),this._connected=new Set,this._result=new Map}v_compute(e){this._connected.has(e)||(this.connect(e.change,()=>this._result.delete(e)),this.connect(e.patching,()=>this._result.delete(e)),this.connect(e.streaming,()=>this._result.delete(e)),this._connected.add(e));let t=this._result.get(e);return null==t&&(t=this._v_compute(e),this._result.set(e,t)),t}}s.Expression=i,i.__name__=\"Expression\"},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const r=t(1),i=t(219),s=t(24),o=r.__importStar(t(18));class a extends i.Expression{constructor(t){super(t)}static init_Stack(){this.define({fields:[o.Array,[]]})}_v_compute(t){var e;const n=null!==(e=t.get_length())&&void 0!==e?e:0,r=new s.NumberArray(n);for(const e of this.fields){const i=t.data[e];if(null!=i)for(let t=0,e=Math.min(n,i.length);tn(t,e,r,...this.values))}}n.FuncTickFormatter=u,u.__name__=\"FuncTickFormatter\",u.init_FuncTickFormatter()},\n function _(r,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const e=r(1),o=e.__importStar(r(188)),a=r(131),i=e.__importStar(r(18));class u extends a.TickFormatter{constructor(r){super(r)}static init_NumeralTickFormatter(){this.define({format:[i.String,\"0,0\"],language:[i.String,\"en\"],rounding:[i.RoundingFunction,\"round\"]})}get _rounding_fn(){switch(this.rounding){case\"round\":case\"nearest\":return Math.round;case\"floor\":case\"rounddown\":return Math.floor;case\"ceil\":case\"roundup\":return Math.ceil}}doFormat(r,t){const{format:n,language:e,_rounding_fn:a}=this;return r.map(r=>o.format(r,n,e,a))}}n.NumeralTickFormatter=u,u.__name__=\"NumeralTickFormatter\",u.init_NumeralTickFormatter()},\n function _(t,r,i){Object.defineProperty(i,\"__esModule\",{value:!0});const e=t(1),n=t(131),o=t(187),a=e.__importStar(t(18));class c extends n.TickFormatter{constructor(t){super(t)}static init_PrintfTickFormatter(){this.define({format:[a.String,\"%s\"]})}doFormat(t,r){return t.map(t=>o.sprintf(this.format,t))}}i.PrintfTickFormatter=c,c.__name__=\"PrintfTickFormatter\",c.init_PrintfTickFormatter()},\n function _(a,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});var v=a(233);r.AnnularWedge=v.AnnularWedge;var l=a(234);r.Annulus=l.Annulus;var t=a(235);r.Arc=t.Arc;var i=a(236);r.Bezier=i.Bezier;var n=a(237);r.Circle=n.Circle;var u=a(241);r.CenterRotatable=u.CenterRotatable;var c=a(242);r.Ellipse=c.Ellipse;var g=a(243);r.EllipseOval=g.EllipseOval;var A=a(94);r.Glyph=A.Glyph;var p=a(111);r.HArea=p.HArea;var s=a(244);r.HBar=s.HBar;var d=a(246);r.HexTile=d.HexTile;var R=a(247);r.Image=R.Image;var o=a(249);r.ImageRGBA=o.ImageRGBA;var y=a(250);r.ImageURL=y.ImageURL;var h=a(92);r.Line=h.Line;var m=a(252);r.MultiLine=m.MultiLine;var B=a(253);r.MultiPolygons=B.MultiPolygons;var P=a(254);r.Oval=P.Oval;var G=a(110);r.Patch=G.Patch;var H=a(255);r.Patches=H.Patches;var I=a(256);r.Quad=I.Quad;var L=a(257);r.Quadratic=L.Quadratic;var M=a(258);r.Ray=M.Ray;var O=a(259);r.Rect=O.Rect;var x=a(260);r.Segment=x.Segment;var C=a(261);r.Step=C.Step;var E=a(262);r.Text=E.Text;var Q=a(113);r.VArea=Q.VArea;var S=a(263);r.VBar=S.VBar;var T=a(264);r.Wedge=T.Wedge;var V=a(93);r.XYGlyph=V.XYGlyph},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),r=e(93),n=e(100),a=e(28),_=e(24),o=i.__importStar(e(18)),d=e(10),h=e(88);class u extends r.XYGlyphView{_map_data(){\"data\"==this.model.properties.inner_radius.units?this.sinner_radius=this.sdist(this.renderer.xscale,this._x,this._inner_radius):this.sinner_radius=this._inner_radius,\"data\"==this.model.properties.outer_radius.units?this.souter_radius=this.sdist(this.renderer.xscale,this._x,this._outer_radius):this.souter_radius=this._outer_radius,this._angle=new _.NumberArray(this._start_angle.length);for(let e=0,t=this._start_angle.length;e=s&&u.push(e)}const l=this.model.properties.direction.value(),c=[];for(const e of u){const i=Math.atan2(s-this.sy[e],t-this.sx[e]);d.angle_between(-i,-this._start_angle[e],-this._end_angle[e],l)&&c.push(e)}return new h.Selection({indices:c})}draw_legend_for_index(e,t,s){n.generic_area_legend(this.visuals,e,t,s)}scenterxy(e){const t=(this.sinner_radius[e]+this.souter_radius[e])/2,s=(this._start_angle[e]+this._end_angle[e])/2;return[this.sx[e]+t*Math.cos(s),this.sy[e]+t*Math.sin(s)]}}s.AnnularWedgeView=u,u.__name__=\"AnnularWedgeView\";class l extends r.XYGlyph{constructor(e){super(e)}static init_AnnularWedge(){this.prototype.default_view=u,this.mixins([a.LineVector,a.FillVector]),this.define({direction:[o.Direction,\"anticlock\"],inner_radius:[o.DistanceSpec],outer_radius:[o.DistanceSpec],start_angle:[o.AngleSpec],end_angle:[o.AngleSpec]})}}s.AnnularWedge=l,l.__name__=\"AnnularWedge\",l.init_AnnularWedge()},\n function _(s,i,e){Object.defineProperty(e,\"__esModule\",{value:!0});const t=s(1),r=s(93),n=s(28),a=t.__importStar(s(18)),_=s(32),u=s(88);class o extends r.XYGlyphView{_map_data(){\"data\"==this.model.properties.inner_radius.units?this.sinner_radius=this.sdist(this.renderer.xscale,this._x,this._inner_radius):this.sinner_radius=this._inner_radius,\"data\"==this.model.properties.outer_radius.units?this.souter_radius=this.sdist(this.renderer.xscale,this._x,this._outer_radius):this.souter_radius=this._outer_radius}_render(s,i,{sx:e,sy:t,sinner_radius:r,souter_radius:n}){for(const a of i)if(!isNaN(e[a]+t[a]+r[a]+n[a])){if(this.visuals.fill.doit){if(this.visuals.fill.set_vectorize(s,a),s.beginPath(),_.is_ie)for(const i of[!1,!0])s.arc(e[a],t[a],r[a],0,Math.PI,i),s.arc(e[a],t[a],n[a],Math.PI,0,!i);else s.arc(e[a],t[a],r[a],0,2*Math.PI,!0),s.arc(e[a],t[a],n[a],2*Math.PI,0,!1);s.fill()}this.visuals.line.doit&&(this.visuals.line.set_vectorize(s,a),s.beginPath(),s.arc(e[a],t[a],r[a],0,2*Math.PI),s.moveTo(e[a]+n[a],t[a]),s.arc(e[a],t[a],n[a],0,2*Math.PI),s.stroke())}}_hit_point(s){const{sx:i,sy:e}=s,t=this.renderer.xscale.invert(i),r=this.renderer.yscale.invert(e);let n,a,_,o;if(\"data\"==this.model.properties.outer_radius.units)n=t-this.max_outer_radius,_=t+this.max_outer_radius,a=r-this.max_outer_radius,o=r+this.max_outer_radius;else{const s=i-this.max_outer_radius,t=i+this.max_outer_radius;[n,_]=this.renderer.xscale.r_invert(s,t);const r=e-this.max_outer_radius,u=e+this.max_outer_radius;[a,o]=this.renderer.yscale.r_invert(r,u)}const d=[];for(const s of this.index.indices({x0:n,x1:_,y0:a,y1:o})){const i=this.souter_radius[s]**2,e=this.sinner_radius[s]**2,[n,a]=this.renderer.xscale.r_compute(t,this._x[s]),[_,u]=this.renderer.yscale.r_compute(r,this._y[s]),o=(n-a)**2+(_-u)**2;o<=i&&o>=e&&d.push(s)}return new u.Selection({indices:d})}draw_legend_for_index(s,{x0:i,y0:e,x1:t,y1:r},n){const a=n+1,_=new Array(a);_[n]=(i+t)/2;const u=new Array(a);u[n]=(e+r)/2;const o=.5*Math.min(Math.abs(t-i),Math.abs(r-e)),d=new Array(a);d[n]=.4*o;const h=new Array(a);h[n]=.8*o,this._render(s,[n],{sx:_,sy:u,sinner_radius:d,souter_radius:h})}}e.AnnulusView=o,o.__name__=\"AnnulusView\";class d extends r.XYGlyph{constructor(s){super(s)}static init_Annulus(){this.prototype.default_view=o,this.mixins([n.LineVector,n.FillVector]),this.define({inner_radius:[a.DistanceSpec],outer_radius:[a.DistanceSpec]})}}e.Annulus=d,d.__name__=\"Annulus\",d.init_Annulus()},\n function _(e,i,s){Object.defineProperty(s,\"__esModule\",{value:!0});const t=e(1),r=e(93),n=e(100),a=e(28),_=t.__importStar(e(18));class c extends r.XYGlyphView{_map_data(){\"data\"==this.model.properties.radius.units?this.sradius=this.sdist(this.renderer.xscale,this._x,this._radius):this.sradius=this._radius}_render(e,i,{sx:s,sy:t,sradius:r,_start_angle:n,_end_angle:a}){if(this.visuals.line.doit){const _=this.model.properties.direction.value();for(const c of i)isNaN(s[c]+t[c]+r[c]+n[c]+a[c])||(e.beginPath(),e.arc(s[c],t[c],r[c],n[c],a[c],_),this.visuals.line.set_vectorize(e,c),e.stroke())}}draw_legend_for_index(e,i,s){n.generic_line_legend(this.visuals,e,i,s)}}s.ArcView=c,c.__name__=\"ArcView\";class d extends r.XYGlyph{constructor(e){super(e)}static init_Arc(){this.prototype.default_view=c,this.mixins(a.LineVector),this.define({direction:[_.Direction,\"anticlock\"],radius:[_.DistanceSpec],start_angle:[_.AngleSpec],end_angle:[_.AngleSpec]})}}s.Arc=d,d.__name__=\"Arc\",d.init_Arc()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),n=e(28),c=e(94),o=e(100),_=e(37),r=s.__importStar(e(18));function a(e,t,i,s,n,c,o,_){const r=[],a=[[],[]];for(let a=0;a<=2;a++){let h,d,x;if(0===a?(d=6*e-12*i+6*n,h=-3*e+9*i-9*n+3*o,x=3*i-3*e):(d=6*t-12*s+6*c,h=-3*t+9*s-9*c+3*_,x=3*s-3*t),Math.abs(h)<1e-12){if(Math.abs(d)<1e-12)continue;const e=-x/d;0Math.max(s,i[e]));break}case\"min\":{const s=this.sdist(this.renderer.xscale,this._x,this._radius),i=this.sdist(this.renderer.yscale,this._y,this._radius);this.sradius=_.map(s,(s,e)=>Math.min(s,i[e]));break}}else this.sradius=this._radius,this.max_size=2*this.max_radius;else this.sradius=_.map(this._size,s=>s/2)}_mask_data(){const[s,i]=this.renderer.plot_view.frame.bbox.ranges;let e,t,r,a;if(null!=this._radius&&\"data\"==this.model.properties.radius.units){const n=s.start,h=s.end;[e,r]=this.renderer.xscale.r_invert(n,h),e-=this.max_radius,r+=this.max_radius;const d=i.start,l=i.end;[t,a]=this.renderer.yscale.r_invert(d,l),t-=this.max_radius,a+=this.max_radius}else{const n=s.start-this.max_size,h=s.end+this.max_size;[e,r]=this.renderer.xscale.r_invert(n,h);const d=i.start-this.max_size,l=i.end+this.max_size;[t,a]=this.renderer.yscale.r_invert(d,l)}return this.index.indices({x0:e,x1:r,y0:t,y1:a})}_render(s,i,{sx:e,sy:t,sradius:r}){for(const a of i)isNaN(e[a]+t[a]+r[a])||(s.beginPath(),s.arc(e[a],t[a],r[a],0,2*Math.PI,!1),this.visuals.fill.doit&&(this.visuals.fill.set_vectorize(s,a),s.fill()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(s,a),s.stroke()))}_hit_point(s){const{sx:i,sy:e}=s,t=this.renderer.xscale.invert(i),r=this.renderer.yscale.invert(e);let a,n,h,d;if(null!=this._radius&&\"data\"==this.model.properties.radius.units)a=t-this.max_radius,n=t+this.max_radius,h=r-this.max_radius,d=r+this.max_radius;else{const s=i-this.max_size,t=i+this.max_size;[a,n]=this.renderer.xscale.r_invert(s,t);const r=e-this.max_size,l=e+this.max_size;[h,d]=this.renderer.yscale.r_invert(r,l)}const l=this.index.indices({x0:a,x1:n,y0:h,y1:d}),_=[];if(null!=this._radius&&\"data\"==this.model.properties.radius.units)for(const s of l){const i=this.sradius[s]**2,[e,a]=this.renderer.xscale.r_compute(t,this._x[s]),[n,h]=this.renderer.yscale.r_compute(r,this._y[s]);(e-a)**2+(n-h)**2<=i&&_.push(s)}else for(const s of l){const t=this.sradius[s]**2;(this.sx[s]-i)**2+(this.sy[s]-e)**2<=t&&_.push(s)}return new c.Selection({indices:_})}_hit_span(s){const{sx:i,sy:e}=s,t=this.bounds();let r,a,n,h;if(\"h\"==s.direction){let s,e;if(n=t.y0,h=t.y1,null!=this._radius&&\"data\"==this.model.properties.radius.units)s=i-this.max_radius,e=i+this.max_radius,[r,a]=this.renderer.xscale.r_invert(s,e);else{const t=this.max_size/2;s=i-t,e=i+t,[r,a]=this.renderer.xscale.r_invert(s,e)}}else{let s,i;if(r=t.x0,a=t.x1,null!=this._radius&&\"data\"==this.model.properties.radius.units)s=e-this.max_radius,i=e+this.max_radius,[n,h]=this.renderer.yscale.r_invert(s,i);else{const t=this.max_size/2;s=e-t,i=e+t,[n,h]=this.renderer.yscale.r_invert(s,i)}}const d=[...this.index.indices({x0:r,x1:a,y0:n,y1:h})];return new c.Selection({indices:d})}_hit_rect(s){const{sx0:i,sx1:e,sy0:t,sy1:r}=s,[a,n]=this.renderer.xscale.r_invert(i,e),[h,d]=this.renderer.yscale.r_invert(t,r),l=[...this.index.indices({x0:a,x1:n,y0:h,y1:d})];return new c.Selection({indices:l})}_hit_poly(s){const{sx:i,sy:e}=s,t=l.range(0,this.sx.length),r=[];for(let s=0,a=t.length;s2*t)),i.data_changed=!1),this.visuals_changed&&(this._set_visuals(a),this.visuals_changed=!1),this.prog.set_uniform(\"u_pixel_ratio\",\"float\",[s.pixel_ratio]),this.prog.set_uniform(\"u_canvas_size\",\"vec2\",[s.width,s.height]),this.prog.set_attribute(\"a_sx\",\"float\",i.vbo_sx),this.prog.set_attribute(\"a_sy\",\"float\",i.vbo_sy),this.prog.set_attribute(\"a_size\",\"float\",i.vbo_s),this.prog.set_attribute(\"a_angle\",\"float\",i.vbo_a),0!=t.length)if(t.length===a)this.prog.draw(this.gl.POINTS,[0,a]);else if(a<65535){const e=window.navigator.userAgent;e.indexOf(\"MSIE \")+e.indexOf(\"Trident/\")+e.indexOf(\"Edge/\")>0&&n.logger.warn(\"WebGL warning: IE is known to produce 1px sprites whith selections.\"),this.index_buffer.set_size(2*t.length),this.index_buffer.set_data(0,new Uint16Array(t)),this.prog.draw(this.gl.POINTS,this.index_buffer)}else{const e=64e3,s=[];for(let t=0,i=Math.ceil(a/e);t2*t)):this.vbo_s.set_data(0,new Float32Array(this.glyph._size))}_set_visuals(t){u(this.prog,this.vbo_linewidth,\"a_linewidth\",t,this.glyph.visuals.line,\"line_width\"),f(this.prog,this.vbo_fg_color,\"a_fg_color\",t,this.glyph.visuals.line,\"line\"),f(this.prog,this.vbo_bg_color,\"a_bg_color\",t,this.glyph.visuals.fill,\"fill\"),this.prog.set_uniform(\"u_antialias\",\"float\",[.8])}}function b(t){return class extends d{get _marker_code(){return t}}}s.MarkerGL=d,d.__name__=\"MarkerGL\";const c=i.__importStar(t(240));s.AsteriskGL=b(c.asterisk),s.CircleGL=b(c.circle),s.CircleCrossGL=b(c.circlecross),s.CircleXGL=b(c.circlex),s.CrossGL=b(c.cross),s.DiamondGL=b(c.diamond),s.DiamondCrossGL=b(c.diamondcross),s.HexGL=b(c.hex),s.InvertedTriangleGL=b(c.invertedtriangle),s.SquareGL=b(c.square),s.SquareCrossGL=b(c.squarecross),s.SquareXGL=b(c.squarex),s.TriangleGL=b(c.triangle),s.XGL=b(c.x)},\n function _(n,i,a){Object.defineProperty(a,\"__esModule\",{value:!0}),a.vertex_shader=\"\\nprecision mediump float;\\nconst float SQRT_2 = 1.4142135623730951;\\n//\\nuniform float u_pixel_ratio;\\nuniform vec2 u_canvas_size;\\nuniform vec2 u_offset;\\nuniform vec2 u_scale;\\nuniform float u_antialias;\\n//\\nattribute float a_sx;\\nattribute float a_sy;\\nattribute float a_size;\\nattribute float a_angle; // in radians\\nattribute float a_linewidth;\\nattribute vec4 a_fg_color;\\nattribute vec4 a_bg_color;\\n//\\nvarying float v_linewidth;\\nvarying float v_size;\\nvarying vec4 v_fg_color;\\nvarying vec4 v_bg_color;\\nvarying vec2 v_rotation;\\n\\nvoid main (void)\\n{\\n v_size = a_size * u_pixel_ratio;\\n v_linewidth = a_linewidth * u_pixel_ratio;\\n v_fg_color = a_fg_color;\\n v_bg_color = a_bg_color;\\n v_rotation = vec2(cos(-a_angle), sin(-a_angle));\\n vec2 pos = vec2(a_sx, a_sy); // in pixels\\n pos += 0.5; // make up for Bokeh's offset\\n pos /= u_canvas_size / u_pixel_ratio; // in 0..1\\n gl_Position = vec4(pos*2.0-1.0, 0.0, 1.0);\\n gl_Position.y *= -1.0;\\n gl_PointSize = SQRT_2 * v_size + 2.0 * (v_linewidth + 1.5*u_antialias);\\n}\\n\"},\n function _(a,n,s){Object.defineProperty(s,\"__esModule\",{value:!0}),s.fragment_shader=a=>`\\nprecision mediump float;\\nconst float SQRT_2 = 1.4142135623730951;\\nconst float PI = 3.14159265358979323846264;\\n//\\nuniform float u_antialias;\\n//\\nvarying vec4 v_fg_color;\\nvarying vec4 v_bg_color;\\nvarying float v_linewidth;\\nvarying float v_size;\\nvarying vec2 v_rotation;\\n\\n${a}\\n\\nvec4 outline(float distance, float linewidth, float antialias, vec4 fg_color, vec4 bg_color)\\n{\\n vec4 frag_color;\\n float t = linewidth/2.0 - antialias;\\n float signed_distance = distance;\\n float border_distance = abs(signed_distance) - t;\\n float alpha = border_distance/antialias;\\n alpha = exp(-alpha*alpha);\\n\\n // If fg alpha is zero, it probably means no outline. To avoid a dark outline\\n // shining through due to aa, we set the fg color to the bg color. Avoid if (i.e. branching).\\n float select = float(bool(fg_color.a));\\n fg_color.rgb = select * fg_color.rgb + (1.0 - select) * bg_color.rgb;\\n // Similarly, if we want a transparent bg\\n select = float(bool(bg_color.a));\\n bg_color.rgb = select * bg_color.rgb + (1.0 - select) * fg_color.rgb;\\n\\n if( border_distance < 0.0)\\n frag_color = fg_color;\\n else if( signed_distance < 0.0 ) {\\n frag_color = mix(bg_color, fg_color, sqrt(alpha));\\n } else {\\n if( abs(signed_distance) < (linewidth/2.0 + antialias) ) {\\n frag_color = vec4(fg_color.rgb, fg_color.a * alpha);\\n } else {\\n discard;\\n }\\n }\\n return frag_color;\\n}\\n\\nvoid main()\\n{\\n vec2 P = gl_PointCoord.xy - vec2(0.5, 0.5);\\n P = vec2(v_rotation.x*P.x - v_rotation.y*P.y,\\n v_rotation.y*P.x + v_rotation.x*P.y);\\n float point_size = SQRT_2*v_size + 2.0 * (v_linewidth + 1.5*u_antialias);\\n float distance = marker(P*point_size, v_size);\\n gl_FragColor = outline(distance, v_linewidth, u_antialias, v_fg_color, v_bg_color);\\n}\\n`,s.circle=\"\\nfloat marker(vec2 P, float size)\\n{\\n return length(P) - size/2.0;\\n}\\n\",s.square=\"\\nfloat marker(vec2 P, float size)\\n{\\n return max(abs(P.x), abs(P.y)) - size/2.0;\\n}\\n\",s.diamond=\"\\nfloat marker(vec2 P, float size)\\n{\\n float x = SQRT_2 / 2.0 * (P.x * 1.5 - P.y);\\n float y = SQRT_2 / 2.0 * (P.x * 1.5 + P.y);\\n float r1 = max(abs(x), abs(y)) - size / (2.0 * SQRT_2);\\n return r1 / SQRT_2;\\n}\\n\",s.hex=\"\\nfloat marker(vec2 P, float size)\\n{\\n vec2 q = abs(P);\\n return max(q.y * 0.57735 + q.x - 1.0 * size/2.0, q.y - 0.866 * size/2.0);\\n}\\n\",s.triangle=\"\\nfloat marker(vec2 P, float size)\\n{\\n P.y -= size * 0.3;\\n float x = SQRT_2 / 2.0 * (P.x * 1.7 - P.y);\\n float y = SQRT_2 / 2.0 * (P.x * 1.7 + P.y);\\n float r1 = max(abs(x), abs(y)) - size / 1.6;\\n float r2 = P.y;\\n return max(r1 / SQRT_2, r2); // Intersect diamond with rectangle\\n}\\n\",s.invertedtriangle=\"\\nfloat marker(vec2 P, float size)\\n{\\n P.y += size * 0.3;\\n float x = SQRT_2 / 2.0 * (P.x * 1.7 - P.y);\\n float y = SQRT_2 / 2.0 * (P.x * 1.7 + P.y);\\n float r1 = max(abs(x), abs(y)) - size / 1.6;\\n float r2 = - P.y;\\n return max(r1 / SQRT_2, r2); // Intersect diamond with rectangle\\n}\\n\",s.cross='\\nfloat marker(vec2 P, float size)\\n{\\n float square = max(abs(P.x), abs(P.y)) - size / 2.5; // 2.5 is a tweak\\n float cross = min(abs(P.x), abs(P.y)) - size / 100.0; // bit of \"width\" for aa\\n return max(square, cross);\\n}\\n',s.circlecross=\"\\nfloat marker(vec2 P, float size)\\n{\\n // Define quadrants\\n float qs = size / 2.0; // quadrant size\\n float s1 = max(abs(P.x - qs), abs(P.y - qs)) - qs;\\n float s2 = max(abs(P.x + qs), abs(P.y - qs)) - qs;\\n float s3 = max(abs(P.x - qs), abs(P.y + qs)) - qs;\\n float s4 = max(abs(P.x + qs), abs(P.y + qs)) - qs;\\n // Intersect main shape with quadrants (to form cross)\\n float circle = length(P) - size/2.0;\\n float c1 = max(circle, s1);\\n float c2 = max(circle, s2);\\n float c3 = max(circle, s3);\\n float c4 = max(circle, s4);\\n // Union\\n return min(min(min(c1, c2), c3), c4);\\n}\\n\",s.squarecross=\"\\nfloat marker(vec2 P, float size)\\n{\\n // Define quadrants\\n float qs = size / 2.0; // quadrant size\\n float s1 = max(abs(P.x - qs), abs(P.y - qs)) - qs;\\n float s2 = max(abs(P.x + qs), abs(P.y - qs)) - qs;\\n float s3 = max(abs(P.x - qs), abs(P.y + qs)) - qs;\\n float s4 = max(abs(P.x + qs), abs(P.y + qs)) - qs;\\n // Intersect main shape with quadrants (to form cross)\\n float square = max(abs(P.x), abs(P.y)) - size/2.0;\\n float c1 = max(square, s1);\\n float c2 = max(square, s2);\\n float c3 = max(square, s3);\\n float c4 = max(square, s4);\\n // Union\\n return min(min(min(c1, c2), c3), c4);\\n}\\n\",s.diamondcross=\"\\nfloat marker(vec2 P, float size)\\n{\\n // Define quadrants\\n float qs = size / 2.0; // quadrant size\\n float s1 = max(abs(P.x - qs), abs(P.y - qs)) - qs;\\n float s2 = max(abs(P.x + qs), abs(P.y - qs)) - qs;\\n float s3 = max(abs(P.x - qs), abs(P.y + qs)) - qs;\\n float s4 = max(abs(P.x + qs), abs(P.y + qs)) - qs;\\n // Intersect main shape with quadrants (to form cross)\\n float x = SQRT_2 / 2.0 * (P.x * 1.5 - P.y);\\n float y = SQRT_2 / 2.0 * (P.x * 1.5 + P.y);\\n float diamond = max(abs(x), abs(y)) - size / (2.0 * SQRT_2);\\n diamond /= SQRT_2;\\n float c1 = max(diamond, s1);\\n float c2 = max(diamond, s2);\\n float c3 = max(diamond, s3);\\n float c4 = max(diamond, s4);\\n // Union\\n return min(min(min(c1, c2), c3), c4);\\n}\\n\",s.x='\\nfloat marker(vec2 P, float size)\\n{\\n float circle = length(P) - size / 1.6;\\n float X = min(abs(P.x - P.y), abs(P.x + P.y)) - size / 100.0; // bit of \"width\" for aa\\n return max(circle, X);\\n}\\n',s.circlex='\\nfloat marker(vec2 P, float size)\\n{\\n float x = P.x - P.y;\\n float y = P.x + P.y;\\n // Define quadrants\\n float qs = size / 2.0; // quadrant size\\n float s1 = max(abs(x - qs), abs(y - qs)) - qs;\\n float s2 = max(abs(x + qs), abs(y - qs)) - qs;\\n float s3 = max(abs(x - qs), abs(y + qs)) - qs;\\n float s4 = max(abs(x + qs), abs(y + qs)) - qs;\\n // Intersect main shape with quadrants (to form cross)\\n float circle = length(P) - size/2.0;\\n float c1 = max(circle, s1);\\n float c2 = max(circle, s2);\\n float c3 = max(circle, s3);\\n float c4 = max(circle, s4);\\n // Union\\n float almost = min(min(min(c1, c2), c3), c4);\\n // In this case, the X is also outside of the main shape\\n float Xmask = length(P) - size / 1.6; // a circle\\n float X = min(abs(P.x - P.y), abs(P.x + P.y)) - size / 100.0; // bit of \"width\" for aa\\n return min(max(X, Xmask), almost);\\n}\\n',s.squarex=\"\\nfloat marker(vec2 P, float size)\\n{\\n float x = P.x - P.y;\\n float y = P.x + P.y;\\n // Define quadrants\\n float qs = size / 2.0; // quadrant size\\n float s1 = max(abs(x - qs), abs(y - qs)) - qs;\\n float s2 = max(abs(x + qs), abs(y - qs)) - qs;\\n float s3 = max(abs(x - qs), abs(y + qs)) - qs;\\n float s4 = max(abs(x + qs), abs(y + qs)) - qs;\\n // Intersect main shape with quadrants (to form cross)\\n float square = max(abs(P.x), abs(P.y)) - size/2.0;\\n float c1 = max(square, s1);\\n float c2 = max(square, s2);\\n float c3 = max(square, s3);\\n float c4 = max(square, s4);\\n // Union\\n return min(min(min(c1, c2), c3), c4);\\n}\\n\",s.asterisk='\\nfloat marker(vec2 P, float size)\\n{\\n // Masks\\n float diamond = max(abs(SQRT_2 / 2.0 * (P.x - P.y)), abs(SQRT_2 / 2.0 * (P.x + P.y))) - size / (2.0 * SQRT_2);\\n float square = max(abs(P.x), abs(P.y)) - size / (2.0 * SQRT_2);\\n // Shapes\\n float X = min(abs(P.x - P.y), abs(P.x + P.y)) - size / 100.0; // bit of \"width\" for aa\\n float cross = min(abs(P.x), abs(P.y)) - size / 100.0; // bit of \"width\" for aa\\n // Result is union of masked shapes\\n return min(max(X, diamond), max(cross, square));\\n}\\n'},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const a=e(1),i=e(93),l=e(28),s=a.__importStar(e(18));class c extends i.XYGlyphView{}n.CenterRotatableView=c,c.__name__=\"CenterRotatableView\";class o extends i.XYGlyph{constructor(e){super(e)}static init_CenterRotatable(){this.mixins([l.LineVector,l.FillVector]),this.define({angle:[s.AngleSpec,0],width:[s.DistanceSpec],height:[s.DistanceSpec]})}}n.CenterRotatable=o,o.__name__=\"CenterRotatable\",o.init_CenterRotatable()},\n function _(e,l,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(243);class t extends s.EllipseOvalView{}i.EllipseView=t,t.__name__=\"EllipseView\";class _ extends s.EllipseOval{constructor(e){super(e)}static init_Ellipse(){this.prototype.default_view=t}}i.Ellipse=_,_.__name__=\"Ellipse\",_.init_Ellipse()},\n function _(t,s,i){Object.defineProperty(i,\"__esModule\",{value:!0});const e=t(1),h=t(241),a=e.__importStar(t(101)),r=t(88);class n extends h.CenterRotatableView{_set_data(){this.max_w2=0,\"data\"==this.model.properties.width.units&&(this.max_w2=this.max_width/2),this.max_h2=0,\"data\"==this.model.properties.height.units&&(this.max_h2=this.max_height/2)}_map_data(){\"data\"==this.model.properties.width.units?this.sw=this.sdist(this.renderer.xscale,this._x,this._width,\"center\"):this.sw=this._width,\"data\"==this.model.properties.height.units?this.sh=this.sdist(this.renderer.yscale,this._y,this._height,\"center\"):this.sh=this._height}_render(t,s,{sx:i,sy:e,sw:h,sh:a,_angle:r}){for(const n of s)isNaN(i[n]+e[n]+h[n]+a[n]+r[n])||(t.beginPath(),t.ellipse(i[n],e[n],h[n]/2,a[n]/2,r[n],0,2*Math.PI),this.visuals.fill.doit&&(this.visuals.fill.set_vectorize(t,n),t.fill()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(t,n),t.stroke()))}_hit_point(t){let s,i,e,h,n,_,l,d,o;const{sx:x,sy:m}=t,w=this.renderer.xscale.invert(x),c=this.renderer.yscale.invert(m);\"data\"==this.model.properties.width.units?(s=w-this.max_width,i=w+this.max_width):(_=x-this.max_width,l=x+this.max_width,[s,i]=this.renderer.xscale.r_invert(_,l)),\"data\"==this.model.properties.height.units?(e=c-this.max_height,h=c+this.max_height):(d=m-this.max_height,o=m+this.max_height,[e,h]=this.renderer.yscale.r_invert(d,o));const p=this.index.indices({x0:s,x1:i,y0:e,y1:h}),y=[];for(const t of p)n=a.point_in_ellipse(x,m,this._angle[t],this.sh[t]/2,this.sw[t]/2,this.sx[t],this.sy[t]),n&&y.push(t);return new r.Selection({indices:y})}draw_legend_for_index(t,{x0:s,y0:i,x1:e,y1:h},a){const r=a+1,n=new Array(r);n[a]=(s+e)/2;const _=new Array(r);_[a]=(i+h)/2;const l=this.sw[a]/this.sh[a],d=.8*Math.min(Math.abs(e-s),Math.abs(h-i)),o=new Array(r),x=new Array(r);l>1?(o[a]=d,x[a]=d/l):(o[a]=d*l,x[a]=d),this._render(t,[a],{sx:n,sy:_,sw:o,sh:x,_angle:[0]})}_bounds({x0:t,x1:s,y0:i,y1:e}){return{x0:t-this.max_w2,x1:s+this.max_w2,y0:i-this.max_h2,y1:e+this.max_h2}}}i.EllipseOvalView=n,n.__name__=\"EllipseOvalView\";class _ extends h.CenterRotatable{constructor(t){super(t)}}i.EllipseOval=_,_.__name__=\"EllipseOval\"},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=t(1),h=t(245),r=t(24),_=i.__importStar(t(18));class a extends h.BoxView{scenterxy(t){return[(this.sleft[t]+this.sright[t])/2,this.sy[t]]}_lrtb(t){return[Math.min(this._left[t],this._right[t]),Math.max(this._left[t],this._right[t]),this._y[t]+.5*this._height[t],this._y[t]-.5*this._height[t]]}_map_data(){this.sy=this.renderer.yscale.v_compute(this._y),this.sh=this.sdist(this.renderer.yscale,this._y,this._height,\"center\"),this.sleft=this.renderer.xscale.v_compute(this._left),this.sright=this.renderer.xscale.v_compute(this._right);const t=this.sy.length;this.stop=new r.NumberArray(t),this.sbottom=new r.NumberArray(t);for(let e=0;e{t.beginPath(),t.rect(i[a],r[a],s[a]-i[a],n[a]-r[a]),t.fill()},()=>this.renderer.request_render()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(t,a),t.beginPath(),t.rect(i[a],r[a],s[a]-i[a],n[a]-r[a]),t.stroke()))}_clamp_viewport(){const t=this.renderer.plot_view.frame.bbox.h_range,e=this.renderer.plot_view.frame.bbox.v_range,i=this.stop.length;for(let s=0;sthis._update_image())}_update_image(){null!=this.image_data&&(this._set_data(null),this.renderer.plot_view.request_render())}_flat_img_to_buf8(e){return this.model.color_mapper.rgba_mapper.v_compute(e)}}a.ImageView=r,r.__name__=\"ImageView\";class o extends i.ImageBase{constructor(e){super(e)}static init_Image(){this.prototype.default_view=r,this.define({color_mapper:[s.Instance,()=>new n.LinearColorMapper({palette:[\"#000000\",\"#252525\",\"#525252\",\"#737373\",\"#969696\",\"#bdbdbd\",\"#d9d9d9\",\"#f0f0f0\",\"#ffffff\"]})]})}}a.Image=o,o.__name__=\"Image\",o.init_Image()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),a=e(93),h=e(24),_=i.__importStar(e(18)),n=e(88),r=e(9),d=e(30),l=e(11);class g extends a.XYGlyphView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.global_alpha.change,()=>this.renderer.request_render())}_render(e,t,{image_data:s,sx:i,sy:a,sw:h,sh:_}){const n=e.getImageSmoothingEnabled();e.setImageSmoothingEnabled(!1),e.globalAlpha=this.model.global_alpha;for(const n of t){if(null==s[n]||isNaN(i[n]+a[n]+h[n]+_[n]))continue;const t=a[n];e.translate(0,t),e.scale(1,-1),e.translate(0,-t),e.drawImage(s[n],0|i[n],0|a[n],h[n],_[n]),e.translate(0,t),e.scale(1,-1),e.translate(0,-t)}e.setImageSmoothingEnabled(n)}_set_data(e){this._set_width_heigh_data();for(let t=0,s=this._image.length;tthis.renderer.request_render())}_index_data(e){const{data_size:t}=this;for(let s=0;snull));const{retry_attempts:e,retry_timeout:t}=this.model;for(let s=0,r=this._url.length;s{this.image[s]=e,this.renderer.request_render()},attempts:e+1,timeout:t})}const s=\"data\"==this.model.properties.w.units,r=\"data\"==this.model.properties.h.units,i=this._x.length,n=new a.NumberArray(s?2*i:i),_=new a.NumberArray(r?2*i:i),{anchor:c}=this.model;function l(e,t){switch(c){case\"top_left\":case\"bottom_left\":case\"center_left\":return[e,e+t];case\"top_center\":case\"bottom_center\":case\"center\":return[e-t/2,e+t/2];case\"top_right\":case\"bottom_right\":case\"center_right\":return[e-t,e]}}function d(e,t){switch(c){case\"top_left\":case\"top_center\":case\"top_right\":return[e,e-t];case\"bottom_left\":case\"bottom_center\":case\"bottom_right\":return[e+t,e];case\"center_left\":case\"center\":case\"center_right\":return[e+t/2,e-t/2]}}if(s)for(let e=0;eNaN),t=null!=this.model.h?this._h:h.map(this._x,()=>NaN);switch(this.model.properties.w.units){case\"data\":this.sw=this.sdist(this.renderer.xscale,this._x,e,\"edge\",this.model.dilate);break;case\"screen\":this.sw=e}switch(this.model.properties.h.units){case\"data\":this.sh=this.sdist(this.renderer.yscale,this._y,t,\"edge\",this.model.dilate);break;case\"screen\":this.sh=t}}_render(e,t,{image:s,sx:r,sy:i,sw:a,sh:n,_angle:h}){const{frame:o}=this.renderer.plot_view;e.rect(o.bbox.left+1,o.bbox.top+1,o.bbox.width-2,o.bbox.height-2),e.clip();let _=!0;for(const o of t){if(isNaN(r[o]+i[o]+h[o]))continue;const t=s[o];null!=t?this._render_image(e,o,t,r,i,a,n,h):_=!1}_&&!this._images_rendered&&(this._images_rendered=!0,this.notify_finished())}_final_sx_sy(e,t,s,r,i){switch(e){case\"top_left\":return[t,s];case\"top_center\":return[t-r/2,s];case\"top_right\":return[t-r,s];case\"center_right\":return[t-r,s-i/2];case\"bottom_right\":return[t-r,s-i];case\"bottom_center\":return[t-r/2,s-i];case\"bottom_left\":return[t,s-i];case\"center_left\":return[t,s-i/2];case\"center\":return[t-r/2,s-i/2]}}_render_image(e,t,s,r,i,a,n,h){isNaN(a[t])&&(a[t]=s.width),isNaN(n[t])&&(n[t]=s.height);const{anchor:o}=this.model,[_,c]=this._final_sx_sy(o,r[t],i[t],a[t],n[t]);e.save(),e.globalAlpha=this.model.global_alpha;const l=a[t]/2,d=n[t]/2;h[t]?(e.translate(_,c),e.translate(l,d),e.rotate(h[t]),e.translate(-l,-d),e.drawImage(s,0,0,a[t],n[t]),e.translate(l,d),e.rotate(-h[t]),e.translate(-l,-d),e.translate(-_,-c)):e.drawImage(s,_,c,a[t],n[t]),e.restore()}bounds(){return this._bounds_rect}}s.ImageURLView=_,_.__name__=\"ImageURLView\";class c extends i.XYGlyph{constructor(e){super(e)}static init_ImageURL(){this.prototype.default_view=_,this.define({url:[n.StringSpec],anchor:[n.Anchor,\"top_left\"],global_alpha:[n.Number,1],angle:[n.AngleSpec,0],w:[n.DistanceSpec],h:[n.DistanceSpec],dilate:[n.Boolean,!1],retry_attempts:[n.Number,0],retry_timeout:[n.Number,0]})}}s.ImageURL=c,c.__name__=\"ImageURL\",c.init_ImageURL()},\n function _(i,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=i(19);class a{constructor(i,e={}){this._image=new Image,this._finished=!1;const{attempts:t=1,timeout:a=1}=e;this.promise=new Promise((o,n)=>{this._image.crossOrigin=\"anonymous\";let r=0;this._image.onerror=()=>{if(++r==t){const a=`unable to load ${i} image after ${t} attempts`;if(s.logger.warn(a),null==this._image.crossOrigin)return void(null!=e.failed&&e.failed());s.logger.warn(`attempting to load ${i} without a cross origin policy`),this._image.crossOrigin=null,r=0}setTimeout(()=>this._image.src=i,a)},this._image.onload=()=>{this._finished=!0,null!=e.loaded&&e.loaded(this._image),o(this._image)},this._image.src=i})}get finished(){return this._finished}get image(){return this._image}}t.ImageLoader=a,a.__name__=\"ImageLoader\"},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),n=e(37),o=e(28),l=s.__importStar(e(101)),r=s.__importStar(e(18)),_=e(12),c=e(13),a=e(94),h=e(100),d=e(88);class y extends a.GlyphView{_project_data(){n.inplace.project_xy(this._xs.array,this._ys.array)}_index_data(e){const{data_size:t}=this;for(let i=0;i0&&o.set(e,i)}return new d.Selection({indices:[...o.keys()],multiline_indices:c.to_object(o)})}get_interpolation_hit(e,t,i){const s=this._xs.get(e),n=this._ys.get(e),o=s[t],l=n[t],r=s[t+1],_=n[t+1];return h.line_interpolation(this.renderer,i,o,l,r,_)}draw_legend_for_index(e,t,i){h.generic_line_legend(this.visuals,e,t,i)}scenterxy(){throw new Error(this+\".scenterxy() is not implemented\")}}i.MultiLineView=y,y.__name__=\"MultiLineView\";class x extends a.Glyph{constructor(e){super(e)}static init_MultiLine(){this.prototype.default_view=y,this.define({xs:[r.XCoordinateSeqSpec,{field:\"xs\"}],ys:[r.YCoordinateSeqSpec,{field:\"ys\"}]}),this.mixins(o.LineVector)}}i.MultiLine=x,x.__name__=\"MultiLine\",x.init_MultiLine()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),n=e(95),o=e(94),r=e(100),l=e(12),h=e(12),_=e(28),a=i.__importStar(e(101)),d=i.__importStar(e(18)),c=e(88),x=e(11);class y extends o.GlyphView{_project_data(){}_index_data(e){const{min:t,max:s}=Math,{data_size:i}=this;for(let n=0;n1&&d.length>1)for(let s=1,i=n.length;s{this._inner_loop(e,t,o),e.fill(\"evenodd\")},()=>this.renderer.request_render()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(e,n),this._inner_loop(e,t,o),e.stroke())}}_hit_rect(e){const{sx0:t,sx1:s,sy0:i,sy1:n}=e,o=[t,s,s,t],r=[i,i,n,n],[l,h]=this.renderer.xscale.r_invert(t,s),[_,d]=this.renderer.yscale.r_invert(i,n),x=this.index.indices({x0:l,x1:h,y0:_,y1:d}),y=[];for(const e of x){const t=this.sxs[e],s=this.sys[e];let i=!0;for(let e=0,n=t.length;e1){let r=!1;for(let e=1;ethis._inner_loop(e,t,r,e.fill),()=>this.renderer.request_render()),this.visuals.line.doit&&(this.visuals.line.set_vectorize(e,n),this._inner_loop(e,t,r,e.stroke))}}_hit_rect(e){const{sx0:t,sx1:s,sy0:i,sy1:n}=e,r=[t,s,s,t],o=[i,i,n,n],[a,c]=this.renderer.xscale.r_invert(t,s),[h,d]=this.renderer.yscale.r_invert(i,n),y=this.index.indices({x0:a,x1:c,y0:h,y1:d}),p=[];for(const e of y){const t=this.sxs.get(e),s=this.sys.get(e);let i=!0;for(let e=0,n=t.length;e1&&(e.stroke(),s=!1)}s?(e.lineTo(t,a),e.lineTo(l,_)):(e.beginPath(),e.moveTo(i[r],n[r]),s=!0),o=r}e.lineTo(i[r-1],n[r-1]),e.stroke()}}draw_legend_for_index(e,t,i){o.generic_line_legend(this.visuals,e,t,i)}}i.StepView=a,a.__name__=\"StepView\";class _ extends s.XYGlyph{constructor(e){super(e)}static init_Step(){this.prototype.default_view=a,this.mixins(r.LineVector),this.define({mode:[l.StepMode,\"before\"]})}}i.Step=_,_.__name__=\"Step\",_.init_Step()},\n function _(t,s,e){Object.defineProperty(e,\"__esModule\",{value:!0});const i=t(1),n=t(93),_=t(28),o=i.__importStar(t(101)),h=i.__importStar(t(18)),l=t(159),a=t(11),r=t(88);class c extends n.XYGlyphView{_rotate_point(t,s,e,i,n){return[(t-e)*Math.cos(n)-(s-i)*Math.sin(n)+e,(t-e)*Math.sin(n)+(s-i)*Math.cos(n)+i]}_text_bounds(t,s,e,i){return[[t,t+e,t+e,t,t],[s,s,s-i,s-i,s]]}_render(t,s,{sx:e,sy:i,_x_offset:n,_y_offset:_,_angle:o,_text:h}){this._sys=[],this._sxs=[];for(const a of s)if(this._sxs[a]=[],this._sys[a]=[],!isNaN(e[a]+i[a]+n[a]+_[a]+o[a])&&null!=h[a]&&this.visuals.text.doit){const s=\"\"+h[a];t.save(),t.translate(e[a]+n[a],i[a]+_[a]),t.rotate(o[a]),this.visuals.text.set_vectorize(t,a);const r=this.visuals.text.cache_select(\"font\",a),{height:c}=l.measure_font(r),x=this.visuals.text.text_line_height.value()*c;if(-1==s.indexOf(\"\\n\")){t.fillText(s,0,0);const o=e[a]+n[a],h=i[a]+_[a],l=t.measureText(s).width,[r,c]=this._text_bounds(o,h,l,x);this._sxs[a].push(r),this._sys[a].push(c)}else{const o=s.split(\"\\n\"),h=x*o.length,l=this.visuals.text.cache_select(\"text_baseline\",a);let r;switch(l){case\"top\":r=0;break;case\"middle\":r=-h/2+x/2;break;case\"bottom\":r=-h+x;break;default:r=0,console.warn(`'${l}' baseline not supported with multi line text`)}for(const s of o){t.fillText(s,0,r);const o=e[a]+n[a],h=r+i[a]+_[a],l=t.measureText(s).width,[c,u]=this._text_bounds(o,h,l,x);this._sxs[a].push(c),this._sys[a].push(u),r+=x}}t.restore()}}_hit_point(t){const{sx:s,sy:e}=t,i=[];for(let t=0;tthis.request_render())}_draw_regions(i){if(!this.visuals.band_fill.doit&&!this.visuals.band_hatch.doit)return;this.visuals.band_fill.set_value(i);const[e,t]=this.grid_coords(\"major\",!1);for(let s=0;s{i.fillRect(n[0],r[0],o[1]-n[0],d[1]-r[0])},()=>this.request_render())}}_draw_grids(i){if(!this.visuals.grid_line.doit)return;const[e,t]=this.grid_coords(\"major\");this._draw_grid_helper(i,this.visuals.grid_line,e,t)}_draw_minor_grids(i){if(!this.visuals.minor_grid_line.doit)return;const[e,t]=this.grid_coords(\"minor\");this._draw_grid_helper(i,this.visuals.minor_grid_line,e,t)}_draw_grid_helper(i,e,t,s){e.set_value(i),i.beginPath();for(let e=0;et[1]&&(n=t[1]);else{[s,n]=t;for(const i of this.plot_view.axis_views)i.dimension==this.model.dimension&&i.model.x_range_name==this.model.x_range_name&&i.model.y_range_name==this.model.y_range_name&&([s,n]=i.computed_bounds)}return[s,n]}grid_coords(i,e=!0){const t=this.model.dimension,s=(t+1)%2,[n,r]=this.ranges();let[o,d]=this.computed_bounds();[o,d]=[Math.min(o,d),Math.max(o,d)];const _=[[],[]],a=this.model.get_ticker();if(null==a)return _;const l=a.get_ticks(o,d,n,r.min,{})[i],h=n.min,c=n.max,u=r.min,m=r.max;e||(l[0]!=h&&l.splice(0,0,h),l[l.length-1]!=c&&l.push(c));for(let i=0;ithis.rebuild())}get child_models(){return this.model.children}}i.BoxView=c,c.__name__=\"BoxView\";class r extends s.LayoutDOM{constructor(e){super(e)}static init_Box(){this.define({children:[o.Array,[]],spacing:[o.Number,0]})}}i.Box=r,r.__name__=\"Box\",r.init_Box()},\n function _(i,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const s=i(81),o=i(20),l=i(72),n=i(19),h=i(8),a=i(115),r=i(78),_=i(212),d=i(273),c=i(77);class u extends r.DOMView{constructor(){super(...arguments),this._idle_notified=!1,this._offset_parent=null,this._viewport={}}initialize(){super.initialize(),this.el.style.position=this.is_root?\"relative\":\"absolute\",this._child_views=new Map}async lazy_initialize(){await this.build_child_views()}remove(){for(const i of this.child_views)i.remove();this._child_views.clear(),super.remove()}connect_signals(){super.connect_signals(),this.is_root&&(this._on_resize=()=>this.resize_layout(),window.addEventListener(\"resize\",this._on_resize),this._parent_observer=setInterval(()=>{const i=this.el.offsetParent;this._offset_parent!=i&&(this._offset_parent=i,null!=i&&(this.compute_viewport(),this.invalidate_layout()))},250));const i=this.model.properties;this.on_change([i.width,i.height,i.min_width,i.min_height,i.max_width,i.max_height,i.margin,i.width_policy,i.height_policy,i.sizing_mode,i.aspect_ratio,i.visible],()=>this.invalidate_layout()),this.on_change([i.background,i.css_classes],()=>this.invalidate_render())}disconnect_signals(){null!=this._parent_observer&&clearTimeout(this._parent_observer),null!=this._on_resize&&window.removeEventListener(\"resize\",this._on_resize),super.disconnect_signals()}css_classes(){return super.css_classes().concat(this.model.css_classes)}get child_views(){return this.child_models.map(i=>this._child_views.get(i))}async build_child_views(){await a.build_views(this._child_views,this.child_models,{parent:this})}render(){super.render(),l.empty(this.el);const{background:i}=this.model;this.el.style.backgroundColor=null!=i?i:\"\",l.classes(this.el).clear().add(...this.css_classes());for(const i of this.child_views)this.el.appendChild(i.el),i.render()}update_layout(){for(const i of this.child_views)i.update_layout();this._update_layout()}update_position(){this.el.style.display=this.model.visible?\"block\":\"none\";const i=this.is_root?this.layout.sizing.margin:void 0;l.position(this.el,this.layout.bbox,i);for(const i of this.child_views)i.update_position()}after_layout(){for(const i of this.child_views)i.after_layout();this._has_finished=!0}compute_viewport(){this._viewport=this._viewport_size()}renderTo(i){i.appendChild(this.el),this._offset_parent=this.el.offsetParent,this.compute_viewport(),this.build()}build(){return this.assert_root(),this.render(),this.update_layout(),this.compute_layout(),this}async rebuild(){await this.build_child_views(),this.invalidate_render()}compute_layout(){const i=Date.now();this.layout.compute(this._viewport),this.update_position(),this.after_layout(),n.logger.debug(`layout computed in ${Date.now()-i} ms`),this.notify_finished()}resize_layout(){this.root.compute_viewport(),this.root.compute_layout()}invalidate_layout(){this.root.update_layout(),this.root.compute_layout()}invalidate_render(){this.render(),this.invalidate_layout()}has_finished(){if(!super.has_finished())return!1;for(const i of this.child_views)if(!i.has_finished())return!1;return!0}notify_finished(){this.is_root?!this._idle_notified&&this.has_finished()&&null!=this.model.document&&(this._idle_notified=!0,this.model.document.notify_idle(this.model)):this.root.notify_finished()}_width_policy(){return null!=this.model.width?\"fixed\":\"fit\"}_height_policy(){return null!=this.model.height?\"fixed\":\"fit\"}box_sizing(){let{width_policy:i,height_policy:t,aspect_ratio:e}=this.model;\"auto\"==i&&(i=this._width_policy()),\"auto\"==t&&(t=this._height_policy());const{sizing_mode:s}=this.model;if(null!=s)if(\"fixed\"==s)i=t=\"fixed\";else if(\"stretch_both\"==s)i=t=\"max\";else if(\"stretch_width\"==s)i=\"max\";else if(\"stretch_height\"==s)t=\"max\";else switch(null==e&&(e=\"auto\"),s){case\"scale_width\":i=\"max\",t=\"min\";break;case\"scale_height\":i=\"min\",t=\"max\";break;case\"scale_both\":i=\"max\",t=\"max\"}const o={width_policy:i,height_policy:t},{min_width:l,min_height:n}=this.model;null!=l&&(o.min_width=l),null!=n&&(o.min_height=n);const{width:a,height:r}=this.model;null!=a&&(o.width=a),null!=r&&(o.height=r);const{max_width:_,max_height:d}=this.model;null!=_&&(o.max_width=_),null!=d&&(o.max_height=d),\"auto\"==e&&null!=a&&null!=r?o.aspect=a/r:h.isNumber(e)&&(o.aspect=e);const{margin:c}=this.model;if(null!=c)if(h.isNumber(c))o.margin={top:c,right:c,bottom:c,left:c};else if(2==c.length){const[i,t]=c;o.margin={top:i,right:t,bottom:i,left:t}}else{const[i,t,e,s]=c;o.margin={top:i,right:t,bottom:e,left:s}}o.visible=this.model.visible;const{align:u}=this.model;return h.isArray(u)?[o.halign,o.valign]=u:o.halign=o.valign=u,o}_viewport_size(){return l.undisplayed(this.el,()=>{let i=this.el;for(;i=i.parentElement;){if(i.classList.contains(d.bk_root))continue;if(i==document.body){const{margin:{left:i,right:t,top:e,bottom:s}}=l.extents(document.body);return{width:Math.ceil(document.documentElement.clientWidth-i-t),height:Math.ceil(document.documentElement.clientHeight-e-s)}}const{padding:{left:t,right:e,top:s,bottom:o}}=l.extents(i),{width:n,height:h}=i.getBoundingClientRect(),a=Math.ceil(n-t-e),r=Math.ceil(h-s-o);if(a>0||r>0)return{width:a>0?a:void 0,height:r>0?r:void 0}}return{}})}export(i,t=!0){const e=\"png\"==i?\"canvas\":\"svg\",s=new c.CanvasLayer(e,t),{width:o,height:l}=this.layout.bbox;s.resize(o,l);for(const e of this.child_views){const o=e.export(i,t),{x:l,y:n}=e.layout.bbox;s.ctx.drawImage(o.canvas,l,n)}return s}serializable_state(){return Object.assign(Object.assign({},super.serializable_state()),{bbox:this.layout.bbox.box,children:this.child_views.map(i=>i.serializable_state())})}}e.LayoutDOMView=u,u.__name__=\"LayoutDOMView\";class m extends s.Model{constructor(i){super(i)}static init_LayoutDOM(){this.define(i=>{const{Boolean:t,Number:e,String:s,Null:l,Auto:n,Color:h,Array:a,Tuple:r,Or:d}=i,c=r(e,e),u=r(e,e,e,e);return{width:[d(e,l),null],height:[d(e,l),null],min_width:[d(e,l),null],min_height:[d(e,l),null],max_width:[d(e,l),null],max_height:[d(e,l),null],margin:[d(e,c,u),[0,0,0,0]],width_policy:[d(_.SizingPolicy,n),\"auto\"],height_policy:[d(_.SizingPolicy,n),\"auto\"],aspect_ratio:[d(e,n,l),null],sizing_mode:[d(o.SizingMode,l),null],visible:[t,!0],disabled:[t,!1],align:[d(o.Align,r(o.Align,o.Align)),\"start\"],background:[d(h,l),null],css_classes:[a(s),[]]}})}}e.LayoutDOM=m,m.__name__=\"LayoutDOM\",m.init_LayoutDOM()},\n function _(e,o,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.bk_root=\"bk-root\"},\n function _(t,o,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),e=t(271),n=t(216),l=s.__importStar(t(18));class u extends e.BoxView{_update_layout(){const t=this.child_views.map(t=>t.layout);this.layout=new n.Column(t),this.layout.rows=this.model.rows,this.layout.spacing=[this.model.spacing,0],this.layout.set_sizing(this.box_sizing())}}i.ColumnView=u,u.__name__=\"ColumnView\";class _ extends e.Box{constructor(t){super(t)}static init_Column(){this.prototype.default_view=u,this.define({rows:[l.Any,\"auto\"]})}}i.Column=_,_.__name__=\"Column\",_.init_Column()},\n function _(t,i,s){Object.defineProperty(s,\"__esModule\",{value:!0});const o=t(1),e=t(272),n=t(216),l=o.__importStar(t(18));class r extends e.LayoutDOMView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.children.change,()=>this.rebuild())}get child_models(){return this.model.children.map(([t])=>t)}_update_layout(){this.layout=new n.Grid,this.layout.rows=this.model.rows,this.layout.cols=this.model.cols,this.layout.spacing=this.model.spacing;for(const[t,i,s,o,e]of this.model.children){const n=this._child_views.get(t);this.layout.items.push({layout:n.layout,row:i,col:s,row_span:o,col_span:e})}this.layout.set_sizing(this.box_sizing())}}s.GridBoxView=r,r.__name__=\"GridBoxView\";class a extends e.LayoutDOM{constructor(t){super(t)}static init_GridBox(){this.prototype.default_view=r,this.define({children:[l.Array,[]],rows:[l.Any,\"auto\"],cols:[l.Any,\"auto\"],spacing:[l.Any,0]})}}s.GridBox=a,a.__name__=\"GridBox\",a.init_GridBox()},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const s=e(272),_=e(212);class n extends s.LayoutDOMView{get child_models(){return[]}_update_layout(){this.layout=new _.ContentBox(this.el),this.layout.set_sizing(this.box_sizing())}}o.HTMLBoxView=n,n.__name__=\"HTMLBoxView\";class i extends s.LayoutDOM{constructor(e){super(e)}}o.HTMLBox=i,i.__name__=\"HTMLBox\"},\n function _(t,o,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),e=t(271),_=t(216),a=s.__importStar(t(18));class n extends e.BoxView{_update_layout(){const t=this.child_views.map(t=>t.layout);this.layout=new _.Row(t),this.layout.cols=this.model.cols,this.layout.spacing=[0,this.model.spacing],this.layout.set_sizing(this.box_sizing())}}i.RowView=n,n.__name__=\"RowView\";class l extends e.Box{constructor(t){super(t)}static init_Row(){this.prototype.default_view=n,this.define({cols:[a.Any,\"auto\"]})}}i.Row=l,l.__name__=\"Row\",l.init_Row()},\n function _(e,t,a){Object.defineProperty(a,\"__esModule\",{value:!0});const i=e(272),s=e(212);class _ extends i.LayoutDOMView{get child_models(){return[]}_update_layout(){this.layout=new s.LayoutItem,this.layout.set_sizing(this.box_sizing())}}a.SpacerView=_,_.__name__=\"SpacerView\";class o extends i.LayoutDOM{constructor(e){super(e)}static init_Spacer(){this.prototype.default_view=_}}a.Spacer=o,o.__name__=\"Spacer\",o.init_Spacer()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),a=e(212),l=e(72),h=e(9),o=i.__importStar(e(18)),c=e(272),d=e(81),r=e(173),n=e(280),_=e(281),b=e(282),p=i.__importDefault(e(283)),u=i.__importDefault(e(284)),m=i.__importDefault(e(285));class v extends c.LayoutDOMView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.tabs.change,()=>this.rebuild()),this.connect(this.model.properties.active.change,()=>this.on_active_change())}styles(){return[...super.styles(),p.default,u.default,m.default]}get child_models(){return this.model.tabs.map(e=>e.child)}_update_layout(){const e=this.model.tabs_location,t=\"above\"==e||\"below\"==e,{scroll_el:s,headers_el:i}=this;this.header=new class extends a.ContentBox{_measure(e){const a=l.size(s),o=l.children(i).slice(0,3).map(e=>l.size(e)),{width:c,height:d}=super._measure(e);if(t){const t=a.width+h.sum(o.map(e=>e.width));return{width:e.width!=1/0?e.width:t,height:d}}{const t=a.height+h.sum(o.map(e=>e.height));return{width:c,height:e.height!=1/0?e.height:t}}}}(this.header_el),t?this.header.set_sizing({width_policy:\"fit\",height_policy:\"fixed\"}):this.header.set_sizing({width_policy:\"fixed\",height_policy:\"fit\"});let o=1,c=1;switch(e){case\"above\":o-=1;break;case\"below\":o+=1;break;case\"left\":c-=1;break;case\"right\":c+=1}const d={layout:this.header,row:o,col:c},r=this.child_views.map(e=>({layout:e.layout,row:1,col:1}));this.layout=new a.Grid([d,...r]),this.layout.set_sizing(this.box_sizing())}update_position(){super.update_position(),this.header_el.style.position=\"absolute\",l.position(this.header_el,this.header.bbox);const e=this.model.tabs_location,t=\"above\"==e||\"below\"==e,s=l.size(this.scroll_el),i=l.scroll_size(this.headers_el);if(t){const{width:e}=this.header.bbox;i.width>e?(this.wrapper_el.style.maxWidth=e-s.width+\"px\",l.display(this.scroll_el)):(this.wrapper_el.style.maxWidth=\"\",l.undisplay(this.scroll_el))}else{const{height:e}=this.header.bbox;i.height>e?(this.wrapper_el.style.maxHeight=e-s.height+\"px\",l.display(this.scroll_el)):(this.wrapper_el.style.maxHeight=\"\",l.undisplay(this.scroll_el))}const{child_views:a}=this;for(const e of a)l.hide(e.el);const h=a[this.model.active];null!=h&&l.show(h.el)}render(){super.render();const{active:e}=this.model,t=this.model.tabs_location,s=\"above\"==t||\"below\"==t,i=this.model.tabs.map((t,s)=>{const i=l.div({class:[n.bk_tab,s==e?r.bk_active:null]},t.title);if(i.addEventListener(\"click\",e=>{e.target==e.currentTarget&&this.change_active(s)}),t.closable){const e=l.div({class:n.bk_close});e.addEventListener(\"click\",e=>{if(e.target==e.currentTarget){this.model.tabs=h.remove_at(this.model.tabs,s);const e=this.model.tabs.length;this.model.active>e-1&&(this.model.active=e-1)}}),i.appendChild(e)}return i});this.headers_el=l.div({class:[n.bk_headers]},i),this.wrapper_el=l.div({class:n.bk_headers_wrapper},this.headers_el);const a=l.div({class:[_.bk_btn,_.bk_btn_default],disabled:\"\"},l.div({class:[b.bk_caret,r.bk_left]})),o=l.div({class:[_.bk_btn,_.bk_btn_default]},l.div({class:[b.bk_caret,r.bk_right]}));let c=0;const d=e=>()=>{const t=this.model.tabs.length;c=\"left\"==e?Math.max(c-1,0):Math.min(c+1,t-1),0==c?a.setAttribute(\"disabled\",\"\"):a.removeAttribute(\"disabled\"),c==t-1?o.setAttribute(\"disabled\",\"\"):o.removeAttribute(\"disabled\");const i=l.children(this.headers_el).slice(0,c).map(e=>e.getBoundingClientRect());if(s){const e=-h.sum(i.map(e=>e.width));this.headers_el.style.left=e+\"px\"}else{const e=-h.sum(i.map(e=>e.height));this.headers_el.style.top=e+\"px\"}};a.addEventListener(\"click\",d(\"left\")),o.addEventListener(\"click\",d(\"right\")),this.scroll_el=l.div({class:_.bk_btn_group},a,o),this.header_el=l.div({class:[n.bk_tabs_header,r.bk_side(t)]},this.scroll_el,this.wrapper_el),this.el.appendChild(this.header_el)}change_active(e){e!=this.model.active&&(this.model.active=e)}on_active_change(){const e=this.model.active,t=l.children(this.headers_el);for(const e of t)e.classList.remove(r.bk_active);t[e].classList.add(r.bk_active);const{child_views:s}=this;for(const e of s)l.hide(e.el);l.show(s[e].el)}}s.TabsView=v,v.__name__=\"TabsView\";class g extends c.LayoutDOM{constructor(e){super(e)}static init_Tabs(){this.prototype.default_view=v,this.define({tabs:[o.Array,[]],tabs_location:[o.Location,\"above\"],active:[o.Number,0]})}}s.Tabs=g,g.__name__=\"Tabs\",g.init_Tabs();class w extends d.Model{constructor(e){super(e)}static init_Panel(){this.define({title:[o.String,\"\"],child:[o.Instance],closable:[o.Boolean,!1]})}}s.Panel=w,w.__name__=\"Panel\",w.init_Panel()},\n function _(e,b,a){Object.defineProperty(a,\"__esModule\",{value:!0}),a.bk_tabs_header=\"bk-tabs-header\",a.bk_headers_wrapper=\"bk-headers-wrapper\",a.bk_headers=\"bk-headers\",a.bk_tab=\"bk-tab\",a.bk_close=\"bk-close\"},\n function _(n,b,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.bk_btn=\"bk-btn\",t.bk_btn_group=\"bk-btn-group\",t.bk_btn_default=\"bk-btn-default\",t.bk_btn_primary=\"bk-btn-primary\",t.bk_btn_success=\"bk-btn-success\",t.bk_btn_warning=\"bk-btn-warning\",t.bk_btn_danger=\"bk-btn-danger\",t.bk_btn_type=function(n){switch(n){case\"default\":return t.bk_btn_default;case\"primary\":return t.bk_btn_primary;case\"success\":return t.bk_btn_success;case\"warning\":return t.bk_btn_warning;case\"danger\":return t.bk_btn_danger}},t.bk_dropdown_toggle=\"bk-dropdown-toggle\"},\n function _(e,b,d){Object.defineProperty(d,\"__esModule\",{value:!0}),d.bk_menu=\"bk-menu\",d.bk_caret=\"bk-caret\",d.bk_divider=\"bk-divider\"},\n function _(n,o,b){Object.defineProperty(b,\"__esModule\",{value:!0});b.default=\"\\n.bk-root .bk-btn {\\n height: 100%;\\n display: inline-block;\\n text-align: center;\\n vertical-align: middle;\\n white-space: nowrap;\\n cursor: pointer;\\n padding: 6px 12px;\\n font-size: 12px;\\n border: 1px solid transparent;\\n border-radius: 4px;\\n outline: 0;\\n user-select: none;\\n -ms-user-select: none;\\n -moz-user-select: none;\\n -webkit-user-select: none;\\n}\\n.bk-root .bk-btn:hover,\\n.bk-root .bk-btn:focus {\\n text-decoration: none;\\n}\\n.bk-root .bk-btn:active,\\n.bk-root .bk-btn.bk-active {\\n background-image: none;\\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\\n}\\n.bk-root .bk-btn[disabled] {\\n cursor: not-allowed;\\n pointer-events: none;\\n opacity: 0.65;\\n box-shadow: none;\\n}\\n.bk-root .bk-btn-default {\\n color: #333;\\n background-color: #fff;\\n border-color: #ccc;\\n}\\n.bk-root .bk-btn-default:hover {\\n background-color: #f5f5f5;\\n border-color: #b8b8b8;\\n}\\n.bk-root .bk-btn-default.bk-active {\\n background-color: #ebebeb;\\n border-color: #adadad;\\n}\\n.bk-root .bk-btn-default[disabled],\\n.bk-root .bk-btn-default[disabled]:hover,\\n.bk-root .bk-btn-default[disabled]:focus,\\n.bk-root .bk-btn-default[disabled]:active,\\n.bk-root .bk-btn-default[disabled].bk-active {\\n background-color: #e6e6e6;\\n border-color: #ccc;\\n}\\n.bk-root .bk-btn-primary {\\n color: #fff;\\n background-color: #428bca;\\n border-color: #357ebd;\\n}\\n.bk-root .bk-btn-primary:hover {\\n background-color: #3681c1;\\n border-color: #2c699e;\\n}\\n.bk-root .bk-btn-primary.bk-active {\\n background-color: #3276b1;\\n border-color: #285e8e;\\n}\\n.bk-root .bk-btn-primary[disabled],\\n.bk-root .bk-btn-primary[disabled]:hover,\\n.bk-root .bk-btn-primary[disabled]:focus,\\n.bk-root .bk-btn-primary[disabled]:active,\\n.bk-root .bk-btn-primary[disabled].bk-active {\\n background-color: #506f89;\\n border-color: #357ebd;\\n}\\n.bk-root .bk-btn-success {\\n color: #fff;\\n background-color: #5cb85c;\\n border-color: #4cae4c;\\n}\\n.bk-root .bk-btn-success:hover {\\n background-color: #4eb24e;\\n border-color: #409240;\\n}\\n.bk-root .bk-btn-success.bk-active {\\n background-color: #47a447;\\n border-color: #398439;\\n}\\n.bk-root .bk-btn-success[disabled],\\n.bk-root .bk-btn-success[disabled]:hover,\\n.bk-root .bk-btn-success[disabled]:focus,\\n.bk-root .bk-btn-success[disabled]:active,\\n.bk-root .bk-btn-success[disabled].bk-active {\\n background-color: #667b66;\\n border-color: #4cae4c;\\n}\\n.bk-root .bk-btn-warning {\\n color: #fff;\\n background-color: #f0ad4e;\\n border-color: #eea236;\\n}\\n.bk-root .bk-btn-warning:hover {\\n background-color: #eea43b;\\n border-color: #e89014;\\n}\\n.bk-root .bk-btn-warning.bk-active {\\n background-color: #ed9c28;\\n border-color: #d58512;\\n}\\n.bk-root .bk-btn-warning[disabled],\\n.bk-root .bk-btn-warning[disabled]:hover,\\n.bk-root .bk-btn-warning[disabled]:focus,\\n.bk-root .bk-btn-warning[disabled]:active,\\n.bk-root .bk-btn-warning[disabled].bk-active {\\n background-color: #c89143;\\n border-color: #eea236;\\n}\\n.bk-root .bk-btn-danger {\\n color: #fff;\\n background-color: #d9534f;\\n border-color: #d43f3a;\\n}\\n.bk-root .bk-btn-danger:hover {\\n background-color: #d5433e;\\n border-color: #bd2d29;\\n}\\n.bk-root .bk-btn-danger.bk-active {\\n background-color: #d2322d;\\n border-color: #ac2925;\\n}\\n.bk-root .bk-btn-danger[disabled],\\n.bk-root .bk-btn-danger[disabled]:hover,\\n.bk-root .bk-btn-danger[disabled]:focus,\\n.bk-root .bk-btn-danger[disabled]:active,\\n.bk-root .bk-btn-danger[disabled].bk-active {\\n background-color: #a55350;\\n border-color: #d43f3a;\\n}\\n.bk-root .bk-btn-group {\\n height: 100%;\\n display: flex;\\n display: -webkit-flex;\\n flex-wrap: nowrap;\\n -webkit-flex-wrap: nowrap;\\n align-items: center;\\n -webkit-align-items: center;\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n}\\n.bk-root .bk-btn-group > .bk-btn {\\n flex-grow: 1;\\n -webkit-flex-grow: 1;\\n}\\n.bk-root .bk-btn-group > .bk-btn + .bk-btn {\\n margin-left: -1px;\\n}\\n.bk-root .bk-btn-group > .bk-btn:first-child:not(:last-child) {\\n border-bottom-right-radius: 0;\\n border-top-right-radius: 0;\\n}\\n.bk-root .bk-btn-group > .bk-btn:not(:first-child):last-child {\\n border-bottom-left-radius: 0;\\n border-top-left-radius: 0;\\n}\\n.bk-root .bk-btn-group > .bk-btn:not(:first-child):not(:last-child) {\\n border-radius: 0;\\n}\\n.bk-root .bk-btn-group .bk-dropdown-toggle {\\n flex: 0 0 0;\\n -webkit-flex: 0 0 0;\\n padding: 6px 6px;\\n}\\n\"},\n function _(n,o,r){Object.defineProperty(r,\"__esModule\",{value:!0});r.default=\"\\n.bk-root .bk-menu-icon {\\n width: 28px;\\n height: 28px;\\n background-size: 60%;\\n background-color: transparent;\\n background-repeat: no-repeat;\\n background-position: center center;\\n}\\n.bk-root .bk-context-menu {\\n position: absolute;\\n display: inline-flex;\\n display: -webkit-inline-flex;\\n flex-wrap: nowrap;\\n -webkit-flex-wrap: nowrap;\\n user-select: none;\\n -ms-user-select: none;\\n -moz-user-select: none;\\n -webkit-user-select: none;\\n width: auto;\\n height: auto;\\n z-index: 100;\\n cursor: pointer;\\n font-size: 12px;\\n background-color: #fff;\\n border: 1px solid #ccc;\\n border-radius: 4px;\\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\\n}\\n.bk-root .bk-context-menu.bk-horizontal {\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n}\\n.bk-root .bk-context-menu.bk-vertical {\\n flex-direction: column;\\n -webkit-flex-direction: column;\\n}\\n.bk-root .bk-context-menu > .bk-divider {\\n cursor: default;\\n overflow: hidden;\\n background-color: #e5e5e5;\\n}\\n.bk-root .bk-context-menu.bk-horizontal > .bk-divider {\\n width: 1px;\\n margin: 5px 0;\\n}\\n.bk-root .bk-context-menu.bk-vertical > .bk-divider {\\n height: 1px;\\n margin: 0 5px;\\n}\\n.bk-root .bk-context-menu > :not(.bk-divider) {\\n border: 1px solid transparent;\\n}\\n.bk-root .bk-context-menu > :not(.bk-divider).bk-active {\\n border-color: #26aae1;\\n}\\n.bk-root .bk-context-menu > :not(.bk-divider):hover {\\n background-color: #f9f9f9;\\n}\\n.bk-root .bk-context-menu.bk-horizontal > :not(.bk-divider):first-child {\\n border-top-left-radius: 4px;\\n border-bottom-left-radius: 4px;\\n}\\n.bk-root .bk-context-menu.bk-horizontal > :not(.bk-divider):last-child {\\n border-top-right-radius: 4px;\\n border-bottom-right-radius: 4px;\\n}\\n.bk-root .bk-context-menu.bk-vertical > :not(.bk-divider):first-child {\\n border-top-left-radius: 4px;\\n border-top-right-radius: 4px;\\n}\\n.bk-root .bk-context-menu.bk-vertical > :not(.bk-divider):last-child {\\n border-bottom-left-radius: 4px;\\n border-bottom-right-radius: 4px;\\n}\\n.bk-root .bk-menu {\\n position: absolute;\\n left: 0;\\n width: 100%;\\n z-index: 100;\\n cursor: pointer;\\n font-size: 12px;\\n background-color: #fff;\\n border: 1px solid #ccc;\\n border-radius: 4px;\\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\\n}\\n.bk-root .bk-menu.bk-above {\\n bottom: 100%;\\n}\\n.bk-root .bk-menu.bk-below {\\n top: 100%;\\n}\\n.bk-root .bk-menu > .bk-divider {\\n height: 1px;\\n margin: 7.5px 0;\\n overflow: hidden;\\n background-color: #e5e5e5;\\n}\\n.bk-root .bk-menu > :not(.bk-divider) {\\n padding: 6px 12px;\\n}\\n.bk-root .bk-menu > :not(.bk-divider):hover,\\n.bk-root .bk-menu > :not(.bk-divider).bk-active {\\n background-color: #e6e6e6;\\n}\\n.bk-root .bk-caret {\\n display: inline-block;\\n vertical-align: middle;\\n width: 0;\\n height: 0;\\n margin: 0 5px;\\n}\\n.bk-root .bk-caret.bk-down {\\n border-top: 4px solid;\\n}\\n.bk-root .bk-caret.bk-up {\\n border-bottom: 4px solid;\\n}\\n.bk-root .bk-caret.bk-down,\\n.bk-root .bk-caret.bk-up {\\n border-right: 4px solid transparent;\\n border-left: 4px solid transparent;\\n}\\n.bk-root .bk-caret.bk-left {\\n border-right: 4px solid;\\n}\\n.bk-root .bk-caret.bk-right {\\n border-left: 4px solid;\\n}\\n.bk-root .bk-caret.bk-left,\\n.bk-root .bk-caret.bk-right {\\n border-top: 4px solid transparent;\\n border-bottom: 4px solid transparent;\\n}\\n\"},\n function _(e,r,n){Object.defineProperty(n,\"__esModule\",{value:!0});n.default='\\n.bk-root .bk-tabs-header {\\n display: flex;\\n display: -webkit-flex;\\n flex-wrap: nowrap;\\n -webkit-flex-wrap: nowrap;\\n align-items: center;\\n -webkit-align-items: center;\\n overflow: hidden;\\n user-select: none;\\n -ms-user-select: none;\\n -moz-user-select: none;\\n -webkit-user-select: none;\\n}\\n.bk-root .bk-tabs-header .bk-btn-group {\\n height: auto;\\n margin-right: 5px;\\n}\\n.bk-root .bk-tabs-header .bk-btn-group > .bk-btn {\\n flex-grow: 0;\\n -webkit-flex-grow: 0;\\n height: auto;\\n padding: 4px 4px;\\n}\\n.bk-root .bk-tabs-header .bk-headers-wrapper {\\n flex-grow: 1;\\n -webkit-flex-grow: 1;\\n overflow: hidden;\\n color: #666666;\\n}\\n.bk-root .bk-tabs-header.bk-above .bk-headers-wrapper {\\n border-bottom: 1px solid #e6e6e6;\\n}\\n.bk-root .bk-tabs-header.bk-right .bk-headers-wrapper {\\n border-left: 1px solid #e6e6e6;\\n}\\n.bk-root .bk-tabs-header.bk-below .bk-headers-wrapper {\\n border-top: 1px solid #e6e6e6;\\n}\\n.bk-root .bk-tabs-header.bk-left .bk-headers-wrapper {\\n border-right: 1px solid #e6e6e6;\\n}\\n.bk-root .bk-tabs-header.bk-above,\\n.bk-root .bk-tabs-header.bk-below {\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n}\\n.bk-root .bk-tabs-header.bk-above .bk-headers,\\n.bk-root .bk-tabs-header.bk-below .bk-headers {\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n}\\n.bk-root .bk-tabs-header.bk-left,\\n.bk-root .bk-tabs-header.bk-right {\\n flex-direction: column;\\n -webkit-flex-direction: column;\\n}\\n.bk-root .bk-tabs-header.bk-left .bk-headers,\\n.bk-root .bk-tabs-header.bk-right .bk-headers {\\n flex-direction: column;\\n -webkit-flex-direction: column;\\n}\\n.bk-root .bk-tabs-header .bk-headers {\\n position: relative;\\n display: flex;\\n display: -webkit-flex;\\n flex-wrap: nowrap;\\n -webkit-flex-wrap: nowrap;\\n align-items: center;\\n -webkit-align-items: center;\\n}\\n.bk-root .bk-tabs-header .bk-tab {\\n padding: 4px 8px;\\n border: solid transparent;\\n white-space: nowrap;\\n cursor: pointer;\\n}\\n.bk-root .bk-tabs-header .bk-tab:hover {\\n background-color: #f2f2f2;\\n}\\n.bk-root .bk-tabs-header .bk-tab.bk-active {\\n color: #4d4d4d;\\n background-color: white;\\n border-color: #e6e6e6;\\n}\\n.bk-root .bk-tabs-header .bk-tab .bk-close {\\n margin-left: 10px;\\n}\\n.bk-root .bk-tabs-header.bk-above .bk-tab {\\n border-width: 3px 1px 0px 1px;\\n border-radius: 4px 4px 0 0;\\n}\\n.bk-root .bk-tabs-header.bk-right .bk-tab {\\n border-width: 1px 3px 1px 0px;\\n border-radius: 0 4px 4px 0;\\n}\\n.bk-root .bk-tabs-header.bk-below .bk-tab {\\n border-width: 0px 1px 3px 1px;\\n border-radius: 0 0 4px 4px;\\n}\\n.bk-root .bk-tabs-header.bk-left .bk-tab {\\n border-width: 1px 0px 1px 3px;\\n border-radius: 4px 0 0 4px;\\n}\\n.bk-root .bk-close {\\n display: inline-block;\\n width: 10px;\\n height: 10px;\\n vertical-align: middle;\\n background-image: url(\\'data:image/svg+xml;utf8, \\');\\n}\\n.bk-root .bk-close:hover {\\n background-image: url(\\'data:image/svg+xml;utf8, \\');\\n}\\n'},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const o=e(274);class _ extends o.ColumnView{}i.WidgetBoxView=_,_.__name__=\"WidgetBoxView\";class n extends o.Column{constructor(e){super(e)}static init_WidgetBox(){this.prototype.default_view=_}}i.WidgetBox=n,n.__name__=\"WidgetBox\",n.init_WidgetBox()},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});e(1).__exportStar(e(288),t);var a=e(289);t.Marker=a.Marker;var _=e(290);t.Scatter=_.Scatter},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const i=e(1),r=e(289),n=i.__importStar(e(238)),s=Math.sqrt(3);function c(e,t){e.rotate(Math.PI/4),a(e,t),e.rotate(-Math.PI/4)}function l(e,t){const o=t*s,i=o/3;e.moveTo(-o/2,-i),e.lineTo(0,0),e.lineTo(o/2,-i),e.lineTo(0,0),e.lineTo(0,t)}function a(e,t){e.moveTo(0,t),e.lineTo(0,-t),e.moveTo(-t,0),e.lineTo(t,0)}function u(e,t){e.moveTo(0,t),e.lineTo(t/1.5,0),e.lineTo(0,-t),e.lineTo(-t/1.5,0),e.closePath()}function d(e,t){const o=t*s,i=o/3;e.moveTo(-t,i),e.lineTo(t,i),e.lineTo(0,i-o),e.closePath()}function v(e,t,o,i,r){a(e,o),c(e,o),i.doit&&(i.set_vectorize(e,t),e.stroke())}function _(e,t,o,i,r){e.arc(0,0,o,0,2*Math.PI,!1),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function f(e,t,o,i,r){e.arc(0,0,o,0,2*Math.PI,!1),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),a(e,o),e.stroke())}function T(e,t,o,i,r){_(e,t,o,i,r),P(e,t,o,i,r)}function z(e,t,o,i,r){e.arc(0,0,o,0,2*Math.PI,!1),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),l(e,o),e.stroke())}function C(e,t,o,i,r){e.arc(0,0,o,0,2*Math.PI,!1),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),c(e,o),e.stroke())}function k(e,t,o,i,r){a(e,o),i.doit&&(i.set_vectorize(e,t),e.stroke())}function m(e,t,o,i,r){u(e,o),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function h(e,t,o,i,r){u(e,o),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.moveTo(0,o),e.lineTo(0,-o),e.moveTo(-o/1.5,0),e.lineTo(o/1.5,0),e.stroke())}function q(e,t,o,i,r){m(e,t,o,i,r),P(e,t,o,i,r)}function P(e,t,o,i,r){!function(e,t){e.beginPath(),e.arc(0,0,t/4,0,2*Math.PI,!1),e.closePath()}(e,o),i.set_vectorize(e,t),e.fillStyle=e.strokeStyle,e.fill()}function D(e,t,o,i,r){!function(e,t){const o=t/2,i=s*o;e.moveTo(t,0),e.lineTo(o,-i),e.lineTo(-o,-i),e.lineTo(-t,0),e.lineTo(-o,i),e.lineTo(o,i),e.closePath()}(e,o),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function g(e,t,o,i,r){D(e,t,o,i,r),P(e,t,o,i)}function S(e,t,o,i,r){e.rotate(Math.PI),d(e,o),e.rotate(-Math.PI),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function G(e,t,o,i,r){const n=3*o/8,s=[n,n,o,o,n,n,-n,-n,-o,-o,-n,-n],c=[o,n,n,-n,-n,-o,-o,-n,-n,n,n,o];for(e.moveTo(s[0],c[0]),t=1;t<12;t++)e.lineTo(s[t],c[t]);e.closePath(),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function L(e,t,o,i,r){const n=2*o;e.rect(-o,-o,n,n),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function M(e,t,o,i,r){const n=3*o/8;e.moveTo(-o,-o),e.quadraticCurveTo(0,-n,o,-o),e.quadraticCurveTo(n,0,o,o),e.quadraticCurveTo(0,n,-o,o),e.quadraticCurveTo(-n,0,-o,-o),e.closePath(),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function p(e,t,o,i,r){const n=2*o;e.rect(-o,-o,n,n),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),a(e,o),e.stroke())}function x(e,t,o,i,r){L(e,t,o,i,r),P(e,t,o,i)}function I(e,t,o,i,r){const n=2*o;e.rect(-o,-o,n,n),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.moveTo(-o,o),e.lineTo(o,-o),e.moveTo(-o,-o),e.lineTo(o,o),e.stroke())}function y(e,t,o,i,r){d(e,o),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function X(e,t,o,i,r){y(e,t,o,i,r),P(e,t,o,i)}function H(e,t,o,i,r){const n=o*s,c=n/3,l=3*c/8;e.moveTo(-o,c),e.quadraticCurveTo(0,l,o,c),e.quadraticCurveTo(s*l/2,l/2,0,c-n),e.quadraticCurveTo(-s*l/2,l/2,-o,c),e.closePath(),r.doit&&(r.set_vectorize(e,t),e.fill()),i.doit&&(i.set_vectorize(e,t),e.stroke())}function Y(e,t,o,i,r){!function(e,t){e.moveTo(-t,0),e.lineTo(t,0)}(e,o),i.doit&&(i.set_vectorize(e,t),e.stroke())}function A(e,t,o,i,r){c(e,o),i.doit&&(i.set_vectorize(e,t),e.stroke())}function b(e,t,o,i,r){l(e,o),i.doit&&(i.set_vectorize(e,t),e.stroke())}function w(e,t,o){var i;const n=class extends r.MarkerView{static initClass(){this.prototype._render_one=t,this.prototype.glglyph_cls=o}};n.initClass();const s=((i=class extends r.Marker{static initClass(){this.prototype.default_view=n}}).__name__=e,i);return s.initClass(),s}o.Asterisk=w(\"Asterisk\",v,n.AsteriskGL),o.CircleCross=w(\"CircleCross\",f,n.CircleCrossGL),o.CircleDot=w(\"CircleDot\",T),o.CircleY=w(\"CircleY\",z),o.CircleX=w(\"CircleX\",C,n.CircleXGL),o.Cross=w(\"Cross\",k,n.CrossGL),o.Dash=w(\"Dash\",Y),o.Diamond=w(\"Diamond\",m,n.DiamondGL),o.DiamondCross=w(\"DiamondCross\",h,n.DiamondCrossGL),o.DiamondDot=w(\"DiamondDot\",q),o.Dot=w(\"Dot\",P),o.Hex=w(\"Hex\",D,n.HexGL),o.HexDot=w(\"HexDot\",g),o.InvertedTriangle=w(\"InvertedTriangle\",S,n.InvertedTriangleGL),o.Plus=w(\"Plus\",G),o.Square=w(\"Square\",L,n.SquareGL),o.SquareCross=w(\"SquareCross\",p,n.SquareCrossGL),o.SquareDot=w(\"SquareDot\",x),o.SquarePin=w(\"SquarePin\",M),o.SquareX=w(\"SquareX\",I,n.SquareXGL),o.Triangle=w(\"Triangle\",y,n.TriangleGL),o.TriangleDot=w(\"TriangleDot\",X),o.TrianglePin=w(\"TrianglePin\",H),o.X=w(\"X\",A,n.XGL),o.Y=w(\"Y\",b),o.marker_funcs={asterisk:v,circle:_,circle_cross:f,circle_dot:T,circle_y:z,circle_x:C,cross:k,diamond:m,diamond_dot:q,diamond_cross:h,dot:P,hex:D,hex_dot:g,inverted_triangle:S,plus:G,square:L,square_cross:p,square_dot:x,square_pin:M,square_x:I,triangle:y,triangle_dot:X,triangle_pin:H,dash:Y,x:A,y:b}},\n function _(e,s,i){Object.defineProperty(i,\"__esModule\",{value:!0});const t=e(1),n=e(93),r=e(28),a=t.__importStar(e(101)),_=t.__importStar(e(18)),h=e(9),l=e(88);class c extends n.XYGlyphView{initialize(){super.initialize();const{webgl:e}=this.renderer.plot_view.canvas_view;null!=e&&null!=this.glglyph_cls&&(this.glglyph=new this.glglyph_cls(e.gl,this))}_render(e,s,{sx:i,sy:t,_size:n,_angle:r}){for(const a of s){if(isNaN(i[a]+t[a]+n[a]+r[a]))continue;const s=n[a]/2;e.beginPath(),e.translate(i[a],t[a]),r[a]&&e.rotate(r[a]),this._render_one(e,a,s,this.visuals.line,this.visuals.fill),r[a]&&e.rotate(-r[a]),e.translate(-i[a],-t[a])}}_mask_data(){const e=this.renderer.plot_view.frame.bbox.h_range,s=e.start-this.max_size,i=e.end+this.max_size,[t,n]=this.renderer.xscale.r_invert(s,i),r=this.renderer.plot_view.frame.bbox.v_range,a=r.start-this.max_size,_=r.end+this.max_size,[h,l]=this.renderer.yscale.r_invert(a,_);return this.index.indices({x0:t,x1:n,y0:h,y1:l})}_hit_point(e){const{sx:s,sy:i}=e,t=s-this.max_size,n=s+this.max_size,[r,a]=this.renderer.xscale.r_invert(t,n),_=i-this.max_size,h=i+this.max_size,[c,o]=this.renderer.yscale.r_invert(_,h),x=this.index.indices({x0:r,x1:a,y0:c,y1:o}),d=[];for(const e of x){const t=this._size[e]/2;Math.abs(this.sx[e]-s)<=t&&Math.abs(this.sy[e]-i)<=t&&d.push(e)}return new l.Selection({indices:d})}_hit_span(e){const{sx:s,sy:i}=e,t=this.bounds(),n=this.max_size/2;let r,a,_,h;if(\"h\"==e.direction){_=t.y0,h=t.y1;const e=s-n,i=s+n;[r,a]=this.renderer.xscale.r_invert(e,i)}else{r=t.x0,a=t.x1;const e=i-n,s=i+n;[_,h]=this.renderer.yscale.r_invert(e,s)}const c=[...this.index.indices({x0:r,x1:a,y0:_,y1:h})];return new l.Selection({indices:c})}_hit_rect(e){const{sx0:s,sx1:i,sy0:t,sy1:n}=e,[r,a]=this.renderer.xscale.r_invert(s,i),[_,h]=this.renderer.yscale.r_invert(t,n),c=[...this.index.indices({x0:r,x1:a,y0:_,y1:h})];return new l.Selection({indices:c})}_hit_poly(e){const{sx:s,sy:i}=e,t=h.range(0,this.sx.length),n=[];for(let e=0,r=t.length;enew r.Range1d,y_range:()=>new r.Range1d})}initialize(){super.initialize(),this.use_map=!0,this.api_key||n.logger.error(\"api_key is required. See https://developers.google.com/maps/documentation/javascript/get-api-key for more information on how to obtain your own.\")}}i.GMapPlot=u,u.__name__=\"GMapPlot\",u.init_GMapPlot()},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=e(1),o=i.__importStar(e(28)),n=i.__importStar(e(18)),s=e(15),a=e(9),l=e(13),_=e(8),h=e(272),c=e(169),u=e(145),d=e(294),b=e(85),g=e(90),p=e(210),m=e(312);r.PlotView=m.PlotView;class f extends h.LayoutDOM{constructor(e){super(e)}static init_Plot(){this.prototype.default_view=m.PlotView,this.mixins([[\"outline_\",o.Line],[\"background_\",o.Fill],[\"border_\",o.Fill]]),this.define({toolbar:[n.Instance,()=>new d.Toolbar],toolbar_location:[n.Location,\"right\"],toolbar_sticky:[n.Boolean,!0],plot_width:[n.Number,600],plot_height:[n.Number,600],frame_width:[n.Number,null],frame_height:[n.Number,null],title:[n.Any,()=>new c.Title({text:\"\"})],title_location:[n.Location,\"above\"],above:[n.Array,[]],below:[n.Array,[]],left:[n.Array,[]],right:[n.Array,[]],center:[n.Array,[]],renderers:[n.Array,[]],x_range:[n.Instance,()=>new p.DataRange1d],extra_x_ranges:[n.Any,{}],y_range:[n.Instance,()=>new p.DataRange1d],extra_y_ranges:[n.Any,{}],x_scale:[n.Instance,()=>new u.LinearScale],y_scale:[n.Instance,()=>new u.LinearScale],lod_factor:[n.Number,10],lod_interval:[n.Number,300],lod_threshold:[n.Number,2e3],lod_timeout:[n.Number,500],hidpi:[n.Boolean,!0],output_backend:[n.OutputBackend,\"canvas\"],min_border:[n.Number,5],min_border_top:[n.Number,null],min_border_left:[n.Number,null],min_border_bottom:[n.Number,null],min_border_right:[n.Number,null],inner_width:[n.Number],inner_height:[n.Number],outer_width:[n.Number],outer_height:[n.Number],match_aspect:[n.Boolean,!1],aspect_scale:[n.Number,1],reset_policy:[n.ResetPolicy,\"standard\"]}),this.override({outline_line_color:\"#e5e5e5\",border_fill_color:\"#ffffff\",background_fill_color:\"#ffffff\"})}get width(){const e=this.properties.width.get_value();return null!=e?e:this.plot_width}set width(e){this.setv({width:e,plot_width:e})}get height(){const e=this.properties.height.get_value();return null!=e?e:this.plot_height}set height(e){this.setv({height:e,plot_height:e})}_doc_attached(){super._doc_attached(),this._push_changes([[this.properties.inner_height,null,this.inner_height],[this.properties.inner_width,null,this.inner_width]])}initialize(){super.initialize(),this.reset=new s.Signal0(this,\"reset\");for(const e of l.values(this.extra_x_ranges).concat(this.x_range)){let t=e.plots;_.isArray(t)&&(t=t.concat(this),e.setv({plots:t},{silent:!0}))}for(const e of l.values(this.extra_y_ranges).concat(this.y_range)){let t=e.plots;_.isArray(t)&&(t=t.concat(this),e.setv({plots:t},{silent:!0}))}}add_layout(e,t=\"center\"){const r=this.properties[t].get_value();this.setv({[t]:[...r,e]})}remove_layout(e){const t=t=>{a.remove_by(t,t=>t==e)};t(this.left),t(this.right),t(this.above),t(this.below),t(this.center)}add_renderers(...e){this.renderers=this.renderers.concat(e)}add_glyph(e,t=new b.ColumnDataSource,r={}){const i=Object.assign(Object.assign({},r),{data_source:t,glyph:e}),o=new g.GlyphRenderer(i);return this.add_renderers(o),o}add_tools(...e){this.toolbar.tools=this.toolbar.tools.concat(e)}get panels(){return[...this.side_panels,...this.center]}get side_panels(){const{above:e,below:t,left:r,right:i}=this;return a.concat([e,t,r,i])}}r.Plot=f,f.__name__=\"Plot\",f.init_Plot()},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1).__importStar(t(18)),c=t(8),o=t(9),n=t(13),a=t(295),l=t(305),r=t=>{switch(t){case\"tap\":return\"active_tap\";case\"pan\":return\"active_drag\";case\"pinch\":case\"scroll\":return\"active_scroll\";case\"multi\":return\"active_multi\"}return null},_=t=>\"tap\"==t||\"pan\"==t;class h extends l.ToolbarBase{constructor(t){super(t)}static init_Toolbar(){this.prototype.default_view=l.ToolbarBaseView,this.define({active_drag:[s.Any,\"auto\"],active_inspect:[s.Any,\"auto\"],active_scroll:[s.Any,\"auto\"],active_tap:[s.Any,\"auto\"],active_multi:[s.Any,null]})}connect_signals(){super.connect_signals();const{tools:t,active_drag:e,active_inspect:i,active_scroll:s,active_tap:c,active_multi:o}=this.properties;this.on_change([t,e,i,s,c,o],()=>this._init_tools())}_init_tools(){if(super._init_tools(),\"auto\"==this.active_inspect);else if(this.active_inspect instanceof a.InspectTool){let t=!1;for(const e of this.inspectors)e!=this.active_inspect?e.active=!1:t=!0;t||(this.active_inspect=null)}else if(c.isArray(this.active_inspect)){const t=o.intersection(this.active_inspect,this.inspectors);t.length!=this.active_inspect.length&&(this.active_inspect=t);for(const t of this.inspectors)o.includes(this.active_inspect,t)||(t.active=!1)}else if(null==this.active_inspect)for(const t of this.inspectors)t.active=!1;const t=t=>{t.active?this._active_change(t):t.active=!0};for(const t of n.values(this.gestures)){t.tools=o.sort_by(t.tools,t=>t.default_order);for(const e of t.tools)this.connect(e.properties.active.change,()=>this._active_change(e))}for(const[e,i]of n.entries(this.gestures)){const s=r(e);if(s){const c=this[s];\"auto\"==c?0!=i.tools.length&&_(e)&&t(i.tools[0]):null!=c&&(o.includes(this.tools,c)?t(c):this[s]=null)}}}}i.Toolbar=h,h.__name__=\"Toolbar\",h.init_Toolbar()},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const n=e(1),s=e(296),i=e(304),_=n.__importStar(e(18));class c extends s.ButtonToolView{}o.InspectToolView=c,c.__name__=\"InspectToolView\";class l extends s.ButtonTool{constructor(e){super(e),this.event_type=\"move\"}static init_InspectTool(){this.prototype.button_view=i.OnOffButtonView,this.define({toggleable:[_.Boolean,!0]}),this.override({active:!0})}}o.InspectTool=l,l.__name__=\"InspectTool\",l.init_InspectTool()},\n function _(t,e,o){Object.defineProperty(o,\"__esModule\",{value:!0});const i=t(1),s=i.__importDefault(t(297)),n=t(78),l=t(298),r=t(72),a=i.__importStar(t(18)),u=t(29),_=t(8),h=t(9),c=t(299),m=i.__importDefault(t(300)),d=i.__importDefault(t(301)),p=i.__importDefault(t(284)),f=t(302);class g extends n.DOMView{initialize(){super.initialize();const t=this.model.menu;if(null!=t){const e=this.parent.model.toolbar_location,o=\"left\"==e||\"above\"==e,i=this.parent.model.horizontal?\"vertical\":\"horizontal\";this._menu=new f.ContextMenu(o?h.reversed(t):t,{orientation:i,prevent_hide:t=>t.target==this.el})}this._hammer=new s.default(this.el,{touchAction:\"auto\",inputClass:s.default.TouchMouseInput}),this.connect(this.model.change,()=>this.render()),this._hammer.on(\"tap\",t=>{var e;(null===(e=this._menu)||void 0===e?void 0:e.is_open)?this._menu.hide():t.target==this.el&&this._clicked()}),this._hammer.on(\"press\",()=>this._pressed())}remove(){var t;this._hammer.destroy(),null===(t=this._menu)||void 0===t||t.remove(),super.remove()}styles(){return[...super.styles(),m.default,d.default,p.default]}css_classes(){return super.css_classes().concat(c.bk_toolbar_button)}render(){r.empty(this.el);const t=this.model.computed_icon;_.isString(t)&&(u.startsWith(t,\"data:image\")?this.el.style.backgroundImage=\"url('\"+t+\"')\":this.el.classList.add(t)),this.el.title=this.model.tooltip,null!=this._menu&&this.root.el.appendChild(this._menu.el)}_pressed(){var t;const{left:e,top:o,right:i,bottom:s}=this.el.getBoundingClientRect(),n=(()=>{switch(this.parent.model.toolbar_location){case\"right\":return{right:e,top:o};case\"left\":return{left:i,top:o};case\"above\":return{left:e,top:s};case\"below\":return{left:e,bottom:o}}})();null===(t=this._menu)||void 0===t||t.toggle(n)}}o.ButtonToolButtonView=g,g.__name__=\"ButtonToolButtonView\";class v extends l.ToolView{}o.ButtonToolView=v,v.__name__=\"ButtonToolView\";class b extends l.Tool{constructor(t){super(t)}static init_ButtonTool(){this.internal({disabled:[a.Boolean,!1]})}get tooltip(){return this.tool_name}get computed_icon(){return this.icon}get menu(){return null}}o.ButtonTool=b,b.__name__=\"ButtonTool\",b.init_ButtonTool()},\n function _(t,e,n){\n /*! Hammer.JS - v2.0.7 - 2016-04-22\n * http://hammerjs.github.io/\n *\n * Copyright (c) 2016 Jorik Tangelder;\n * Licensed under the MIT license */\n !function(t,n,i,r){\"use strict\";var s,o=[\"\",\"webkit\",\"Moz\",\"MS\",\"ms\",\"o\"],a=n.createElement(\"div\"),h=Math.round,u=Math.abs,c=Date.now;function l(t,e,n){return setTimeout(y(t,n),e)}function p(t,e,n){return!!Array.isArray(t)&&(f(t,n[e],n),!0)}function f(t,e,n){var i;if(t)if(t.forEach)t.forEach(e,n);else if(void 0!==t.length)for(i=0;i\\s*\\(/gm,\"{anonymous}()@\"):\"Unknown Stack Trace\",s=t.console&&(t.console.warn||t.console.log);return s&&s.call(t.console,r,i),e.apply(this,arguments)}}s=\"function\"!=typeof Object.assign?function(t){if(null==t)throw new TypeError(\"Cannot convert undefined or null to object\");for(var e=Object(t),n=1;n-1}function S(t){return t.trim().split(/\\s+/g)}function b(t,e,n){if(t.indexOf&&!n)return t.indexOf(e);for(var i=0;in[e]})):i.sort()),i}function D(t,e){for(var n,i,r=e[0].toUpperCase()+e.slice(1),s=0;s1&&!n.firstMultiple?n.firstMultiple=W(e):1===r&&(n.firstMultiple=!1);var s=n.firstInput,o=n.firstMultiple,a=o?o.center:s.center,h=e.center=q(i);e.timeStamp=c(),e.deltaTime=e.timeStamp-s.timeStamp,e.angle=U(a,h),e.distance=L(a,h),function(t,e){var n=e.center,i=t.offsetDelta||{},r=t.prevDelta||{},s=t.prevInput||{};1!==e.eventType&&4!==s.eventType||(r=t.prevDelta={x:s.deltaX||0,y:s.deltaY||0},i=t.offsetDelta={x:n.x,y:n.y});e.deltaX=r.x+(n.x-i.x),e.deltaY=r.y+(n.y-i.y)}(n,e),e.offsetDirection=H(e.deltaX,e.deltaY);var l=k(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=l.x,e.overallVelocityY=l.y,e.overallVelocity=u(l.x)>u(l.y)?l.x:l.y,e.scale=o?(p=o.pointers,f=i,L(f[0],f[1],X)/L(p[0],p[1],X)):1,e.rotation=o?function(t,e){return U(e[1],e[0],X)+U(t[1],t[0],X)}(o.pointers,i):0,e.maxPointers=n.prevInput?e.pointers.length>n.prevInput.maxPointers?e.pointers.length:n.prevInput.maxPointers:e.pointers.length,function(t,e){var n,i,r,s,o=t.lastInterval||e,a=e.timeStamp-o.timeStamp;if(8!=e.eventType&&(a>25||void 0===o.velocity)){var h=e.deltaX-o.deltaX,c=e.deltaY-o.deltaY,l=k(a,h,c);i=l.x,r=l.y,n=u(l.x)>u(l.y)?l.x:l.y,s=H(h,c),t.lastInterval=e}else n=o.velocity,i=o.velocityX,r=o.velocityY,s=o.direction;e.velocity=n,e.velocityX=i,e.velocityY=r,e.direction=s}(n,e);var p,f;var v=t.element;_(e.srcEvent.target,v)&&(v=e.srcEvent.target);e.target=v}(t,n),t.emit(\"hammer.input\",n),t.recognize(n),t.session.prevInput=n}function W(t){for(var e=[],n=0;n=u(e)?t<0?2:4:e<0?8:16}function L(t,e,n){n||(n=N);var i=e[n[0]]-t[n[0]],r=e[n[1]]-t[n[1]];return Math.sqrt(i*i+r*r)}function U(t,e,n){n||(n=N);var i=e[n[0]]-t[n[0]],r=e[n[1]]-t[n[1]];return 180*Math.atan2(r,i)/Math.PI}Y.prototype={handler:function(){},init:function(){this.evEl&&I(this.element,this.evEl,this.domHandler),this.evTarget&&I(this.target,this.evTarget,this.domHandler),this.evWin&&I(O(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&A(this.element,this.evEl,this.domHandler),this.evTarget&&A(this.target,this.evTarget,this.domHandler),this.evWin&&A(O(this.element),this.evWin,this.domHandler)}};var V={mousedown:1,mousemove:2,mouseup:4};function j(){this.evEl=\"mousedown\",this.evWin=\"mousemove mouseup\",this.pressed=!1,Y.apply(this,arguments)}g(j,Y,{handler:function(t){var e=V[t.type];1&e&&0===t.button&&(this.pressed=!0),2&e&&1!==t.which&&(e=4),this.pressed&&(4&e&&(this.pressed=!1),this.callback(this.manager,e,{pointers:[t],changedPointers:[t],pointerType:\"mouse\",srcEvent:t}))}});var G={pointerdown:1,pointermove:2,pointerup:4,pointercancel:8,pointerout:8},Z={2:\"touch\",3:\"pen\",4:\"mouse\",5:\"kinect\"},B=\"pointerdown\",$=\"pointermove pointerup pointercancel\";function J(){this.evEl=B,this.evWin=$,Y.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}t.MSPointerEvent&&!t.PointerEvent&&(B=\"MSPointerDown\",$=\"MSPointerMove MSPointerUp MSPointerCancel\"),g(J,Y,{handler:function(t){var e=this.store,n=!1,i=t.type.toLowerCase().replace(\"ms\",\"\"),r=G[i],s=Z[t.pointerType]||t.pointerType,o=\"touch\"==s,a=b(e,t.pointerId,\"pointerId\");1&r&&(0===t.button||o)?a<0&&(e.push(t),a=e.length-1):12&r&&(n=!0),a<0||(e[a]=t,this.callback(this.manager,r,{pointers:e,changedPointers:[t],pointerType:s,srcEvent:t}),n&&e.splice(a,1))}});var K={touchstart:1,touchmove:2,touchend:4,touchcancel:8};function Q(){this.evTarget=\"touchstart\",this.evWin=\"touchstart touchmove touchend touchcancel\",this.started=!1,Y.apply(this,arguments)}function tt(t,e){var n=x(t.touches),i=x(t.changedTouches);return 12&e&&(n=P(n.concat(i),\"identifier\",!0)),[n,i]}g(Q,Y,{handler:function(t){var e=K[t.type];if(1===e&&(this.started=!0),this.started){var n=tt.call(this,t,e);12&e&&n[0].length-n[1].length==0&&(this.started=!1),this.callback(this.manager,e,{pointers:n[0],changedPointers:n[1],pointerType:\"touch\",srcEvent:t})}}});var et={touchstart:1,touchmove:2,touchend:4,touchcancel:8};function nt(){this.evTarget=\"touchstart touchmove touchend touchcancel\",this.targetIds={},Y.apply(this,arguments)}function it(t,e){var n=x(t.touches),i=this.targetIds;if(3&e&&1===n.length)return i[n[0].identifier]=!0,[n,n];var r,s,o=x(t.changedTouches),a=[],h=this.target;if(s=n.filter((function(t){return _(t.target,h)})),1===e)for(r=0;r-1&&i.splice(t,1)}),2500)}}function at(t){for(var e=t.srcEvent.clientX,n=t.srcEvent.clientY,i=0;i-1&&this.requireFail.splice(e,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(t){return!!this.simultaneous[t.id]},emit:function(t){var e=this,n=this.state;function i(n){e.manager.emit(n,t)}n<8&&i(e.options.event+ft(n)),i(e.options.event),t.additionalEvent&&i(t.additionalEvent),n>=8&&i(e.options.event+ft(n))},tryEmit:function(t){if(this.canEmit())return this.emit(t);this.state=32},canEmit:function(){for(var t=0;te.threshold&&r&e.direction},attrTest:function(t){return mt.prototype.attrTest.call(this,t)&&(2&this.state||!(2&this.state)&&this.directionTest(t))},emit:function(t){this.pX=t.deltaX,this.pY=t.deltaY;var e=vt(t.direction);e&&(t.additionalEvent=this.options.event+e),this._super.emit.call(this,t)}}),g(yt,mt,{defaults:{event:\"pinch\",threshold:0,pointers:2},getTouchAction:function(){return[\"none\"]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.scale-1)>this.options.threshold||2&this.state)},emit:function(t){if(1!==t.scale){var e=t.scale<1?\"in\":\"out\";t.additionalEvent=this.options.event+e}this._super.emit.call(this,t)}}),g(Tt,pt,{defaults:{event:\"press\",pointers:1,time:251,threshold:9},getTouchAction:function(){return[\"auto\"]},process:function(t){var e=this.options,n=t.pointers.length===e.pointers,i=t.distancee.time;if(this._input=t,!i||!n||12&t.eventType&&!r)this.reset();else if(1&t.eventType)this.reset(),this._timer=l((function(){this.state=8,this.tryEmit()}),e.time,this);else if(4&t.eventType)return 8;return 32},reset:function(){clearTimeout(this._timer)},emit:function(t){8===this.state&&(t&&4&t.eventType?this.manager.emit(this.options.event+\"up\",t):(this._input.timeStamp=c(),this.manager.emit(this.options.event,this._input)))}}),g(Et,mt,{defaults:{event:\"rotate\",threshold:0,pointers:2},getTouchAction:function(){return[\"none\"]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.rotation)>this.options.threshold||2&this.state)}}),g(It,mt,{defaults:{event:\"swipe\",threshold:10,velocity:.3,direction:30,pointers:1},getTouchAction:function(){return gt.prototype.getTouchAction.call(this)},attrTest:function(t){var e,n=this.options.direction;return 30&n?e=t.overallVelocity:6&n?e=t.overallVelocityX:24&n&&(e=t.overallVelocityY),this._super.attrTest.call(this,t)&&n&t.offsetDirection&&t.distance>this.options.threshold&&t.maxPointers==this.options.pointers&&u(e)>this.options.velocity&&4&t.eventType},emit:function(t){var e=vt(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)}}),g(At,pt,{defaults:{event:\"tap\",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[\"manipulation\"]},process:function(t){var e=this.options,n=t.pointers.length===e.pointers,i=t.distance{this.model.active?this.activate():this.deactivate()})}activate(){}deactivate(){}}i.ToolView=r,r.__name__=\"ToolView\";class _ extends a.Model{constructor(t){super(t)}static init_Tool(){this.prototype._known_aliases=new Map,this.internal({active:[n.Boolean,!1]})}get synthetic_renderers(){return[]}_get_dim_tooltip(t,e){switch(e){case\"width\":return t+\" (x-axis)\";case\"height\":return t+\" (y-axis)\";case\"both\":return t}}_get_dim_limits([t,e],[i,n],o,a){const r=o.bbox.h_range;let _;\"width\"==a||\"both\"==a?(_=[s.min([t,i]),s.max([t,i])],_=[s.max([_[0],r.start]),s.min([_[1],r.end])]):_=[r.start,r.end];const l=o.bbox.v_range;let c;return\"height\"==a||\"both\"==a?(c=[s.min([e,n]),s.max([e,n])],c=[s.max([c[0],l.start]),s.min([c[1],l.end])]):c=[l.start,l.end],[_,c]}static register_alias(t,e){this.prototype._known_aliases.set(t,e)}static from_string(t){const e=this.prototype._known_aliases.get(t);if(null!=e)return e();{const e=[...this.prototype._known_aliases.keys()];throw new Error(`unexpected tool name '${t}', possible tools are ${e.join(\", \")}`)}}}i.Tool=_,_.__name__=\"Tool\",_.init_Tool()},\n function _(o,b,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.bk_toolbar=\"bk-toolbar\",t.bk_toolbar_hidden=\"bk-toolbar-hidden\",t.bk_toolbar_button=\"bk-toolbar-button\",t.bk_button_bar=\"bk-button-bar\",t.bk_toolbar_button_custom_action=\"bk-toolbar-button-custom-action\"},\n function _(o,b,t){Object.defineProperty(t,\"__esModule\",{value:!0});t.default='\\n.bk-root .bk-toolbar-hidden {\\n visibility: hidden;\\n opacity: 0;\\n transition: visibility 0.3s linear, opacity 0.3s linear;\\n}\\n.bk-root .bk-toolbar,\\n.bk-root .bk-button-bar {\\n display: flex;\\n display: -webkit-flex;\\n flex-wrap: nowrap;\\n -webkit-flex-wrap: nowrap;\\n align-items: center;\\n -webkit-align-items: center;\\n user-select: none;\\n -ms-user-select: none;\\n -moz-user-select: none;\\n -webkit-user-select: none;\\n}\\n.bk-root .bk-toolbar .bk-logo {\\n flex-shrink: 0;\\n -webkit-flex-shrink: 0;\\n}\\n.bk-root .bk-toolbar.bk-above,\\n.bk-root .bk-toolbar.bk-below {\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n justify-content: flex-end;\\n -webkit-justify-content: flex-end;\\n}\\n.bk-root .bk-toolbar.bk-above .bk-button-bar,\\n.bk-root .bk-toolbar.bk-below .bk-button-bar {\\n display: flex;\\n display: -webkit-flex;\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n}\\n.bk-root .bk-toolbar.bk-above .bk-logo,\\n.bk-root .bk-toolbar.bk-below .bk-logo {\\n order: 1;\\n -webkit-order: 1;\\n margin-left: 5px;\\n margin-right: 0px;\\n}\\n.bk-root .bk-toolbar.bk-left,\\n.bk-root .bk-toolbar.bk-right {\\n flex-direction: column;\\n -webkit-flex-direction: column;\\n justify-content: flex-start;\\n -webkit-justify-content: flex-start;\\n}\\n.bk-root .bk-toolbar.bk-left .bk-button-bar,\\n.bk-root .bk-toolbar.bk-right .bk-button-bar {\\n display: flex;\\n display: -webkit-flex;\\n flex-direction: column;\\n -webkit-flex-direction: column;\\n}\\n.bk-root .bk-toolbar.bk-left .bk-logo,\\n.bk-root .bk-toolbar.bk-right .bk-logo {\\n order: 0;\\n -webkit-order: 0;\\n margin-bottom: 5px;\\n margin-top: 0px;\\n}\\n.bk-root .bk-toolbar-button {\\n width: 30px;\\n height: 30px;\\n cursor: pointer;\\n background-size: 60% 60%;\\n background-origin: border-box;\\n background-color: transparent;\\n background-repeat: no-repeat;\\n background-position: center center;\\n}\\n.bk-root .bk-toolbar-button:hover {\\n background-color: rgba(192, 192, 192, 0.15);\\n}\\n.bk-root .bk-toolbar-button:focus {\\n outline: none;\\n}\\n.bk-root .bk-toolbar-button::-moz-focus-inner {\\n border: 0;\\n}\\n.bk-root .bk-toolbar.bk-above .bk-toolbar-button {\\n border-bottom: 2px solid transparent;\\n}\\n.bk-root .bk-toolbar.bk-above .bk-toolbar-button.bk-active {\\n border-bottom-color: #26aae1;\\n}\\n.bk-root .bk-toolbar.bk-below .bk-toolbar-button {\\n border-top: 2px solid transparent;\\n}\\n.bk-root .bk-toolbar.bk-below .bk-toolbar-button.bk-active {\\n border-top-color: #26aae1;\\n}\\n.bk-root .bk-toolbar.bk-right .bk-toolbar-button {\\n border-left: 2px solid transparent;\\n}\\n.bk-root .bk-toolbar.bk-right .bk-toolbar-button.bk-active {\\n border-left-color: #26aae1;\\n}\\n.bk-root .bk-toolbar.bk-left .bk-toolbar-button {\\n border-right: 2px solid transparent;\\n}\\n.bk-root .bk-toolbar.bk-left .bk-toolbar-button.bk-active {\\n border-right-color: #26aae1;\\n}\\n.bk-root .bk-button-bar + .bk-button-bar:before {\\n content: \" \";\\n display: inline-block;\\n background-color: lightgray;\\n}\\n.bk-root .bk-toolbar.bk-above .bk-button-bar + .bk-button-bar:before,\\n.bk-root .bk-toolbar.bk-below .bk-button-bar + .bk-button-bar:before {\\n height: 10px;\\n width: 1px;\\n}\\n.bk-root .bk-toolbar.bk-left .bk-button-bar + .bk-button-bar:before,\\n.bk-root .bk-toolbar.bk-right .bk-button-bar + .bk-button-bar:before {\\n height: 1px;\\n width: 10px;\\n}\\n'},\n function _(A,g,C){Object.defineProperty(C,\"__esModule\",{value:!0});C.default='\\n.bk-root .bk-tool-icon-copy-to-clipboard {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AUSDBoBvcHQeQAAAG9JREFUWMNjXLhsJcNAAiaGAQYwB/xHwh/Q+ITEkfHQCwEWND4jmeb8H/JpgBwfI6cNBhLSEkqaGXRpgFRAcZoZsmlg1AGjDhh1wKgDRh0w6gCaVcf/R2wIkNqw+D9s0wADvUNiyIYA47BJAwPuAAAj/Cjd0TCN6wAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-replace-mode {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AUFFxokK3gniQAAAHpJREFUWMNjXLhsJcNAAiaGAQajDhhwB7DgEP+PxmeksvjgDwFcLmYkUh2hkBj8IcBIZXsYh1w2/I8v3sgAOM0bLYhGc8GgrwuICgldfQO88pcvXvg/aOuCUQeM5oLRuoCFCJcTbOMh5XOiW0JDNhdQS3y0IBp1ABwAAF8KGrhC1Eg6AAAAAElFTkSuQmCC\");\\n}\\n.bk-root .bk-tool-icon-append-mode {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AUFFxkZWD04WwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAoUlEQVRYw+1WQQ6AIAwrhO8Y/bIXEz9jIMSDr8ETCUEPQzA4pMeFLKNbu4l5WR0CDOMEALBGIzMuQIBEZQjPgP9JLjwTfBjY9sO9lZsFA9IafZng3BlIyVefgd8XQFZBAWe8jfNxwsDhir6rzoCiPiy1K+J8/FRQemv2XfAdFcQ9znU4Viqg9ta1qYJ+D1BnAIBrkgGVOrXNqUA9rbyZm/AEzFh4jEeY/soAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-intersect-mode {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AUFFxkrkOpp2wAAAPhJREFUWMPtV1EKwjAMTUavI3oawR/vtn5srJdREfzwMvHHQlcT2mpdMzFfWxiP5r2+JMN+mAiCOB72CABgR1cln4oOGocJnuMTSxWk8jMm7OggYkYXA9gPE3uyd8NXHONJ+eYMdE/NqCJmEZ5ZqlJJ4sUksKN7cYSaPoCZFWR1QI+Xm1fBACU63Cw22x0AAJxudwrffVwvZ+JmQdAHZkw0d4EpAMCw8k87pMdbnwtizQumJYv3nwV6XOA1qbUT/oQLUJgFRbsiNwFVucBIlyR3p0tdMp+XmFjfLKi1LatyAXtCRjPWBdL3Ke3VuACJKFfDr/xFN2fgAR/Go0qaLlmEAAAAAElFTkSuQmCC\");\\n}\\n.bk-root .bk-tool-icon-subtract-mode {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AUFFxgsF5XNOQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAABFUlEQVRYw9VWUQqDMAxNpWfxQxD1MoP97G7zQ5mH2RTZYLtM9lWoMbXtxLXNX4OG9r28l4hrd0PQoqxqAACYpxH25C/nkwCHyCBwSPoS09k1T5Fo+4EiExcC4v584xGFmyIXHBLRISAVZyZufUPVa4rcrwmPDgr93ylo+2GliLRUYHK6th/o/6r7nfLpqaCsagEA8Hh9FmcNKeRmgeYDC+SCq0B6FFi8/BcV6BdR9cL3gCv3ijPKOacsn3rBEcjmaVxpfGcg4wHxzgJJnc6241Hn23DERFRAu1bNcWa3Q0uXi62XR6sCaWoSejbtdLYmU3kTEunNgj0bUbQqYG/IcMaqwPS9jftoVCAQ0ZVDJwf0zQdH4AsyW6fpQu4YegAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-clear-selection {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AUGEhcuan3d3wAAAoRJREFUWMPtlzFP3EAQhd+b3TNSzg0N5TWXLkJQUUaKhIQ4fgP/g5ArrriE/I3opEgRrZtIVJR0FJQ010SioUmEZHtnUpwN9gWHGA5BJCy58MraffvmfZ41v3z9hqe8BE98vQh4cgG+Ydzmnrng8efvQJNi/uN7dznx/B3ggtfhf4ehNdUttRzBDIm/2VTiiWCG1HK0nc+3UWtq8BQIiEEakEQOADBIA4QCQmBqoHBhFNR27ikQSmGdYCdTqCpEHMDZmEKRWUBEv1gBDg5SzRJnpopILWICgWuRYflLamuzxB2BmtYqSRIka5VWU8QduXO+1hRc5YZu5GAwmP2ZJzND0IBu5HCV2+NQcAhAVRsnC2IbPzPdSjzd6to6VtfWkXi6YLaVWr7xoAwkfpb8MnC3SH7rKSMBe4M0jA/OTicFIbtCGRIyNbURhcf3ErCd6YwA1m0HgAxhw1NGQnlXBHG4kylVlSJuH0RfIP2CkL2I/qS1gIAAQiBl1QwFggIHtyxgrxK5PgyfC0JWKoT0HLh8LwoietB4TYKaIl7yeNURxB05UtMxDOcVQlZIrlRKdK6m47gjR/fuBRQihyLArtNeJD50Izcx2Eczu7iFkIug4VM3cpOr3MKDekFED0fWUHv9Zq0kpLnridjhY3XDg7NTN0jDrhO3X7O9Wg7wwyANu4mnayNg3gmbu0tCNoUyBNGv2l4rB9EXynA7082FOxAQLhU6rQVO9T2AvWowFToNCJcPORGxIRcnpjZSKATSU9NxvOQnAPArDSaQoUKnNI4iufkGtD4P3EHIcWZhz4HLceSOyrR3Izf5memPAL2cX3yhAkonysZVaWLBkd9dw1Ivv2a/AYPkK+ty1U1DAAAAAElFTkSuQmCC\");\\n}\\n.bk-root .bk-tool-icon-box-select {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg0kduFrowAAAIdJREFUWMPtVtEKwCAI9KL//4e9DPZ3+wP3KgOjNZouFYI4C8q7s7DtB1lGIeMoRMRinCLXg/ML3EcFqpjjloOyZxRntxpwQ8HsgHYARKFAtSFrCg3TCdMFCE1BuuALEXJLjC4qENsFVXCESZw38/kWLOkC/K4PcOc/Hj03WkoDT3EaWW9egQul6CUbq90JTwAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-box-zoom {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg82t254aQAAAkBJREFUWMPN11+E1FEUB/DPTFn2qaeIpcSwr5NlUyJiKWVXWUqvlUh/iE3RY9mUekkPPURtLKNRrFJEeuphGfUUaVliiX1aVjGs6aG7+XX9ZnZ+d2fTl2vmnHvPPfeee/79Sk+may2/UQq/q7Qu+bAJoxjHIKqB/wlfUMcMVqI9bLZ+DGIKwzlzQ2GcxCx2xwvKOUKlaHTiX8bHNspjDONHkOmJBW5jIof/FvPh/06MZOb6cRc7cGn1AKUE5cdzlM/gAr5F/O24H3xkFRfxAbVygvK+cIsspjGWo1zgjeFpxL+BvnLw7laBA4xjIFJwrgu52DoVjKdY4HBEX8dSF3JLYe1fe6UcYCii3xWQjdfuSTnAtoheKCC7GNED5Zx4L4qt61jbTLHA94geKSC7P7ZeShQ0Inoi1IJuEOeORooFXkV0FZNdZs5qvFfKAeqYy7nZ6yg//HG0MBfffh71lFrQDCW2EvEP4mt4okZUDftz9rmGZkotmMxJRtlisy+MTniAWrty3AlXw0hFM2TD89l+oNsoOJXjbIs4EpqNtTCLXbiZ0g+M4mFObj8U3vsNjoZCVcmk60ZwthpepLZkB/AsivWfOJZxtpUQHfWib7KWDwzjeegBZJSdKFiE2qJTFFTwElsi/unQ/awXrU4WGMD7nOJxBY/1EO2iYConq93CHT1GOwucjdqnRyFz+VcHmMNefMY9nNkA3SWUOoXhQviSWQ4huLIRFlirFixnQq/XaKXUgg2xQNGv4V7x/RcW+AXPB3h7H1PaiQAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-zoom-in {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEgsUBmL8iQAAA2JJREFUWMO9l12IlFUYx3//MzPrLpSjkm5oN4FFIWVEl66IQlFYwtLOzozsjHdGRSCRF0sfBEXRVV0FQuQiLm5CZNBFgRRaRLVFhbJ2EdiN5gbK7toObTPn6eYdPTvNzPvOBz5Xh/ec5/n/n89zXtEHmZqeSXSuXBz/3zfdKvBWJHQrwZuRcP0El+QkbQXeBX6WZEgm6TtJk5lM5o4Lc+cV6qpf4Ga20Tm338zeATItVK9Ker6yvPzp4NDQ3+XieGsCU9MzTYumGbhz7m4ze9/MHgvBgItACrgfGAj2jgAvAYs3wlEujjc13kii8YyZrXXOfWhmo9GnFUlvOOemarVapVqtkslksmb2KjARqL62ecuWN9NxbRInzrldAXhV0uFSIfdew7G/gNLU9MwS8CwSmE3Oz88fcXG5blfpqVRq0Ix8VIAAX0XgrVL7HDCHGcCaWrV60LUBN8Dae58aQIxEqcA592I9M610JL0cpG/U9TIHJNKY3RV5z0R+7Nd4HZ0P1g/2RMBuegLAsRMnb4vT8d5vqKfMzOgtAlADrkmqGywmiMBTwfr3dC9j1Xv/r6Tvg/5/5ejxE6cO7M9faVbQZrYNOFSPmqQvVo9FKexvi5uWX58943aM7DwAfBDY+FbSCxP5sdkGx55GeguzrUEXPaSo2pFkAbiSZQCAzZJOmdkjwd6SpB/M7KykQTPbA2wDhoIzRzcNDx9MJwGNIXdJ0mEzmwbujL7dbma7gd03A7lKfnTOvf74nl0r6bonTUbujRSUCrm2d4L3/kvn3JPe+8+BDW2i9o+kT7z3kxP5sYsA6W47oE64TsR7P9tQL4vA2mh9WdIscKxUyJ0M7aR7acOGzikD65EQLEjaa2ZXzMwDFeB6qZBbbLTRE4EGeSaozNOZgYFf8qP7lmIvs354n0qlHpB0T7B9Ogl4IgJJrmjv/SiQjbrkD+BMUkfSbYATPdckrTOzkciWAXOlQu5cYgLdPEIapud9wMOR9zVJH3ViKx333mtHMJvNuoWFhZ3A+ojMcja77njXBEKwJJfTcqUyCIQ34Mf7nnh0paMnXacFuGoC1mr3AtuDfLzd8Zuyl+rfuGn4HLAD+Az4qZQf+61TAj0Noj8vX6oC35SL43u7teG6rf5+iXppwW7/JUL5D03qaFRvvUe+AAAAAElFTkSuQmCC\");\\n}\\n.bk-root .bk-tool-icon-zoom-out {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEgsHgty9VwAAA0FJREFUWMO9l09oXFUUxn/fmXlpItppi22k7UJBRSlVkCytSAuKUloIdjKT0El3FXVXdVFKRVAQV7qQohsNwdA0UFvBhYtqUVyIVlRaogtFQVq7qSTVjA3z3nHzBq/jvPmTN/Ss7rv3nvN99/y794kByMzcfE/7picn/jenmwWeRUI3E7wdCRskuCSTdDfwBvCtJEdySV9KOhpF0e0/LF5SqKtBgbv7ZjObcvfXgShD9Zqk5+orKx8Oj4z8NT05kU1gZm6+bdK0Azezu9z9hLs/HoIBvwAF4H5gKFh7B3gBWFY3460kWve4+3oze9fdx9OpVUmvmNlMHMf1RqNBFEUldz8OHAxUX9q6bduryut+Sfvc/Wz62ZD0fK1afjND9y3gGSRwv1GMojstTxUUCoVhdyopEYDzKXjWwZ4FFnEHWBc3Goet00m7lZlZYQixKw0FZnakGZksHUnHgvCN5/KARBH37enpOVg58H13HV0Kxg/kIuD/ngSA2ZMLt3bTSZJkUzNk7k4+D0AM/CGpaXCyBw/sC8Y/qZd2GpZiuL9YLN4Sx/HpoP5/c/exQ1OVq+1yyt13SLoArEsJnMjlgfOffvK3u58Kprab2QezJxfG2iTzUzI70wRPG9jbmpmb95SNB9mpzp7/j2yVdNbdx4K565K+cvfPJQ27+x5gBzAS7Hlvy+jo4WIvoC3kWpcvS3rR3eeAO9K529x9N7C7zX6AC2b28hN7Hl1Vt44niVq13LUjmtlYkiQfA5s6eO+GpDNJkhw9NFX5ueNt2ARodyF1IHIN2JiOl4H16fiKpK+B2Vq1vBAqFAf4IJkGNiIhWJK0192vunsC1IE/a9XycquNXARa5OnApeeioaHvKuP7r3dTGsiLqFAo7JR0T7B8rhfwXARa2us4UEqr5Ffgs151i/08oTNKdIO770ptObBYq5Yv5ibQq/sl3Qc8lJ4+lnSqH1vFfp9koZRKJVtaWnqkWXqSVkqlDe+vmUDWpZMlK/X6MBDegKf3P/nYaj8ErN9fqZBYEsf3Ag8G8Xit33BaniTcvGX0IvAw8BHwTa1y4Md+CeRqRL9fudwAvpienNi7Vhu21uwflOT+L+i1X2TJP57iUvUFtHWsAAAAAElFTkSuQmCC\");\\n}\\n.bk-root .bk-tool-icon-help {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAABltpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MzI8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MzI8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+eG1wLmlpZDpBODVDNDBDMzIwQjMxMUU0ODREQUYzNzM5QTM2MjBCRTwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDpBODVDNDBDNDIwQjMxMUU0ODREQUYzNzM5QTM2MjBCRTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgPHN0UmVmOmluc3RhbmNlSUQ+eG1wLmlpZDpBODVDNDBDMTIwQjMxMUU0ODREQUYzNzM5QTM2MjBCRTwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SUQ+eG1wLmRpZDpBODVDNDBDMjIwQjMxMUU0ODREQUYzNzM5QTM2MjBCRTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgPC94bXBNTTpEZXJpdmVkRnJvbT4KICAgICAgICAgPGRjOnN1YmplY3Q+CiAgICAgICAgICAgIDxyZGY6U2VxLz4KICAgICAgICAgPC9kYzpzdWJqZWN0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNjoxMToyOCAxMToxMTo4MjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjY8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cphjt2AAAAT7SURBVFgJxRdbaFxFdGb2bhui227BWrsVKYgf2kJUbP9EUPuzEB803WTXJjH61Q/7Ya1+CMYKEVTsh4J/EpvY7BoabUiNiA8s1p+4KIhpoUUEselHqyS76TbZ3HuP58ydc3d2u4+IkQxczpz3mZkzZ86VYpXjvenpjZsLhUcliE4AuUuASAgptmt1EFdwPiclzIIUUwubNn17OJlcXo1p2UpodHRiux9xB1Eug1+slbzhFxGOKc851tu7/0oznYYBDA8Pt0U2tL8KQryIq2tvZqQhD0QJHRz3yqWhgYGBpXpydQMwqz6NCnurleCSADkJEfgKfOePqL80R/wV1ZaQyr1LenKfkPCkEPKeaj0xg7vxVL3duCmA0Vyuw/fl52hgBxsBED+h4Cv9z3R/zbRm8MTJTx7HQN7GQB6w5C4L4SX7M5lfLBpurjXMyvNIShiyi0l1pL8n9b7EDGPR8fHxzSsQ6XDB3618/xqo6Pk25V5MpVJllgHM1BO58RdQ612kOYZ+GXdij70TYQB05mpj+1kU5G2fB+l3PZtOf8NGx6ambnMXb3yAxg8wjSEG6OKKR9oicBQD+ZvpH2Wzj0lQpxCPG9qMv1x6hHNCsSAlHM7ZOa682vlI9tRDbvHGbD3nZAPpDoD/3JIrLpAs26UFkC3EMUA99hpfGtEBfJjNJnS2Gwnadnvl+Xw+iuc3DAJuNyIaSCHpilVldyDjjUxj3WDZIAhxhHHyRcdNuA7AAfUaXzVKODpzFiZ4/uLvh5G+m2no+C/pyIf7MqlEJB7bpqR6nXkEUfbeawuLaZsW2ISfNQ2vtaktQlGFQyIVGT0o2+2EC4iQNGwjBIN9qdQ5Qg4mk4X4rW3vCClLtowE2FOFUxKDfNmiZci3ovKKRFPh4FK9q4Zbdr+lKKJiA13TcHR2dmLBgdmQ0GAS2MZaEowY+XbAk09IvgtYZGp16SyvFhaHcIUh645t8T9DBCcnz5zZ4hZLu3DzK2QlL1QQa0Y+pHiJKPSuOGj3PmZTheM5w2TwqBxnvBZOTk7G5gvXJ5Aelms8wnJURL+olSWcfEhf6gDoUXPMq6ZlqbzWU2pE+3hi4s6F68tfIj9cBMlikr7Z0/P0b/X0yIcUXsDCF1WhtL4OROHaXk+xlkbV0Cu732Nmhc4peaWSg73pA8dq5RkvO37ldUTfXCKZv2q45MkhvG87WQEzpCCUSvV1d9GONBy3lMvgKSwrZig8gjAietWY0QriylO2jIo4yVbOSb7KB/qmI9BPKjHpSSXYauRyn92Nq9/Kcrj13x3s3v8D481glQ/0raiNYgX9njPSBOImbrHZePl+tfFmc9sH+Xaoh8NjOKSVdDMhjjYzQLy+dFceH5+IJQf9VYXX4tROg4ZFU8m31M3mfPEqUoJqCGJfvWpo2xnNfdrhC28n06SCeSzNZxlvBINGRXCtKS7EY1uV6V7HWAm38y1cXaXsMcOCvr9ySPj+af7A1U2HJXHzVNvUXVLIGyPf+jV0pf8GHoN+TLAyPkidTCi2RpPApmnR0Bd1zGRaB/B8Oj2HSw7LLbVR1MmskW8RdEWVXSJf3JbpAMgRtc4IZoxTh9qotQjCasm46M0YX9pV1VmbpvRH5OwwgdRtSg2vKaAz/1dNKVtb17Y8DCL4HVufHxMOYl1/zTgIgiYvBnFKfaNp3YjTdPz3n9Na8//X7/k/O1tdwopcZlcAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-hover {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4oVHp0SwAAAQJJREFUWMPtlsENgzAMRb8RQ5VJItFDOgaZAMaAA0iZpN3KPZSoEEHSQBCViI/G8pfNt/KAFFcPshPdoAGgZkYVVYjQAFCyFLN8tlAbXRwAxp61nc9XCkGERpZCxRDvBl0zoxp7K98GAACxxH29srNNmPsK2l7zHoHHXZDr+/9vwDfB3kgeSB5IHkgeOH0DmesJjSXi6pUvkYt5u9teVy6aWREDM0D0BRvmGRV5N6DsQkMzI64FidtI5t3AOKWaFhuioY8dlYf9TO1PREUh/9HVeAqzIThHgWZ6MuNmC1jiL1mK4pAzlKUojEmNsxcmL0J60tazWjLZFpClPbd9BMJfL95145YajN5RHQAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-crosshair {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADEUlEQVRYR81XXVIaQRCeHqug8CXmBNETaE4gniDwIgpVspxAbxC9ATkBkCpQ8gKeQDiB5AQxNyAvUlrldr7eHxyGXZi1rMJ5opbp7m++7un+htSGF204vsoMoNXrlzSpfWa1oxQfhAegCZGaEtPorHo8znIoJwCt6+td8uk7ApUQCIHTF4BNAWzImq8ap6cP68CsBdDp9i9ZqXM7ML79g/EnCWD+jgMKENKqWT+tXK0CkQqgNRjs0OxpQIqKhoMxaG6/6JeRnK7T6yO2UvVqhYSlLX+ryORfgKn9ORDFIy7ky41yGcwsr0QAQfDH5zucOswx819fs4egI9OFCcD8DjBF7VNbEX0JzdWEt3NHSSASAcCxBDqMgt/623kvyTgNgNjJIfTjk4D4FqaJR1715MjmYAmA5Bx3AwUXQL+t105KaTlcBSC26XRvhjEIoLiq1yqXpr8FAGG16/ug4IT27fxBWu7EiQuAiImJpEMKE6nYM30uAIDDttSUOPfJP7JzbjPhAiBIh9QE67vIvoOi9WJfCwDavf40ulpjbCqmUf+W753ezURuh7Dg1SqflwAEHU6pgfyBq9Y4qx0LG++2fnZ/eUzcstmdM2AWH+jfc+liWdBJfSENf8Lifi3GVwC9mybOfi5dzatWVrbbLIHNva8p5h/16gkaFiLGGxbufkoE6XguwePiXLF3XmMfCUCUAqtKXU7sumd1CowOuJEi3Pg1FBpjitIGhyvVSfvmjci6ZR+rFQfDiPVE2jFYeICQ+PoewwjC5h7CZld6DBdyu6nDSKgzOyIMhmhK5TTqXYbRorZYM46TmpKAAOrGWwSJJekSB1yqJNOzp1Gs7YJ0EDeySDIMtJbQHh6Kf/uFfNFZkolJICRmz0P8DKWZuIG2g1hpok+Mk0Qphs0h9lzMtWRoNvYLuVImUWrmPJDlBKeRBDfATGOpHkhw670QSHWGLLckmF1PTsMlYqMJpyUbiO0weiMMceqLVTcotnMCYAYJJbcuQrVgZFP0NOOJYpr62pf3AmrHfWUG4O7abefGAfwH7EXSMJafOlYAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-lasso-select {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEgwlGP1qdAAABMBJREFUWMO9V1uIVVUY/r61z57ZMx4DnbzgkbQXL5iCJphlWdpIGY4jpFBkEiU9ZNaDRRcITcIwMwgxoQtU2IMXdAZfMjFvpERXYiSbysyBEXFmyuHMnLP32uvrwT2xnY5nxvHQ93Jg7fWv71/r//7L4a59TRgqJk+Z6v3a+sv0OI5nk5wu6VaSVZImAThHsgjgrKTvM5nMUWvtmf5n8HodCIKgOgzDhc65pSTrJQWDsSNpJX1ljHnDOfdT37oZLLHv+8OMMasKhcIJ59xHAJYMlhwAJGUAzJfUTHLFuFzOG5QDU6dNMyQfs9Yedc5tBpAD4IYYNQGoBrDtQnt7/b0LFrJsCHzfn2itfQfAnZLiazytA3AaQAuAiwDaEgeNpGkkswAWSBqRONB38b88z5uTKePt6iiKXkk8jq+iJC5LOmiMaTLGHLPWhmWeHr7vV0dRtATAapAzIVmSo51zyzIlbm2stesFPA6pKk0r6Ryg93y/ek8YFvPOOTg3cDSiKCoC2OP7/rEoirYm4rUkF12lAWNM1lr7lqQn0+QA8gI2jBg5cj6Aj8OwmB+KAKIoukhyp6SRJAUgl0ndPLDWPi9pJQCbuviXvu+/GIZhW1dnJ24UJFuTjCCA2ADA8sYGWmsXS3qmL94kDYAtkh4Nw7ANlQJ5U6INT1KrAYC9zQdykl7nFSj5fXp5Y8NWVBhy7mUAjqShMYdMXV2dJ2klyRwAJ8lIeuGWCRMP7N7frEqSG2OmAFhKshNAp5wrmO7u7jEAngPQm1S2z2pqapr+OPt7XEly0oxwzq2RdFmSD2AMgKKJouhhAL4kA+Cs53l7e3t7uytJHgRBreTWkXwkKVJnJD0B4GAGwIJE9R6AFufc6UqSZ7PZbD6ff5dkA4CQZEHSqwAOISmXtwGIE+F1SeqqIP8d+Xz+C0mLJYWSAODteXffczjdDQNJ0BWMCoLg5gqIbRTJNwHsljQhUb0luWPM2LE7Thw/9m/5NCT/TByxAOYWi8X6/gdWV1dnfN8fNRBxJpMZTXKdc+6IpFVJWAEgkvSJpA0X2tvtVTaSjgOYBCAEEADYSHK87/sfhmEYA9gShuEDkgzJHyWtB/B1irQ2juP7ADxkrX0wOUOpzmdpzEY590HJ7Ni1r2kSyZOSiv2+hSRjSTXp/QAukzySNJOJkmalyNIl10hqMcasdc61XDNcQRD8BnITgNp+36r6kfcNFMMlLQGwTNLMEuQGQBfJl2bdPru+HDkAZAqFQux53jZHEsC6aw0eg2gylNRBcqcx5v04ji999+03AwsWAOI4Lsy9a94WkisAnE5a5WCJYwCfA1g7LJudI2lTHMeXBm1faiQzxkyRtF3S5CTupeAB+KG2tnZFT0/P30NO2VKLzrmfAbwGMipjG5Oc0dPTc0Md05SZ5U4Q2FxChErtEYD7jTGNQ3UgM8Asv90Yc9I5LSKRlXSI5CxJa0jWSALJjKRnAewfkniT+vwf7N7fXHK9rq7O7+jo+BTA/NRrdBpjnnLOnUrvXd7YMPQXSBunneno6IhIHgYwW1JtkgmBpBkATlVMAwOk3nFJ+VSoqgCMr6gIy2FcLtdKspAedyQN/98caDt/3kpyabUmf8WvG/8A1vODTBVE/0MAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-pan {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4lKssI9gAAAOtJREFUWMPVll0KwyAMgNPgoc0JzDX2Mtgp3csKErSamGabIEUo/T6bHz0ezxdsjPJ5kvUDaROem7VJAp3gufkbtwtI+JYEOsHNEugIN0mgM1wtsVoF1MnyKtZHZBW4DVxoMh6jaAW0MTfnBAbALyUwCD6UwEB4VyJN4FXx4aqUAACgFLjzrsRP9AECAP4Cm88QtJeJrGivdeNdPpko+j1H7XzUB+6WYHmo4eDk4wj41XFMEfBZGXpK0F/eB+QhVcXslVo7i6eANjF5NYSojCN7wi05MJNgbfKiMaPZA75TBVKCrWWbnGrb3DPePZ9Bcbe/QecAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-xpan {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4X4hxZdgAAAMpJREFUWMPtlsEKwjAMhr/pwOOedINJe/PobWXCfAIvgo/nA4heOiilZQqN2yE5lpD/I38SWt3uD9aMHSuHAiiAAmwaYCqoM/0KMABtQYDW11wEaHyiEei28bWb8LGOkk5C4iEEgE11YBQWDyHGuAMD0CeS30IQPfACbC3o+Vd2bOIOWMCtoO1mC+ap3CfmoCokFs/SZd6E0ILjnzrhvFbyEJ2FIZzXyB6iZ3AkjITn8WOdSbbAoaD4NSW+tIZdQYBOPyQKoAAKkIsPv0se4A/1UC0AAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-ypan {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4anK0lywAAAMVJREFUWMPtlzEKwzAMRX/S7rlpIMXeOnaLaME36FLo8XqCdNFghGljyc4kgQi2Q/SUj0F/eL7eMMTKz6j9wNlYPGRrFcSoLH4XxQPvdQeYuPOlcLbw2dRTgqvoXEaolWM0aP4LYm0NkHYWzyFSSwlmzjw2sR6OvAXNwgEcwAEcwAEcwAEcoGYk20SiMCHlmVoCzACoojEqjHBmCeJOCOo1lgPA7Q8E8TvdjMmHuzsV3NFD4w+1t+Ai/gTx3qHuOFqdMQB8ASMwJX0IEHOeAAAAAElFTkSuQmCC\");\\n}\\n.bk-root .bk-tool-icon-range {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAABCJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjU8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjMyPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMjwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxkYzpzdWJqZWN0PgogICAgICAgICAgICA8cmRmOkJhZy8+CiAgICAgICAgIDwvZGM6c3ViamVjdD4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTgtMDQtMjhUMTQ6MDQ6NDk8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPlBpeGVsbWF0b3IgMy43PC94bXA6Q3JlYXRvclRvb2w+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgrsrWBhAAAD60lEQVRYCcVWv2scRxSemZ097SHbSeWkcYwwclDhzr1Q5T6QE1LghP6BGNIYJGRWNlaZItiFK1mr+JAu4HQu0kjpU8sgF3ITAsaFg0hOvt2Zyfvmdsa7a610Unx44Zgf773vvfneezPHNzrbhn3CT3xC3wPXYOC8LDzqdi8YY/gwh4BeknS/2th6dr2kf94AOp3OFyWgMyziOPbMDxV9FTtJnl1ut795Xd0/YQ0/vtYQwMT1KXWCfr2IjOWwtNehwN4xL9ykTrm6Pzl58yLn3J+mKh9mXbT3uRjGEDph+O8/TjfP5dBp7Ha7AX7O3o5nZeD/0E/OGyXntDgzA0X6qmCnrVutVlrUWV9f/3xo+pwhGDhvEPHOjoxnZjJggXmMHzBQ7NGNp9vxk61fr0HR7e/u7pZzCGHlc7qwBYYTT7tJYSx1AQzppyFPft5apta9w7SKcn0b7P7+/jCsDQ5mbc0dCmIJGDN0ehdcjsmkm6A6KUeKFOTE11PLxrC7Ukqh3ylL2fT0NAP9q6ur6rRCJJYsbKB0JsbCKMuy+xREePDyxQPCz+Crlw062QcA5wBOOt1l6vIl2WiI9F1fN6Q+BBqit6hEC4Hk08GQJMn4myjSP7RavVxgdaVUh/3U6HCMsPr9pYnJKRziHtWQ+un58+hGs6nsjQSjpuTyKGN3CX+FBwHXSiEVgjP+O8X6N12kIePES+GzTKAkGbNp8yJsGUMVzz8jPKReiyAQRimy5/cjye5RpF8utFp/+nwmT7d/NMzcFkS7yjJNGDaPURQxIQThEQy0SyF4l5WJYYhBa816vZ6dU7A6CAhbZVow/pDe0O9hVOoCi13r4BgBAvJHqMSQL2vE/iH6IAXEwgrRVUmBoRRwnwJQT98xEeVeSUyB4dJ5nwJBKdCFFGRmUCcu7rwIYypCTblaChuNBhWODrman5ub+4v0rMNBt8z6Ezh7GksJQpCbm79cMQE7QBFm/X6f0rjWnv8WRYg/QdbUpwDAEBy8vPyA8rNGzg3a8MiElwiM7dAtRqNoNptjGPM1laVxP9umWEMGLOKhKUOJDtBwDmzsw9fC/CzHr9SGuCTi2LbbKvVtmqXpCjMihBFa79Wrt5fGx9PDzc3fmu32Lf8qFliwU9emKhBSp+kRKn/hu9k1COEDbFdt/BoKWOAkuEbdVYyoIXv8+I/QK9dMHEb1Knb7MHOv8LFFOsjzCVHWOD7Ltn+MXCRF4729vWMDK+p8rLkvwjLg4N4v741m5YuwCI9CvHp1Ha8gFdBoPnQAkGsYYGxxcfEI7QQlFCTGUXwjAz4tWF+EpymOWu7fglE7qsOvrYE6g4+9/x/vhRbMdLOCFgAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-polygon-select {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEjc1OfiVKAAAAe1JREFUWMPt1r9rU1EUB/DPK0XbqphFHETo4OCiFhwF0V1KHbRSROLqon+AUMVRRFBwEbRFMBiV+mMW/wIxi5OD1kERRVKRJHUwLvfBTZrU5OWBGXLgQu7Jfe98z/ec7z0vKa88b2q1BDtRHdAPBaylm1NzsxsOjPnPNt6WSWprbft+/c3I3zOAjhT1Y4+fvcjEQJIXnVECSa+AhqIHqlHH5lWCZoe+Gk4GRgDG86j9SAUdlDBSQaZhlOkuHyoVdJmsw98D1S5fM4NYM1LCpqM+Lwa240oLgmZzpVZvzKT75VLZcqksSZKWlQeAy/iORVwIvh31xvotvK7VG3Px4aWHj3Jl4C2uYSvq+Bn8v6LLbaVWb9zsBiKLCvbiNG7gLm7jAYqbPHMJMziZ9lsKoh8GtqCEVVzHftwJn+TFHp4/hg8BSCYVfMOZoPEv2NZGdy9WCGUr9toDR3E2/H4V6nwRe/BmgN65H1ZhvMuB3XiKIyFoGefwO6ysVkUlrNUNsyAK/jli533Q+Y8cJFvAeXyMS1CI/jiMr/gUtD2LQwMGr4R3p7bY3oQHQ5b38CT4D2AXXg6YcQXHpyYnlqKsi5iOAVSwL9zd7zJ09r+Cpwq72omFMazjT9Dnibym0dTkRDUKrrgwH7MwXVyYB38BstaGDfLUTsgAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-redo {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4itK+dVQAAAaFJREFUWMPt1L1rFFEUBfDfJDaBBSslIFjbaSFp1FJQFMVCHkzhKIqdUYOCoBgErVz8rCwiTDMwBCIKipDWyip/gxAIWAmBgBC0eYFh2Gx2l9lFcA5M8e59782Zc84dWrT435Hs1siLchqn43MS0zgW22vYxjesYjVLw3YjBPKinMUTBOwf8J5fKLGYpWFjJAJ5Uc7gIW6jM6Kim3iNZ1katgYmEL/6I+YasvY7Lg6iRpIX5VF8wuEe/XV8wGf8jN6LWTiAc7iEQ7ucPZ+lYW0vAtfwvlbfwCKW9gpXDOv1mJvZHiSO91MiyYsyiQSuxtpXXM7SsDmM5nlRdrCMMz3sOJWl4Xevc/vwBzdwAl+yNNwZxfRI+GxelK9ikHcwh8d4NNR/YFRES1ZwoTYdR7I0rNf3TzVNIGbmSvR/Bx08mIgCFSVu4l2ltIWD9WxNGR+W8KOynqnZ0rwCeVG+wa0hjrxtWoF5dAfc28V8Mib/n+Nev5dnabg/zgw87aNEN/bHOwVRiRe4Wym9zNKwMKkpgIWKEt24njxiJlq0aPFv4i9ZWXMSPPhE/QAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-reset {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4gWqH8eQAABLdJREFUWMPtlktsVGUUx3/nfvfOlLQaY2IiRRMQIRpI0PjamJhoVASDvNpCpYw1vJQYSVwZwIVQF6wwRHmkAUof9ElrI6VqDAXcID4TF0IiYQMkSlTokNCZ+b7jove2t+NMH7rQBWd3v+989/zP+Z8X3Jb/WGQySvUNTQBJESkNguAVYIWqzhaRhwBU9WcR+QXoymazn6jqzUQiMQSQzWZRVdal1vwzAI2tHQBPOuc2AbWTdOyQ53n7nHNfRwee51GzqoIQMCLDpr3x/tLQ0oZzrk5Vj0/BOEBt+KYuOlBVGlrahr0Wob27t3gEjnZ2AyQzmUwHsDgP6J/AYRE553neDwDOuUdU9QngNeCumK4TkRMhZUORcYC1qysLA6iuSQHIwkWLD6lqapQsuSmwTVV3h99I7EcAR462A2xR2Ilq6ehTaejvO1774kuLNALR33eclsaGsQDe3fYegHl43vyNwEeqGl1963mm2jl7YZRTQ82qlWP4HM6ZToC5ztkW4LHQoALru7s6Di5dvlIj/e6ujrEAWoZDn8hmMjXATMACGaAVuBjXTVVXFc/AxhaA+4zvn1DV+eHxVWPMAmvtb5GeMWZyZVhI2rt7qVy2pOh9U1snwIPW2vMi4oWJuBPYHkVAVScPoKmtkzVVK6cEMsyJraHhiCqJqJUwj/JRz7TW1iSSyR2rVyylqa0Ta+24Ic8vXaAEmDFc/l5Z2A/80OibuVyuz/f9ElUdHCmvw82t5HK5h6y1PYhsz2YyGw43t2KtBZHIGwB6+j4rCkBVUdV7gXrggnPuu8h4eP+xMeZS2D0rJYZ6AdAMzAt1b4nI26p6IFZOY8pugijcKSIHVLUK0LyST4vnrVfnWr3mjmP4QTATaERkXkypRFX3isjmuHdRJEK6Ckqquopp06bdKCkp2Sgi7XnGLcg7gzeutwNIiPYc8HixqIrIOlU9ONVIhHPEd851icgSVXUiskVV94gIqoonIt0i8gfQCfwae38e6BWRXuBZz5jZ8VbaOE4EIqlZVUEQBLlkMplS1QER2RwkEnsSyaREDUzyeNsvIhvCMqkH1kdIJ2o+k8iJB1LVVRfjZ6nqqlEAIbdVQGto8Lrv+/dbawcjAL7vc+6bs+zetetfLSHxniIFGofGGsU2oC7eOCbDfZ7nQawBOSAX74SF9oEPImOq+r7nmVmxb5raukZa8UReGmNmhbMkAwwBH467EYVZe49z7kdgenj8k7V2oTHm8kgdWcvrNdVFjR8cHkYzjDH9wLjDaEwEzpwa4MypgWvAjtjxfGNMj4jMiT+M+kFsZI/Q6Pv+HGNMT8w4wI7TAyevxXVPD5z8+zD64tRXAMHVK1eaVLUyVvuDqroV2BOnJF4ZIedviUidqt4Re9s+vbx8zZXLl7PR2+nl5Tz/zNOFp2FzxzGAklw22wUsLLaSKXwf8vhosZUM6PeDYEUum70VHfpBwKsVyyfeikOP6oBNwN1TrLbfgX3A1kKLzKeff8nLLzw38T5wZDgxn1LnNk5lLRfP26/OnR2hwfNYW2Atn9RCsrf+EECyrKysDFimqhXhyjY3VLkAXBKRDqA7nU6nS0tLhyIj6XSaN9bVclv+l/IXAmkwvZc+jNUAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-save {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4UexUIzAAAAIRJREFUWMNjXLhs5X+GAQRMDAMMWJDYjGhyf7CoIQf8x2H+f0KGM9M7BBio5FNcITo408CoA0YdQM1cwEhtB/ylgqMkCJmFLwrOQguj/xTg50hmkeyARAYGhlNUCIXjDAwM0eREwTUGBgbz0Ww46oBRB4w6YNQBow4YdcCIahP+H5EhAAAH2R8hH3Rg0QAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-tap-select {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3NzIwRUFGMDYyMjE2ODExOTdBNUNBNjVEQTY5OTRDRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCOTJBQzE0RDQ0RDUxMUU0QTE0ODk2NTE1M0M0MkZENCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCOTJBQzE0QzQ0RDUxMUU0QTE0ODk2NTE1M0M0MkZENCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6OTQ0QzIwMUM1RjIxNjgxMUE3QkFFMzhGRjc2NTI3MjgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzcyMEVBRjA2MjIxNjgxMTk3QTVDQTY1REE2OTk0Q0UiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6eYZ88AAADLklEQVR42rSXf2TUYRzHv7tuGcfE6Vwb5zLSSjEj7Y9KWqfEmFZJP+yPMdKKmUrrn0iUfjhWlLFi6YfNrF+StBoTo39iYkTGco4xxxG59P7k/T2PT8/37nu3bx9ezvPj+zyf5/PreS78bGLS8SmrwE6yje3NHJsDBTALpknBz6JhH3NiYAB0gHqPOVv52wJ6QQ48BzdAttTioRJjdeA8mAHHS2xuk3p+M8M16ipVQE49Ds6CiFO9RLjGONf05QLx6wPQaBlbBlPgJVgkP0ETiIJ2sB/E1XfimjfgBOOlKDUqCGOcqBcQnw6BYW5YTo4wbvQhMmCfGRemC2rBiGXzWUb+kM/NRZ6CHWBM9ce5R61NgX6ayhSJ5EPlItlDRNkz4JbFHf06BkSzHjXxM+gDv1S/mPUo2AXWgt9UUHL/IVhS8yUV1/EbV3o4N+NaoE9Fu/i827K5pNYHnqAVJECShWmAaddpscYFFXwR7vnXBRGlnUN/L6kqKJlxnRUuDbaDBiL+vst5d4gpcpBrqk/2jIgCKVUolhntplzivHmwh4stGOPfwBWwl/2dpp8p7xjQZqFLiQJtauKkivYm+kzccpK57yXfOUe+P23JqAnVbhMFmlXntCWnxbT31am9ZJ4BJifsUmNTqt0cYhA5ypympPg7VkEKunPbVb8cIG+0kyHLJZNR7fUMooUKFHAPkfQo58VLK+RzwRDd4FdWG9mjpaAXzqkJa1R7kQttqEABWXMjOOxxVRfnhRm5URX1prk/0pQHwNcKlchZ+jdpC+hFdVqO0my9Hj5dkYgCn1Rfh/KdlNDHrJhPqlDih+IfBd6qwpOgEqYMsorJ2HtWxtagLJDn/W3KRfPOZhoeBJfZPgVeGKeKrkQBh5dLXl25Ny3pc4/1fkTdbvFqFQgbxWeYD0hXulhQ0pYiM1jG547fcbMQpVnHTZEn9W3ljsCzwHxCdVteNHIZvQa7/7cC7nV6zHIfyFP9EXjFa7YxKAVqPP4bxhhoLWW+z9JyCb6M/MREg59/RlmmXbmneIybB+YC/ay+yrffqEddDzwGvKxxDmzhc0tc80XVgblqFfgjwAAPubcGjAOl1wAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-undo {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4em8Dh0gAAAatJREFUWMPt1rFrFFEQBvDfGhACASshkL/ALpWVrSAKEQV5sIULWlgZNSgIFkGIVQ412gkBt1lYLERREFJqJRaW1oHAoZUQsDqwecWy7N3tbe6C4H2wxc682Zn3zTfvLXPM8b8j6RqYF+UCzsfnHBawGt3fMcAX7GEvS8NgKgXkRbmMxwg41TLsN0psZmnodyogL8pFPMIdLHUk7hA7eJKl4U/rAuKu3+HslFr/FZezNPSTFslX8QErDe4DvMVH/Iq9F7VwGpdwZUjsPtaSFjv/1vCBPjaxO0xcNbHejLpZrrlvJCMCT+JzA+2fcC1Lw+GE4l3CG1yIptfjCtiKoqtiJ0vD3aM0Py/K57iIMxgkQxat4EdN7e9xdRzlk+LEEPvDWvIDXJ928sYxjL36icWK+VaWhlezOIqbGFirJd/H7szugrwoX+D2BDEvszSsT5OBdfRaru/F9dPXQF6U27g/KnmWhgctxqyzBrZGMNGL/rHI0nDkKXiKexXTsywNGx0OnFbFNk3BRoWJXnw//j+ivCi32/S8CxPVNiWOAdUiJtXITIqYY45/Cn8B2D97FYW2H+IAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-wheel-pan {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEgswOmEYWAAABddJREFUWMO9l09oXNcVxn/n3vc0fzRjj2RHyIZ6ERuy6CarxJtS0pQSCsXNpqGFWK5tTHAwyqIGN7VdEts1LV04BEoxdlJnUbfNogtDCYWQRZOSxtAUCoFiJY0pWJVUjeTKM9LMe+9+Xcyb8ZMychuofeHCffeee7/vnXvOuefYlV/+mv932//tb91z/Y2rvxmMHQ+4FcEfOIGN4A+UwDDwoQScc7vM7AIwB8yZ2QXn3K77Ab6OgJnVgeOSbkqaBiaACUnTkm4Cx3OZzwf+qzcRQup1zNZ9RwDe+0YI4YKZTUn6zCGSMLOfAF/03r+QZdnyfwO+ePEiI6N1nPMgMDMkETLRbd2mXG8gCbd9YiIKIUxLKoLfBN7I+80+CUlTIYTp7RMT0b3Af37p8kh5y9gZcy4Fzt+5szqSaxkzUR7dwtrKMmaGW242d0t6vrD/He/90865o865o977p4F3Ctp4frnZ3L0Z+OryUrVSrZ0z8ZxhHjhcq1XPrS43q/0flDlK9XpPA2ma7gMeyvfPx3H8TJZlH4YQWiGEVpZlH8Zx/Awwn8s8lKbpvmq1ahvB641SXNk6dhLskNA2MIBtwKHK1vGTW8bKMRbAMgyPqWeETxUM8VSSJAv52JmZA0iSZMHMThWwnipXKp8hsLLcSaIR92oU8xjSayCQXotiHotG3Ku3m+0EOQwPQCDggMf7BzQajSs5eAk4B5zLx4O1vD2eJMmAQKliscgASJMw21pansFs1swQ/DNLmUmTMNuXX+taXHTDaj5OW612R1JZ0nFJJ/J+XFJ5aWmpA6S5bHV8fHsPHFU6q3pJCjtFxtrKMuXRLUUXXxdrRLazFOtUolZlsGhmACsgnHPTwJnCnjP5HMBKLotzxsTE9rgDL0t6LoriKsDIaB31ZEK+JxQJRHFUBR2NqLw8OTkZR0OC0ntm9k1JWU7OA4vD/mZ+YfElsANmNEKi75vztzB5M8uAr+bx48me88g757PQ1U5zNg52YH7hX8l6f+4Fi3c3BqHNmkI4YQOV2MGCNu9qHPYCewfzbrC+XSGcWEcgTRKA3wFfyzdDz5d+D3x9CIcfA4eBbQS9LscskgfLnHNPAnslvS/pbZDHLLPADpx9N9fqpSIBH8cxWZY9m6bpb4Ev5fN/iKLo2TRNgdx/eo8Wk5O7Ts/N/SOSdMjHdj4kmgkIEJLJzPZKetvMTkIvFLsR25Ml2gfuF5M7vnA66sdooJYkCSGERe/9VAjhzRxoKk3Tvg3U8nulVqvx8cyNpER2umM+SdOkbc5B8JhpqBdIgTRR24h+lpKen731aRIN7thscH9Zlv0d2F8YD2TIX7F2uw3A7ZWV1a0TYz9ca8cJZHRbuRuaDfUCw9/qJHamPOKToAwHtHN6lMvlSkH2o7wDMDo6WuGuQbbn5+YAKNcb3J5fSvrhtTY+vsOPuD1IOyRhMOkj9kSx29HfXB5RUnS964NT2+3vbGbxG9auO2cDNuV6A8NTb5TitBuOpQkfYD2vwOxgmvBB2g3Hto5X42EJyVsFlztbKpXGNgqVSqUxSWcLU2+tdToa9hasLjfPYlwGa+bTi8Dl1dvNsyvNtQQL9MO2w+HM7BqwlAtPdrvdq9773WAVsIr3fne3270KTOYyS2Z2bbXdHhogKmPj7YWF+VOSXs/v/9KdO+0fVBrjbRkgB/KIDBnYu9f/7D+ZmfmRxPd6qwB8YmZXcq1MAQ/nJhTM+OnDe/a8+PGNG9lm19V/D1Qw7HXZlcRa69+U6w38l5/4ipxzf5X0CPBILjcGPJH34pVcc8692FxcXLlXRnTwwH7+9P4f8aWe3fY59LIqo1NMyQBCCHNmdgx4BegUWefjDvCKmR0LIcz9L8nokSNH+PRvH4HC3YQ098pSbevg24qlmZmNmtmjkg4D3+j/tZldkvQXSa3PW5ptlpL3ZaIN99OS9F7+IgKUgSyEkNyv2nHT7DZX0dr9rpjua2l2r4rogRAYVqZvnPsPqVnpEXjEaB4AAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-wheel-zoom {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEgskILvMJQAABTtJREFUWMPdl1+MXVUVxn/fPvf2zrSFmUKnoBCUdjRoVaIxEpO2JhilMYBCtBQS2hejpg1Uo2NUrIFAoyGmtiE+GHwQGtvQJhqDmKYRBv+URFsFDNCSptH60DJTO3dKnX/33rM/H7rvsDu9M20fDMaVnGTvtb69z7fWXmvtc/TEzqd4OyXwNsv/FwFJQVI/sA14SZKRLOlPkr5TrVYXHz70quYkEEK4TtI2YAgYkrQthHDdhV5uuw+43/ZrwCbgRttgY/tjtrc0m83X3/f+D6ydnJhYcB4BSZcBA7aP2d4ELAGW2N5k+xgwkDB0IH19CGGH7R8B1aQeAf4KvAw0ku4K2zu7uru3ApdPEyiKohd4TNKjtjt5h6RHgccSNrddbvuHtm9Jqoak7xVF8WFgdavV+pSk5cCObNmXgK++85prCj3z28HKqZMnH7D9YAY4BvwujT8BvCuL1INX9vVt+dfwcCvNb7f9q2RuSfrGvWu/sL2Nf3LX7pzvj4ENSGBPVarVd4fRkZFltjdmoMGiKO4IIWwIIWwoiuIOYDDzeOPoyMiyFLkum7WJCMDztrcrTTrIRuAQZ6NcK1utL4dWq/VZoC8BhqvV6l1lWb4YYxyLMY6VZflitVq9CxhOmL60hhCKeYiV7WMKIXw9jT1HpXw3c+bOAKzOjJubzebJrKQCQLPZPClpc7bP6rMYKtjXth2OMf7tIkr11Wz8oQDc1Fb09vY+kQw1YAuwJY2nbUluAnCWpKkaFl6IQIzxivaR2SYA89sJVK/Xp2x32R6w/a30DNjuqtfrU0ArYecDCEqgLqm94T0dEm9mBG7PxkdDlkBnkhebgIezNQ8nHcCZPL9ijE1Jf/bZZoPtzbavmqNZLbf9tSxq+yoduuJ+SZ+zXSZyBXCqU+d8fvC5yRUrV+0G2j3g2hDCLyXd/+Su3QdnvP/zCuH72LWsgf2k0oHlH2c2odlkxcpVEdgr6aDtjyb8x20/J+mA7T9I6rL9SWA5dne2/GdXLl58qNJh398An85yTMA+4DOz8Dgu6Zu2dwJXJ91ltm8Gbp7Fgb+EEB4aHhpq5CEtACqVyr3AC0AlPS8k3TSmQ2YPhhBuS/1/LpmS9JTtNTHGfwBU2uUALARotVqniqJYH2Pck85pfavVaufAwnQvnHc0McaDKVptebN94QAnJB0EdtjekydyZXqjs/0ZgLIs/w6sy8bnYGYJ63pgERKC05JutT1kOwITwL9tvzlzUQUYB+Zjs2DBgu6xsbGJZHstByZbezregcBXeCsEz1bnzXt5anLyzLq71zDLxTRdVgemdx0fv2e2w5thO5DbiqL4oKT3ZKpnpyYnz+SY2ZpTAPZmJfdIrVZbNBNUq9UW2X4kU+2dcf53Aj1pj2PA7y/6m1DS00A9za9uNBq7iqJYBuoGdRdFsazRaOzKSqye1rTbaa/tlbYrqXQP2X4FIA9/J1l39xrC0v7+w5IeB8XkwS1lWe6TGJAYKMty31tfO4qSHl/a3384I3CDpI+kzC4lnRfrue6GytEjR8oQwlY73gC0L4qlth/q0M1/LYWtR48cKQF6enrC6dOnVwGLEpnxnp7en4+O1i/tszzGOCTpPmB7ahb57QUwBWyXdF+McWg6MScmuoA8OX8xOlpvXGz422XYTsB/SnpA0h7bX5R0WzI9HUL4qe2XbI+dk3xl+V7gxoztD5jRI+YK/zkEEokx2/uB/RdzIfUtueqVN04cXwF8G3iHY3z9Urw/j8ClyhsnjrcS2Vv/J/8NLxT+/zqBTkcxU/cfEkyEAu3kmjAAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-box-edit {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEg4QfHjM1QAAAGRJREFUWMNjXLhsJcNAAiaGAQYsDAwM/+lsJ+OgCwGsLqMB+D8o08CoA0YdMOqAUQewDFQdMBoFIyoN/B/U7YFRB7DQIc7xyo9GwbBMA4xDqhxgISH1klXbDYk0QOseEeOgDgEAIS0JQleje6IAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-freehand-draw {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADTElEQVRYCeWWTWwMYRjH/88721X1lZJIGxJxcEE4OOiBgzjXWh8TJKR76kWacOBGxdEJIdk4VChZI/phidRBHMRRIr7DSUiaSCRFRM3u88gz+o7Z6bBTdjmYZPf9eJ55fv/5zzvvDPC/H9QsA66Olo9Ga+/MdR+Ljm2/KQIULsz9FqItGdOfJKLhApLgVkiSCGODjWit7QpKWy+TNrFeXvzKVUT8NiTVaIgDcbiCFJ7GiT8WkARXAdYBK0Lbhi/CenArRNskuM7/tgNp4ArQ42dwjf3WY5gWTqC7O/NbNn2Xkfw/YwdSw/We14HP2IEZwX+y9cZ9SH0LmgFP7UCz4KkENBNeV0Cz4b8U8DfgKiDxMWwUXETqLvJpCQpXZfawbzS7t9v5pL19cHBwfja7YA0y/lyCM0+E5hv5+piZXwKYcF23as+37bTXsQVqgkL0p/34fHR7DcBtbetFsBmGDwMOJCggYG55yw7dMlk6DuC1Bdu2RsCU9TYWQq2IoGbsreZ5NzvEqfSBsIsIy8OTbcdgiRHeh4o8AFAEwDakbY2AaCCpH7V9aGhoUUUy3UyVbkPYFuYLDlUZH8XBpwxkK0Dbgxg5HcVi0ent7a0RULMIozaHBSMfF9b2SzdutFcFB2FkwMIJOG6qfteXOa1nHZ48tyefuwyfT9s6wtzZ3t7eZse2DR2I228TtHXzuWCx9g8MtK5cuHCZTH4tiHEOa4xFngvTyS8f35d6enomiCi4/foEXBkZaQuukChL4FYA2Whd7YcC4gEdW3CpdL3LtGAVCVYJywEyTpAuJKeMOKXZs/Bw947C50KhUFOG4cwz35cjWNBlHGeD53n3xsfHP/T19U1qciggar8Fa4I3PHobIotBWBtc2hSiChyZxVzM53Pv7FVH6Tp3uVy+g0r1ImD2GjIrQGYIxjnfuXTZGICS5k/bBwJoubwEFX4TLah9EXomJGMA3za+f9913Yl4TnzsDQ+vE6YTZOjHh4ngibstt1pzQwd04F0bPStEBpXqRoBeQ/AKghfBnOEKgS+Q7z91Xfdz/HGKg8Ox7z8iYD9z6wqTkZFgnvhMGP9VZ2or1XVkPM9z0mytSfVsHa1RLBZbLoyNzUnK+ydz3wC6I9x+lwbngwAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-poly-draw {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEjglo9eZgwAAAc5JREFUWMPt1zFrU1EUB/DfS4OmVTGDIChCP4BgnQXRxVHqIJUupp9AB8VBQcRBQUXIB9DWQoMRiXZzcnQSA34A7aAuHSJKkgo2LvfBrU3aJnlYkBy4vHcP557zP/9z3r33JdXa647N0kHSZd5Nn0rSxc8G3cXp85sMcnZZ8vge3osZ+l3vB8CWFA0iL14t79h210swAjACMAIwAjACkB90D/8/GchI9ve4nPwTBh5E9ws7OepzGWb9EddSn51Op9ZstadSg4VK1UKlKkmSDSMLALewiuNh/hVJq71Wxttmqz0dG88vPc+MgWP4grvYG3SLOBrZFFFrttqPe4HIDxh4GSei+98iSlusuYopXEAjBtEPA3tQwUpwluAbDm4TPJUz+BTW9l2Ce6G7L0X/Bw8D3T/7SKKIDzHg7QCcxjvcQAEtXAnrrg/RP0/DKPbqgcN4iVOR7gcO4dcQgRuoh7HSqwlP4n20m63jJu5n8MkWMYfP3UowhzdR8FU8w9iQwevBdyq3/27CMRzAE5yLuvsRLg+ZcR1nJ8YL81HWJUzGAPaFZwe/Q5MdyYDyNHgjzO90YyGHtVDncuiJchaHw8R4oREFV5qdiVmYLM3OgD9k5209/atmIAAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-point-draw {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEMEiERGWPELgAAA4RJREFUWMO1lr1uG1cQhb9ztdRSP7AF1QxgwKlcuZSqRC9gWUUUINWqTh5AnaFOnVPEteQmRuhCURqWsSqqc9IolREXdEvQBElxtdw7KURSFEVKu4w8wAKLxdw9Z+bMnRmZGXfZ29//II8th4WwGVNyIoQLYB5vxA9Caq04iUd9A+7ZlsNC2I7TdSd2hZXMJKlnTqp9jtl/GBaqoyQ0noFKpUIzBicYYc+DEFpxkglc4oVJa5gvDn8v1xV2irG3FM4NSVwjUKlUaMcpJhCGmSEJQ6QGD8M5WnHCd8+f3QCXpPLx8WNwv0j6Bm9FMK7FJ3WBE+R/2t7c/GBmFvSBrzRTCsyTDjXrxUgEMtpxynJYmJoBJ4VAybwVARgvL7Oik0okCodnKpVKX7P0leiVMb0VvbJT+upznK4vh0GIeQwwQStJkHQD3MwsCALTJRG7Qrdrj5m/djgYaIa0hlkRdJk26XEgC9txurccBtVW3IudBImmZuACUP+ZlIDBt9FKcubYNTcAH/X0RYM1E7utJPlqe+uZzPxUcEkiSS4sTT95n15Mud0xWC0o2PAWOCdK3KYZlFxfM+tHOcnMzNr1es18ug+cgsVjP4yBU/Ppfrter1m/+l0+zYygML1xRVHU7TSb1cSzBzoBzszsH+AMdJJ49jrNZjWKou6wBnwOzcyndBpNbuueURR1Dw8Pq35p9cc5p/Dy9Dypt7jXrtdGwQECS9NPhr6Gq6txUzNigE6zydLK6lTw12/KT4FGFEUfJX2YJNONq5tVs4ODA7sD/DnwJ/BoADZuE3tHFs12dna6d4C/BI6AlbyzI8ii2TTw12/KK33gb2cdXsNZoAntbZC2SeO4c9592k/5eNQbiwvFd1kJuFGwLJr1wSPg/SwpvyFBHufOeXcFeAlE97U/uCxOY+P3b+Bn4B3Q+L8EdJfD4a+/AbC4UBzPxiPg3wlHZquB28Cn2IuR9x3gr3uV4DbwfvSDOvi4uFA8BDZmIRHkjHpS9Ht9iRqd8+5G3g05mAGcQbsdiX5QJ428G7Kygo8XYdb1/K4NWVmjzkNge2sz84bs+ELmpDDLtqWsNZBXgvmw8CTtpWVMT7x5YWBjLARnwZfKQNYN2U2LPvrh+5nBt7c2M2/It9bArCTKR8eZN+SJ13AScPnoODeRdqNenH+wul5w2gUr2WUjMFAt8bZ/0axX/wNnv4H8vTFb1QAAAABJRU5ErkJggg==\");\\n}\\n.bk-root .bk-tool-icon-poly-edit {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gELFi46qJmxxAAABV9JREFUWMOdl19vFFUYxn9n9u9sCyylUIzWUoMQBAWCMdEEIt6xIRQSLIEKtvHe6AcA4yeQb7CAUNJy0daLeomJN8SEULAC2kBBapBKoLvbmdl/c14vdmY7u91tF95kknPOnHmf95znPc97Ro2OTeBbdjFDT3c32ZxVHUOE9kSMB0/m6ExuoJn1H+ur6Y+OTfD50SMN5168OgrAlyf7CfuD+z7+iDs3p8hkLUQ0iFQ/yFl5Nm/qonfHVva+s32Zw9GxCYILsZ08tpNfBhbs+1YN4OH9+7huGdECSBVfqUosbsllfmauBqiR+cCNwOr7AEo8pPHJnymXykhg5fUWjoQpl0vVvhZhbSzGoUOHqgBlt6B6uruj2Zy1E9jo0fhfeyL2x4Mnc8VErK0KUEOB64JSyptfG4RSytsJjUJVxw2lsFy3urL9nx1Qd25ObctkrVMi+jQivd7U2ZyV/3Hzpq7h3h1b/7p9Y0o8v8rwAbTWrGpSocN/FGDlbAI0Rl23PCBan0Ok158H9Ipwzi25A/Mzc9Gl/BYx/E4kYqC1NKRARNAaDCNUM27Z+Zr+ouXs0q4+LSLBHPYCFkTkC6uU39kwCdsS7WRKmaYUiAhdnZ3MPX2K4+QjQI+C94A93rMzm8ltMwyDeDzWjMZeEb2pYQDdW3vITU2jtUZ5QThOPgm8C7wP7J15OPsBsB3oWpGnVWisCeDS1VHj4vBI92+/3tgB7Ab2AruAXiDBK5oIOkhtkEYRNRuJhObrd8Dl9ewf4D5wG7hVLpen29vb5wzD+BrkbBMaL3d1dk5nsrnlFDTTFWAWmAZueWD3gCemGde2k2fw1Al1YXhEvjozoO49eczdqekrWmsc2zlrmvEKOGoW1GUjFLqSk2KpJrCLwyMCPAP+BO54QL8DM6YZX/ClsP9YnwKkXnIBP4jdIpJRpdJTCYdMwwi98KU0Hjc/dDILNyUcwTCWdOSMJ0TRmBktGRhLugu0xyLk7CIqVNm+0bGJptl1YXikD0grpY4Rjc4a8Fbgdab/6OGbAJeCUuyJnnHmZH9pbSyGuBXV8NUwlUpR1EWyixmSyTWEwqGlJ2Swbo2JXbAAfgDGgGQA9I1A9t1tlq0AxrXxn0ilUpw4fhQqYkH/sT41OTnJJwf2s6FjI5mshdYa7bqVR2uezr9MJmJt14FvGrh/O9D+e6UkM/xyCuCqEKCYnJyUTKFQrZDHjxzGshwWLQcRsOz8Hi85P23id0ug/XilAMLBmm4tPGdoaKjSH5+oAGrhwvBI9SjZTn4QSK9yenoD7dlrExPoJlXW8G8ytpNHxRKk02lGxsdRKFwXLNvx5yY94HQLGhGk4LFCYQSqaE0AwWM1eOoEbR0dKBSW7bC4mKuffxs4D/wCLKwQQPAUzIkslfp6cVomROWSolh0GjldAM4nzDi2k9/i5UAzC9aKfwNJ3zgJg9YEvN6+C7SHgKm69+sD7RfNnKTTaZRPQfAut4oFV//IS7gkcB34VlVo8kGzphlfB+DU+TfNGBpZtRastvrvARJmfMF28ge9sc2B9/PNnCilMIDwK6y8/ow/Ai4kvILTljAXvDvEvrqKSUs60KolzPjBxspavQD2tKqCAGF/Ba+xE/Wbilu54wZV8NEKF5fXzQHl/bh4hUsE0WAXSlDMYcQSrQXgCmsTseXHsJkNnjqBFGwKJaHsKlxtUHYVhbLCzr1kaOA4bcn1y1Swmb+iLpJKpVrfgdpfsiVVCYcgluwgnU7jEgJ4s5UkLFtWYyHyEg0/N1q1tmQH+YXnAMFr97Nmv3p+0QsHQRsF8qpBOE5+rb9Nkaj50tVQKjqh4OU3GNL/1/So3vuUgbAAAAAASUVORK5CYII=\");\\n}\\n.bk-root .bk-tool-icon-line-edit {\\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAG/3pUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarVdpknSpDfzPKXwEJBDLccQW4Rv4+E4BtXR198znCdeLLijgQUoppWg3//Pv5f6FDwefXJRcUk3J4xNrrKzoFH8+pyUf9/f+8J3C7y/j7jnBGApow/mZ5l2vGJfXCzne8fZ13OV+9yl3ozvx2DDYyXbauCDvRoHPON3frl5Imt7MuX8hH0seiz9/xwxnDMFgYMczUPD7m89J4fwp/iK+OVRbiMf6gm8K4bv/3NN1Pzjw2fvwn+93PLzccTZ6mJU+/HTHSX723/bSOyLi58n8jmiqz/798+a/tUZZax7rNCKOakzXqIcpu4eFDe483kh4Mv4E/byfiqd49R2OHzC1Od/woxLD44siDVJaNHfbqQNi5MkZLXPnsMdKyFy5gwwCHXhocXahhhEK+OhgLmCYn1hon1vtPBxWcPIgrGTCZrR5fHvc58A/fb5stJaFOZEvT18BF1t8AYYxZ99YBUJoXZ/K9i+50/jPjxEbwKBsNxcYqL6dLZrQK7bC5jl4cVga/Ql5yuNuABfhbAEYCmDAJwpCiXxmzkTwYwE/CuQcIjcwQOKEB1ByDCGBnMJ2Nt7JtNey8BmGvIAICSlkUFODgqwYJSbkW0EIqZMgUUSSZClSRVNIMUlKKSfTKc0hxyw55ZxLrllLKLFISSWXUmrRyjVAxsTVVHMttVZVHKpRsZdivWKgcQstNmmp5VZabdoRPj126annXnrtOniEAQlwI408yqhDJ02E0oxTZpp5llmnLsTaCisuWWnlVVZd+mTtsvqVtU/m/po1uqzxJsrW5RdrGM75sQWZnIhxBsY4EhjPxgACmo0zXyhGNuaMM185uBCEgVKMnEHGGBiMk1gWPbl7Mfcrbw7e/V9545+Yc0bd/4M5Z9S9Mfedtx9YG7rlNmyCLAvhUyhkQPrNhvO5AJFnrZIR0plaLL5liQYdDi5TubaIokFDkmoFEB8CzxZVxemssDqthPhUblPgW1iQU5g6XwNwyVI7bUFRm035iNziMkgWvEso2SXnsJfveR0Y4SlVF8YWC1pVQhJiQa8JwDvlMNIxAfq3F7GDObHU1LlhzlZaWwNp6BvACxAgInGXlllMGZCpEnZHrGA6GM2718xuFcz7YdUQxzEEfjdWz4GlkcwaonT0pgA6mB25grPILtnSMhuCpsGhmMU6uJbixJs4lbKHqh+wos1jW2rchyGRCIvN9MXu+KAmMSfAlIKVvi/tybhCPJZCu2Ow9pLdyo427+X2ovMBmKNu8PA0zgl3fS0PB1DWWkVYB47bkyiJHhkFPzTzCjzn4Dq1mqoIWzCmcDGsHQmQAQdEHsixK1IXESd5rLU7THVJNV8obHS8sZeN0G5Jdt5pQTVKCCbgK1hItTS8o92iEZpuWJ/oC2r/0+zTmhvFXoaMVKRe27altDtid6OvG1hENVwBnC61KKugNoemOiPCCNb3GoHAZOFuDxxPsD+07nbSPcr/o1Zmc4jARhotrA5F5ZcjP9rPk90vR8A+k028A+8+5wKlHVID542sMzMCuXktkRzUCpE+xCBZywjNcJITx0II9x5948CekBl4XaC5OCX2nCyObdwN3HwQh5DWL/BBEkhDYHn/vpXNgZkVTZs8rj+HO8JFC6qvDVhgAEQSYCDyC86rMhG1WPzAVB9ZldDWG6EzDcFiqJBDvFS8mXDv3SK2LPoguVB2kwUx7UL5KqZWiEzocsbvSjNnaYDNtcYJuA5cDcsrvHd6yCxGjqvl9+wh3Qh8Kc9py8sNW8ncU8qwxdPj1qIGfrPqlXeoS4/JLa/LwRLTCtxuSoZUT+2Su6kXW3QNacYQbId6NUKVbROpviybFSPQQL9lhB2MamEnFyB9Y+hrG1+xBg+L0QG2TZdTdlcsBdq9oHdt9Bu5/IM9+Nfh1AwrSqlboTA6Bgq568A7UfbaMrZjoQZhQphofvNw93+bN+5X7FYKBgLmRid+tSdV6c02A4R0cHwKobmoMt5+6WI9XNISFIywpf6RMd5/a91vE78FzVHIFmxud4woyJx76OMTCa4yhgN3iJO2VfRPFMv9sYTxFzU+1eWeYS52pwOoSJldZY6koib4P1O427rbeUrNZfu44hWjz5ZSuu/vKPpimoXbLkfxWSPetvxDWG5jQSaZCxA3ad+p6rlttDhK+YwwK1LHVe0drDtorc5vnQ1247g58vewDtU7L3DRwrG4dhCUDRKKOtYr2dXHtpt+33d1WZmfkAHdl7Q8ENF+CNgB+nOw29n5F7SeNo/ckbu4laLTCdqJLHjmhJbKzmrCEX7zULrhefuHmu0V/1nbP1pnb6FaT7sOxn4pvWkfrYhYtCeJ4Xv+kOXrroIs1eHWXN1/AfzaY94ms5vaAAABg2lDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV/TSkUqDnYQUchQnSyIijhqFYpQIdQKrTqYXPoFTRqSFBdHwbXg4Mdi1cHFWVcHV0EQ/ABxcnRSdJES/5cUWsR4cNyPd/ced+8AoVFhmhUaBzTdNtPJhJjNrYrhV4QwjAgGIMrMMuYkKQXf8XWPAF/v4jzL/9yfo1fNWwwIiMSzzDBt4g3i6U3b4LxPHGUlWSU+Jx4z6YLEj1xXPH7jXHRZ4JlRM5OeJ44Si8UOVjqYlUyNeIo4pmo65QtZj1XOW5y1So217slfGMnrK8tcpzmEJBaxBAkiFNRQRgU24rTqpFhI037Cxz/o+iVyKeQqg5FjAVVokF0/+B/87tYqTE54SZEE0PXiOB8jQHgXaNYd5/vYcZonQPAZuNLb/moDmPkkvd7WYkdA3zZwcd3WlD3gcgcYeDJkU3alIE2hUADez+ibckD/LdCz5vXW2sfpA5ChrlI3wMEhMFqk7HWfd3d39vbvmVZ/P2aecqIM1FFZAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AQdDBkQmV+argAABM5JREFUWMOtl9trHFUcxz9n9jYzm7Tb9JIWGtqUllLwVgRBQWl90S6lTaGmF6E2/4H+A4r+A0offdlWodL4kEZw9bG+iC9iKqLF0os0EBq02dtcdmdnfj7szGZ2M5vulv5g4JwzZ873+7ufUfMLi0RSa1TZNzVFrW511xBhzMxx79EyOwrbGSSzZ073zOcXFnlv5lTi3mvfzAPwwYVZ0tHiq6+/xu+/LlGtWYgEINL9oG657N41yfSRgxw9cHjDgfMLi8QVsR0X23E3gMXnkXQJ3L9zB99vI4EA0sVXqsPF93xW7y73ACVJBJwE1j8HUBIi3Sz/QNtrIzHN+yWdSdNue915IMKWXI4TJ050Adp+U+2bmkrV6tZeYAXwEJExMyf3Hi0rM5fvAvS4wPdBKRW6vZeEUiq0RIBCddddpymu0+rRbPvEzkPVmmWLBA1EdGAbYNctt7V712QwfeSgd/uXJQnPVVoEEAQBTxXpuEMELNtNNFW1WrsrQdBCRImQEeE/wBUh53v+7tW7y5n1+BZRIoJSioXvy3itdgclURSZTBrP87AdV57G1TT0d4GPgC+Bw8Ca7bifATsTgzBvjlH1qgNdICJM7tjB8soKw4jtuD+Gw3c229e1wF+P/uHPpT86rhBBRHActwAcAl4EjgIvAYcFJnlOoq5dv6EBU8AR4OUQ6AVgGjATwuC5YUdZ4A+z+1mBTUM/AKwqpZSIpPfu2VP7+/6DYEMMPE9N83lzq23ZWwxDd4GaQnmgUloqperSCpKC8HGCXz8G7NANU8CWUKPzsUDbyLPVyjYC39e0VMZx3Ccoha4b4lQqbUlnsBqNWCXpEMgKfA38DNSBcdPQr4zlMtTtFiqlulmQmJv9ks2idUZGZMjZmZMAfBUvxWHR0y5dmPV2FcbPG9ncFdPQS3nTuAJQLBZpBS1qjSqFwjipdGr9SWlsHTewm9ZmnngMKAaV9nBd+/bmdxSLRc6dnemm3+yZ06pcLvPGW2+yfWIn1ZpFEAQEvt95goCV1TXMXH4zAt4woaRF7RTAVylAUS6Xpdpsdjvk2VMnsSyHhuVEZTh+xgywBhwLfZIdKRfj7dWqPGFubq7T428ukslkaHttLNsZ9P3nwIfh+DhwS4EO9DA0zByBCE2n1fPxpQuznSCaX1js9nFp2pjbtqGhobQ0jUY9CbgALERah3IM+El1rNqTaqaph5W1uYGAFrfA5YvnyE9MoFBYtjMI/BXgQR/4pqVDZL3V9/cYrX+x7SnsXh/H5TLwW2iBQbVLNgn65CDsrSPOIJOXwmdQ4fRHrZilUqmXwNXrNzbbfxv4ArgFVBLeJ95oDEMHwHHcvvUcRqEwuBf0SSUEB9gfxsAgAkO1kcj/WvwKPaR8EhvPAUvRtdIMtR1FtBH37w8DEeChaehXw/xfAnzHcVOjEkhHrIe0Qlz7T8PuWLEd9+2w9KphgUUgQJ7JAgAPDT13NTrJyOYqIilrlEwQv/NPMTSByxfPIU37eCqtq2zWmPYDjbavaLYVdn2NuffPjqRJK2hRLBaHzoK+X7L1QE+nIFeYoFQqkTVMaTn2UOe1LWtwEJqGzqgRnS9M4Fb+3XBJGfSrFzW9dBw0icioJBzHzUXdMJM18APwWo6Kmy1O6X+V8UHDotBqogAAAABJRU5ErkJggg==\");\\n}\\n'},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=t(1),s=t(72),o=t(303),l=n.__importStar(t(282));class h{constructor(t,e={}){this.items=t,this.options=e,this.el=s.div(),this._open=!1,this._item_click=t=>{var e;null===(e=this.items[t])||void 0===e||e.handler(),this.hide()},this._on_mousedown=t=>{var e,i;const{target:n}=t;n instanceof Node&&this.el.contains(n)||(null===(i=(e=this.options).prevent_hide)||void 0===i?void 0:i.call(e,t))||this.hide()},this._on_keydown=t=>{t.keyCode==s.Keys.Esc&&this.hide()},this._on_blur=()=>{this.hide()},s.undisplay(this.el)}get is_open(){return this._open}get can_open(){return 0!=this.items.length}remove(){s.remove(this.el),this._unlisten()}_listen(){document.addEventListener(\"mousedown\",this._on_mousedown),document.addEventListener(\"keydown\",this._on_keydown),window.addEventListener(\"blur\",this._on_blur)}_unlisten(){document.removeEventListener(\"mousedown\",this._on_mousedown),document.removeEventListener(\"keydown\",this._on_keydown),window.removeEventListener(\"blur\",this._on_blur)}_position(t){const e=this.el.parentElement;if(null!=e){const i=e.getBoundingClientRect();this.el.style.left=null!=t.left?t.left-i.left+\"px\":\"\",this.el.style.top=null!=t.top?t.top-i.top+\"px\":\"\",this.el.style.right=null!=t.right?i.right-t.right+\"px\":\"\",this.el.style.bottom=null!=t.bottom?i.bottom-t.bottom+\"px\":\"\"}}render(){var t,e;s.empty(this.el,!0);const i=null!==(t=this.options.orientation)&&void 0!==t?t:\"vertical\";s.classes(this.el).add(\"bk-context-menu\",\"bk-\"+i);for(const[t,i]of o.enumerate(this.items)){let n;if(null==t)n=s.div({class:l.bk_divider});else{if(null!=t.if&&!t.if())continue;{const i=null!=t.icon?s.div({class:[\"bk-menu-icon\",t.icon]}):null;n=s.div({class:(null===(e=t.active)||void 0===e?void 0:e.call(t))?\"bk-active\":null,title:t.tooltip},i,t.label)}}n.addEventListener(\"click\",()=>this._item_click(i)),this.el.appendChild(n)}}show(t){if(0!=this.items.length&&!this._open){if(this.render(),0==this.el.children.length)return;this._position(null!=t?t:{left:0,top:0}),s.display(this.el),this._listen(),this._open=!0}}hide(){this._open&&(this._open=!1,this._unlisten(),s.undisplay(this.el))}toggle(t){this._open?this.hide():this.show(t)}}i.ContextMenu=h,h.__name__=\"ContextMenu\"},\n function _(e,n,o){Object.defineProperty(o,\"__esModule\",{value:!0});const t=e(9);function*r(e,n){const o=e.length;if(n>o)return;const r=t.range(n);for(yield r.map(n=>e[n]);;){let f;for(const e of t.reversed(t.range(n)))if(r[e]!=e+o-n){f=e;break}if(null==f)return;r[f]+=1;for(const e of t.range(f+1,n))r[e]=r[e-1]+1;yield r.map(n=>e[n])}}o.enumerate=function*(e){let n=0;for(const o of e)yield[o,n++]},o.combinations=r,o.subsets=function*(e){for(const n of t.range(e.length+1))yield*r(e,n)}},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const o=e(296),i=e(173),s=e(72);class c extends o.ButtonToolButtonView{render(){super.render(),s.classes(this.el).toggle(i.bk_active,this.model.active)}_clicked(){const{active:e}=this.model;this.model.active=!e}}n.OnOffButtonView=c,c.__name__=\"OnOffButtonView\"},\n function _(t,o,e){Object.defineProperty(e,\"__esModule\",{value:!0});const i=t(1),s=t(19),l=t(72),n=t(115),a=i.__importStar(t(18)),r=t(78),_=t(9),c=t(13),h=t(8),u=t(81),v=t(306),d=t(307),b=t(308),p=t(295),g=t(299),f=t(310),m=t(173),w=i.__importDefault(t(300)),y=i.__importDefault(t(311));class T extends u.Model{constructor(t){super(t)}static init_ToolbarViewModel(){this.define({_visible:[a.Any,null],autohide:[a.Boolean,!1]})}get visible(){return!this.autohide||null!=this._visible&&this._visible}}e.ToolbarViewModel=T,T.__name__=\"ToolbarViewModel\",T.init_ToolbarViewModel();class k extends r.DOMView{initialize(){super.initialize(),this._tool_button_views=new Map,this._toolbar_view_model=new T({autohide:this.model.autohide})}async lazy_initialize(){await this._build_tool_button_views()}connect_signals(){super.connect_signals(),this.connect(this.model.properties.tools.change,async()=>{await this._build_tool_button_views(),this.render()}),this.connect(this.model.properties.autohide.change,()=>{this._toolbar_view_model.autohide=this.model.autohide,this._on_visible_change()}),this.connect(this._toolbar_view_model.properties._visible.change,()=>this._on_visible_change())}styles(){return[...super.styles(),w.default,y.default]}remove(){n.remove_views(this._tool_button_views),super.remove()}async _build_tool_button_views(){const t=null!=this.model._proxied_tools?this.model._proxied_tools:this.model.tools;await n.build_views(this._tool_button_views,t,{parent:this},t=>t.button_view)}set_visibility(t){t!=this._toolbar_view_model._visible&&(this._toolbar_view_model._visible=t)}_on_visible_change(){const t=this._toolbar_view_model.visible,o=g.bk_toolbar_hidden;this.el.classList.contains(o)&&t?this.el.classList.remove(o):t||this.el.classList.add(o)}render(){if(l.empty(this.el),this.el.classList.add(g.bk_toolbar),this.el.classList.add(m.bk_side(this.model.toolbar_location)),this._toolbar_view_model.autohide=this.model.autohide,this._on_visible_change(),null!=this.model.logo){const t=\"grey\"===this.model.logo?f.bk_grey:null,o=l.a({href:\"https://bokeh.org/\",target:\"_blank\",class:[f.bk_logo,f.bk_logo_small,t]});this.el.appendChild(o)}for(const[,t]of this._tool_button_views)t.render();const t=[],o=t=>this._tool_button_views.get(t).el,{gestures:e}=this.model;for(const i of c.values(e))t.push(i.tools.map(o));t.push(this.model.actions.map(o)),t.push(this.model.inspectors.filter(t=>t.toggleable).map(o));for(const o of t)if(0!==o.length){const t=l.div({class:g.bk_button_bar},o);this.el.appendChild(t)}}update_layout(){}update_position(){}after_layout(){this._has_finished=!0}}function M(){return{pan:{tools:[],active:null},scroll:{tools:[],active:null},pinch:{tools:[],active:null},tap:{tools:[],active:null},doubletap:{tools:[],active:null},press:{tools:[],active:null},pressup:{tools:[],active:null},rotate:{tools:[],active:null},move:{tools:[],active:null},multi:{tools:[],active:null}}}e.ToolbarBaseView=k,k.__name__=\"ToolbarBaseView\";class B extends u.Model{constructor(t){super(t)}static init_ToolbarBase(){this.prototype.default_view=k,this.define({tools:[a.Array,[]],logo:[a.Logo,\"normal\"],autohide:[a.Boolean,!1]}),this.internal({gestures:[a.Any,M],actions:[a.Array,[]],inspectors:[a.Array,[]],help:[a.Array,[]],toolbar_location:[a.Location,\"right\"]})}initialize(){super.initialize(),this._init_tools()}_init_tools(){const t=function(t,o){if(t.length!=o.length)return!0;const e=new Set(o.map(t=>t.id));return _.some(t,t=>!e.has(t.id))},o=this.tools.filter(t=>t instanceof p.InspectTool);t(this.inspectors,o)&&(this.inspectors=o);const e=this.tools.filter(t=>t instanceof b.HelpTool);t(this.help,e)&&(this.help=e);const i=this.tools.filter(t=>t instanceof d.ActionTool);t(this.actions,i)&&(this.actions=i);const l=(t,o)=>{t in this.gestures||s.logger.warn(`Toolbar: unknown event type '${t}' for tool: ${o}`)},n={pan:{tools:[],active:null},scroll:{tools:[],active:null},pinch:{tools:[],active:null},tap:{tools:[],active:null},doubletap:{tools:[],active:null},press:{tools:[],active:null},pressup:{tools:[],active:null},rotate:{tools:[],active:null},move:{tools:[],active:null},multi:{tools:[],active:null}};for(const t of this.tools)if(t instanceof v.GestureTool&&t.event_type)if(h.isString(t.event_type))n[t.event_type].tools.push(t),l(t.event_type,t);else{n.multi.tools.push(t);for(const o of t.event_type)l(o,t)}for(const o of Object.keys(n)){const e=this.gestures[o];t(e.tools,n[o].tools)&&(e.tools=n[o].tools),e.active&&_.every(e.tools,t=>t.id!=e.active.id)&&(e.active=null)}}get horizontal(){return\"above\"===this.toolbar_location||\"below\"===this.toolbar_location}get vertical(){return\"left\"===this.toolbar_location||\"right\"===this.toolbar_location}_active_change(t){const{event_type:o}=t;if(null==o)return;const e=h.isString(o)?[o]:o;for(const o of e)if(t.active){const e=this.gestures[o].active;null!=e&&t!=e&&(s.logger.debug(`Toolbar: deactivating tool: ${e} for event type '${o}'`),e.active=!1),this.gestures[o].active=t,s.logger.debug(`Toolbar: activating tool: ${t} for event type '${o}'`)}else this.gestures[o].active=null}}e.ToolbarBase=B,B.__name__=\"ToolbarBase\",B.init_ToolbarBase()},\n function _(e,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=e(296),n=e(304);class u extends s.ButtonToolView{}t.GestureToolView=u,u.__name__=\"GestureToolView\";class _ extends s.ButtonTool{constructor(e){super(e),this.button_view=n.OnOffButtonView}}t.GestureTool=_,_.__name__=\"GestureTool\"},\n function _(o,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const e=o(296),i=o(15);class s extends e.ButtonToolButtonView{_clicked(){this.model.do.emit(void 0)}}n.ActionToolButtonView=s,s.__name__=\"ActionToolButtonView\";class c extends e.ButtonToolView{connect_signals(){super.connect_signals(),this.connect(this.model.do,o=>this.doit(o))}}n.ActionToolView=c,c.__name__=\"ActionToolView\";class l extends e.ButtonTool{constructor(o){super(o),this.button_view=s,this.do=new i.Signal(this,\"do\")}}n.ActionTool=l,l.__name__=\"ActionTool\"},\n function _(o,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const i=o(1),l=o(307),s=i.__importStar(o(18)),n=o(309);class _ extends l.ActionToolView{doit(){window.open(this.model.redirect)}}t.HelpToolView=_,_.__name__=\"HelpToolView\";class r extends l.ActionTool{constructor(o){super(o),this.tool_name=\"Help\",this.icon=n.bk_tool_icon_help}static init_HelpTool(){this.prototype.default_view=_,this.define({help_tooltip:[s.String,\"Click the question mark to learn more about Bokeh plot tools.\"],redirect:[s.String,\"https://docs.bokeh.org/en/latest/docs/user_guide/tools.html\"]}),this.register_alias(\"help\",()=>new r)}get tooltip(){return this.help_tooltip}}t.HelpTool=r,r.__name__=\"HelpTool\",r.init_HelpTool()},\n function _(o,_,l){Object.defineProperty(l,\"__esModule\",{value:!0}),l.bk_tool_icon_box_select=\"bk-tool-icon-box-select\",l.bk_tool_icon_box_zoom=\"bk-tool-icon-box-zoom\",l.bk_tool_icon_zoom_in=\"bk-tool-icon-zoom-in\",l.bk_tool_icon_zoom_out=\"bk-tool-icon-zoom-out\",l.bk_tool_icon_help=\"bk-tool-icon-help\",l.bk_tool_icon_hover=\"bk-tool-icon-hover\",l.bk_tool_icon_crosshair=\"bk-tool-icon-crosshair\",l.bk_tool_icon_lasso_select=\"bk-tool-icon-lasso-select\",l.bk_tool_icon_pan=\"bk-tool-icon-pan\",l.bk_tool_icon_xpan=\"bk-tool-icon-xpan\",l.bk_tool_icon_ypan=\"bk-tool-icon-ypan\",l.bk_tool_icon_range=\"bk-tool-icon-range\",l.bk_tool_icon_polygon_select=\"bk-tool-icon-polygon-select\",l.bk_tool_icon_redo=\"bk-tool-icon-redo\",l.bk_tool_icon_reset=\"bk-tool-icon-reset\",l.bk_tool_icon_save=\"bk-tool-icon-save\",l.bk_tool_icon_tap_select=\"bk-tool-icon-tap-select\",l.bk_tool_icon_undo=\"bk-tool-icon-undo\",l.bk_tool_icon_wheel_pan=\"bk-tool-icon-wheel-pan\",l.bk_tool_icon_wheel_zoom=\"bk-tool-icon-wheel-zoom\",l.bk_tool_icon_box_edit=\"bk-tool-icon-box-edit\",l.bk_tool_icon_freehand_draw=\"bk-tool-icon-freehand-draw\",l.bk_tool_icon_poly_draw=\"bk-tool-icon-poly-draw\",l.bk_tool_icon_point_draw=\"bk-tool-icon-point-draw\",l.bk_tool_icon_poly_edit=\"bk-tool-icon-poly-edit\",l.bk_tool_icon_line_edit=\"bk-tool-icon-line-edit\"},\n function _(o,l,b){Object.defineProperty(b,\"__esModule\",{value:!0}),b.bk_logo=\"bk-logo\",b.bk_logo_notebook=\"bk-logo-notebook\",b.bk_logo_small=\"bk-logo-small\",b.bk_grey=\"bk-grey\"},\n function _(l,n,o){Object.defineProperty(o,\"__esModule\",{value:!0});o.default=\"\\n.bk-root .bk-logo {\\n margin: 5px;\\n position: relative;\\n display: block;\\n background-repeat: no-repeat;\\n}\\n.bk-root .bk-logo.bk-grey {\\n filter: url(\\\"data:image/svg+xml;utf8,#grayscale\\\");\\n /* Firefox 10+, Firefox on Android */\\n filter: gray;\\n /* IE6-9 */\\n -webkit-filter: grayscale(100%);\\n /* Chrome 19+, Safari 6+, Safari 6+ iOS */\\n}\\n.bk-root .bk-logo-small {\\n width: 20px;\\n height: 20px;\\n background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAOkSURBVDiNjZRtaJVlGMd/1/08zzln5zjP1LWcU9N0NkN8m2CYjpgQYQXqSs0I84OLIC0hkEKoPtiH3gmKoiJDU7QpLgoLjLIQCpEsNJ1vqUOdO7ppbuec5+V+rj4ctwzd8IIbbi6u+8f1539dt3A78eXC7QizUF7gyV1fD1Yqg4JWz84yffhm0qkFqBogB9rM8tZdtwVsPUhWhGcFJngGeWrPzHm5oaMmkfEg1usvLFyc8jLRqDOMru7AyC8saQr7GG7f5fvDeH7Ej8CM66nIF+8yngt6HWaKh7k49Soy9nXurCi1o3qUbS3zWfrYeQDTB/Qj6kX6Ybhw4B+bOYoLKCC9H3Nu/leUTZ1JdRWkkn2ldcCamzrcf47KKXdAJllSlxAOkRgyHsGC/zRday5Qld9DyoM4/q/rUoy/CXh3jzOu3bHUVZeU+DEn8FInkPBFlu3+nW3Nw0mk6vCDiWg8CeJaxEwuHS3+z5RgY+YBR6V1Z1nxSOfoaPa4LASWxxdNp+VWTk7+4vzaou8v8PN+xo+KY2xsw6une2frhw05CTYOmQvsEhjhWjn0bmXPjpE1+kplmmkP3suftwTubK9Vq22qKmrBhpY4jvd5afdRA3wGjFAgcnTK2s4hY0/GPNIb0nErGMCRxWOOX64Z8RAC4oCXdklmEvcL8o0BfkNK4lUg9HTl+oPlQxdNo3Mg4Nv175e/1LDGzZen30MEjRUtmXSfiTVu1kK8W4txyV6BMKlbgk3lMwYCiusNy9fVfvvwMxv8Ynl6vxoByANLTWplvuj/nF9m2+PDtt1eiHPBr1oIfhCChQMBw6Aw0UulqTKZdfVvfG7VcfIqLG9bcldL/+pdWTLxLUy8Qq38heUIjh4XlzZxzQm19lLFlr8vdQ97rjZVOLf8nclzckbcD4wxXMidpX30sFd37Fv/GtwwhzhxGVAprjbg0gCAEeIgwCZyTV2Z1REEW8O4py0wsjeloKoMr6iCY6dP92H6Vw/oTyICIthibxjm/DfN9lVz8IqtqKYLUXfoKVMVQVVJOElGjrnnUt9T9wbgp8AyYKaGlqingHZU/uG2NTZSVqwHQTWkx9hxjkpWDaCg6Ckj5qebgBVbT3V3NNXMSiWSDdGV3hrtzla7J+duwPOToIg42ChPQOQjspnSlp1V+Gjdged7+8UN5CRAV7a5EdFNwCjEaBR27b3W890TE7g24NAP/mMDXRWrGoFPQI9ls/MWO2dWFAar/xcOIImbbpA3zgAAAABJRU5ErkJggg==);\\n}\\n.bk-root .bk-logo-notebook {\\n display: inline-block;\\n vertical-align: middle;\\n margin-right: 5px;\\n}\\n\"},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});var s=this&&this.__rest||function(t,e){var i={};for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&e.indexOf(s)<0&&(i[s]=t[s]);if(null!=t&&\"function\"==typeof Object.getOwnPropertySymbols){var n=0;for(s=Object.getOwnPropertySymbols(t);nt)}}request_layout(){this._needs_layout=!0,this.request_paint()}reset(){\"standard\"==this.model.reset_policy&&(this.clear_state(),this.reset_range(),this.reset_selection()),this.model.trigger_event(new c.Reset)}remove(){this.ui_event_bus.destroy(),p.remove_views(this.renderer_views),p.remove_views(this.tool_views),this.canvas_view.remove(),super.remove()}render(){super.render(),this.el.appendChild(this.canvas_view.el),this.canvas_view.render()}initialize(){this.pause(),super.initialize(),this.state_changed=new u.Signal0(this,\"state_changed\"),this.lod_started=!1,this.visuals=new b.Visuals(this.model),this._initial_state_info={selection:new Map,dimensions:{width:0,height:0}},this.visibility_callbacks=[],this.state={history:[],index:-1};const{hidpi:t,output_backend:e}=this.model;this.canvas=new a.Canvas({hidpi:t,output_backend:e}),this.frame=new n.CartesianFrame(this.model.x_scale,this.model.y_scale,this.model.x_range,this.model.y_range,this.model.extra_x_ranges,this.model.extra_y_ranges),this.throttled_paint=m.throttle(()=>this.repaint(),1e3/60);const{title_location:i,title:s}=this.model;null!=i&&null!=s&&(this._title=s instanceof h.Title?s:new h.Title({text:s}));const{toolbar_location:o,toolbar:l}=this.model;null!=o&&null!=l&&(this._toolbar=new d.ToolbarPanel({toolbar:l}),l.toolbar_location=o),this.renderer_views=new Map,this.tool_views=new Map}async lazy_initialize(){this.canvas_view=await p.build_view(this.canvas,{parent:this}),this.ui_event_bus=new f.UIEvents(this,this.model.toolbar,this.canvas_view.events_el),await this.build_renderer_views(),await this.build_tool_views(),this.update_dataranges(),this.unpause(!0),g.logger.debug(\"PlotView initialized\")}_width_policy(){return null==this.model.frame_width?super._width_policy():\"min\"}_height_policy(){return null==this.model.frame_height?super._height_policy():\"min\"}_update_layout(){this.layout=new x.BorderLayout,this.layout.set_sizing(this.box_sizing());const{frame_width:t,frame_height:e}=this.model;this.layout.center_panel=this.frame,this.layout.center_panel.set_sizing(Object.assign(Object.assign({},null!=t?{width_policy:\"fixed\",width:t}:{width_policy:\"fit\"}),null!=e?{height_policy:\"fixed\",height:e}:{height_policy:\"fit\"}));const i=w.copy(this.model.above),s=w.copy(this.model.below),n=w.copy(this.model.left),a=w.copy(this.model.right),o=t=>{switch(t){case\"above\":return i;case\"below\":return s;case\"left\":return n;case\"right\":return a}},{title_location:l,title:r}=this.model;null!=l&&null!=r&&o(l).push(this._title);const{toolbar_location:_,toolbar:c}=this.model;if(null!=_&&null!=c){const t=o(_);let e=!0;if(this.model.toolbar_sticky)for(let i=0;i{const i=this.renderer_views.get(e);return i.layout=new z.SidePanel(t,i)},p=(t,e)=>{const i=\"above\"==t||\"below\"==t,s=[];for(const n of e)if(v.isArray(n)){const e=n.map(e=>{const s=u(t,e);if(e instanceof d.ToolbarPanel){const t=i?\"width_policy\":\"height_policy\";s.set_sizing(Object.assign(Object.assign({},s.sizing),{[t]:\"min\"}))}return s});let a;i?(a=new M.Row(e),a.set_sizing({width_policy:\"max\",height_policy:\"min\"})):(a=new M.Column(e),a.set_sizing({width_policy:\"min\",height_policy:\"max\"})),a.absolute=!0,s.push(a)}else s.push(u(t,n));return s},f=null!=this.model.min_border?this.model.min_border:0;this.layout.min_border={left:null!=this.model.min_border_left?this.model.min_border_left:f,top:null!=this.model.min_border_top?this.model.min_border_top:f,right:null!=this.model.min_border_right?this.model.min_border_right:f,bottom:null!=this.model.min_border_bottom?this.model.min_border_bottom:f};const b=new y.VStack,g=new y.VStack,m=new y.HStack,O=new y.HStack;b.children=w.reversed(p(\"above\",i)),g.children=p(\"below\",s),m.children=w.reversed(p(\"left\",n)),O.children=p(\"right\",a),b.set_sizing({width_policy:\"fit\",height_policy:\"min\"}),g.set_sizing({width_policy:\"fit\",height_policy:\"min\"}),m.set_sizing({width_policy:\"min\",height_policy:\"fit\"}),O.set_sizing({width_policy:\"min\",height_policy:\"fit\"}),this.layout.top_panel=b,this.layout.bottom_panel=g,this.layout.left_panel=m,this.layout.right_panel=O}get axis_views(){const t=[];for(const[,e]of this.renderer_views)e instanceof _.AxisView&&t.push(e);return t}set_cursor(t=\"default\"){this.canvas_view.el.style.cursor=t}set_toolbar_visibility(t){for(const e of this.visibility_callbacks)e(t)}update_dataranges(){const t=new Map,e=new Map;let i=!1;for(const[,t]of this.frame.x_ranges)t instanceof o.DataRange1d&&\"log\"==t.scale_hint&&(i=!0);for(const[,t]of this.frame.y_ranges)t instanceof o.DataRange1d&&\"log\"==t.scale_hint&&(i=!0);for(const[s,n]of this.renderer_views)if(n instanceof l.GlyphRendererView){const a=n.glyph.bounds();if(null!=a&&t.set(s,a),i){const t=n.glyph.log_bounds();null!=t&&e.set(s,t)}}let s=!1,n=!1;const{width:a,height:r}=this.frame.bbox;let h;!1!==this.model.match_aspect&&0!=a&&0!=r&&(h=1/this.model.aspect_scale*(a/r));for(const[,i]of this.frame.x_ranges){if(i instanceof o.DataRange1d){const n=\"log\"==i.scale_hint?e:t;i.update(n,0,this.model,h),i.follow&&(s=!0)}null!=i.bounds&&(n=!0)}for(const[,i]of this.frame.y_ranges){if(i instanceof o.DataRange1d){const n=\"log\"==i.scale_hint?e:t;i.update(n,1,this.model,h),i.follow&&(s=!0)}null!=i.bounds&&(n=!0)}if(s&&n){g.logger.warn(\"Follow enabled so bounds are unset.\");for(const[,t]of this.frame.x_ranges)t.bounds=null;for(const[,t]of this.frame.y_ranges)t.bounds=null}this.range_update_timestamp=Date.now()}push_state(t,e){const{history:i,index:s}=this.state,n=null!=i[s]?i[s].info:{},a=Object.assign(Object.assign(Object.assign({},this._initial_state_info),n),e);this.state.history=this.state.history.slice(0,this.state.index+1),this.state.history.push({type:t,info:a}),this.state.index=this.state.history.length-1,this.state_changed.emit()}clear_state(){this.state={history:[],index:-1},this.state_changed.emit()}can_undo(){return this.state.index>=0}can_redo(){return this.state.index=a.end&&(n=!0,a.end=t,(e||i)&&(a.start=t+l)),null!=o&&o<=a.start&&(n=!0,a.start=o,(e||i)&&(a.end=o-l))):(null!=t&&t>=a.start&&(n=!0,a.start=t,(e||i)&&(a.end=t+l)),null!=o&&o<=a.end&&(n=!0,a.end=o,(e||i)&&(a.start=o-l)))}}if(!(i&&n&&s))for(const[e,i]of t)e.have_updated_interactively=!0,e.start==i.start&&e.end==i.end||e.setv(i)}_get_weight_to_constrain_interval(t,e){const{min_interval:i}=t;let{max_interval:s}=t;if(null!=t.bounds&&\"auto\"!=t.bounds){const[e,i]=t.bounds;if(null!=e&&null!=i){const t=Math.abs(i-e);s=null!=s?Math.min(s,t):t}}let n=1;if(null!=i||null!=s){const a=Math.abs(t.end-t.start),o=Math.abs(e.end-e.start);i>0&&o0&&o>s&&(n=(s-a)/(o-a)),n=Math.max(0,Math.min(1,n))}return n}update_range(t,e=!1,i=!1,s=!0){this.pause();const{x_ranges:n,y_ranges:a}=this.frame;if(null==t){for(const[,t]of n)t.reset();for(const[,t]of a)t.reset();this.update_dataranges()}else{const o=[];for(const[e,i]of n)o.push([i,t.xrs.get(e)]);for(const[e,i]of a)o.push([i,t.yrs.get(e)]);i&&this._update_ranges_together(o),this._update_ranges_individually(o,e,i,s)}this.unpause()}reset_range(){this.update_range(null)}_invalidate_layout(){(()=>{for(const t of this.model.side_panels){if(this.renderer_views.get(t).layout.has_size_changed())return!0}return!1})()&&this.root.compute_layout()}get_renderer_views(){return this.computed_renderers.map(t=>this.renderer_views.get(t))}async build_renderer_views(){this.computed_renderers=[];const{above:t,below:e,left:i,right:s,center:n,renderers:a}=this.model;this.computed_renderers.push(...t,...e,...i,...s,...n,...a),null!=this._title&&this.computed_renderers.push(this._title),null!=this._toolbar&&this.computed_renderers.push(this._toolbar);for(const t of this.model.toolbar.tools)null!=t.overlay&&this.computed_renderers.push(t.overlay),this.computed_renderers.push(...t.synthetic_renderers);await p.build_views(this.renderer_views,this.computed_renderers,{parent:this})}async build_tool_views(){const t=this.model.toolbar.tools;(await p.build_views(this.tool_views,t,{parent:this})).map(t=>this.ui_event_bus.register_tool(t))}connect_signals(){super.connect_signals();const{x_ranges:t,y_ranges:e}=this.frame;for(const[,e]of t)this.connect(e.change,()=>{this._needs_layout=!0,this.request_paint()});for(const[,t]of e)this.connect(t.change,()=>{this._needs_layout=!0,this.request_paint()});const{plot_width:i,plot_height:s}=this.model.properties;this.on_change([i,s],()=>this.invalidate_layout());const{above:n,below:a,left:o,right:l,center:r,renderers:h}=this.model.properties;this.on_change([n,a,o,l,r,h],async()=>await this.build_renderer_views()),this.connect(this.model.toolbar.properties.tools.change,async()=>{await this.build_renderer_views(),await this.build_tool_views()}),this.connect(this.model.change,()=>this.request_paint()),this.connect(this.model.reset,()=>this.reset())}set_initial_range(){let t=!0;const{x_ranges:e,y_ranges:i}=this.frame,s=new Map,n=new Map;for(const[i,n]of e){const{start:e,end:a}=n;if(null==e||null==a||isNaN(e+a)){t=!1;break}s.set(i,{start:e,end:a})}if(t)for(const[e,s]of i){const{start:i,end:a}=s;if(null==i||null==a||isNaN(i+a)){t=!1;break}n.set(e,{start:i,end:a})}t?(this._initial_state_info.range={xrs:s,yrs:n},g.logger.debug(\"initial ranges set\")):g.logger.warn(\"could not set initial ranges\")}has_finished(){if(!super.has_finished())return!1;if(this.model.visible)for(const[,t]of this.renderer_views)if(!t.has_finished())return!1;return!0}after_layout(){if(super.after_layout(),this._needs_layout=!1,this.model.setv({inner_width:Math.round(this.frame.bbox.width),inner_height:Math.round(this.frame.bbox.height),outer_width:Math.round(this.layout.bbox.width),outer_height:Math.round(this.layout.bbox.height)},{no_change:!0}),!1!==this.model.match_aspect&&(this.pause(),this.update_dataranges(),this.unpause(!0)),!this._outer_bbox.equals(this.layout.bbox)){const{width:t,height:e}=this.layout.bbox;this.canvas_view.resize(t,e),this._outer_bbox=this.layout.bbox,this._invalidate_all=!0,this._needs_paint=!0}this._inner_bbox.equals(this.frame.inner_bbox)||(this._inner_bbox=this.layout.inner_bbox,this._needs_paint=!0),this._needs_paint&&this.paint()}repaint(){this._needs_layout&&this._invalidate_layout(),this.paint()}paint(){if(this.is_paused||!this.model.visible)return;g.logger.trace(\"PlotView.paint() for \"+this.model.id);const{document:t}=this.model;if(null!=t){const e=t.interactive_duration();e>=0&&e{t.interactive_duration()>this.model.lod_timeout&&t.interactive_stop(),this.request_paint()},this.model.lod_timeout):t.interactive_stop()}for(const[,t]of this.renderer_views)if(null==this.range_update_timestamp||t instanceof l.GlyphRendererView&&t.set_data_timestamp>this.range_update_timestamp){this.update_dataranges();break}let e=!1,i=!1;if(this._invalidate_all)e=!0,i=!0;else for(const t of this._invalidated_painters){const{level:s}=t.model;if(\"overlay\"!=s?e=!0:i=!0,e&&i)break}this._invalidated_painters.clear(),this._invalidate_all=!1;const s=[this.frame.bbox.left,this.frame.bbox.top,this.frame.bbox.width,this.frame.bbox.height],{primary:n,overlays:a}=this.canvas_view;e&&(n.prepare(),this.canvas_view.prepare_webgl(s),this.canvas_view.clear_webgl(),this._map_hook(n.ctx,s),this._paint_empty(n.ctx,s),this._paint_outline(n.ctx,s),this._paint_levels(n.ctx,\"image\",s,!0),this._paint_levels(n.ctx,\"underlay\",s,!0),this._paint_levels(n.ctx,\"glyph\",s,!0),this._paint_levels(n.ctx,\"guide\",s,!1),this._paint_levels(n.ctx,\"annotation\",s,!1),n.finish()),i&&(a.prepare(),this._paint_levels(a.ctx,\"overlay\",s,!1),a.finish()),null==this._initial_state_info.range&&this.set_initial_range(),this._needs_paint=!1}_paint_levels(t,e,i,s){for(const n of this.computed_renderers){if(n.level!=e)continue;const a=this.renderer_views.get(n);t.save(),(s||a.needs_clip)&&(t.beginPath(),t.rect(...i),t.clip()),a.render(),t.restore(),a.has_webgl&&a.needs_webgl_blit&&(this.canvas_view.blit_webgl(t),this.canvas_view.clear_webgl())}}_map_hook(t,e){}_paint_empty(t,e){const[i,s,n,a]=[0,0,this.layout.bbox.width,this.layout.bbox.height],[o,l,r,h]=e;this.visuals.border_fill.doit&&(this.visuals.border_fill.set_value(t),t.fillRect(i,s,n,a),t.clearRect(o,l,r,h)),this.visuals.background_fill.doit&&(this.visuals.background_fill.set_value(t),t.fillRect(o,l,r,h))}_paint_outline(t,e){if(this.visuals.outline_line.doit){t.save(),this.visuals.outline_line.set_value(t);let[i,s,n,a]=e;i+n==this.layout.bbox.width&&(n-=1),s+a==this.layout.bbox.height&&(a-=1),t.strokeRect(i,s,n,a),t.restore()}}to_blob(){return this.canvas_view.to_blob()}export(t,e=!0){const i=\"png\"==t?\"canvas\":\"svg\",s=new a.CanvasLayer(i,e),{width:n,height:o}=this.layout.bbox;s.resize(n,o);const{canvas:l}=this.canvas_view.compose();return s.ctx.drawImage(l,0,0),s}serializable_state(){const t=super.serializable_state(),{children:e}=t,i=s(t,[\"children\"]),n=this.get_renderer_views().map(t=>t.serializable_state()).filter(t=>\"bbox\"in t);return Object.assign(Object.assign({},i),{children:[...e,...n]})}}i.PlotView=k,k.__name__=\"PlotView\"},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});var n=this&&this.__decorate||function(e,t,s,n){var _,a=arguments.length,o=a<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,s):n;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,s,n);else for(var r=e.length-1;r>=0;r--)(_=e[r])&&(o=(a<3?_(o):a>3?_(t,s,o):_(t,s))||o);return a>3&&o&&Object.defineProperty(t,s,o),o};function _(e){return function(t){t.prototype.event_name=e}}class a{to_json(){const{event_name:e}=this;return{event_name:e,event_values:this._to_json()}}}s.BokehEvent=a,a.__name__=\"BokehEvent\";class o extends a{constructor(){super(...arguments),this.origin=null}_to_json(){return{model:this.origin}}}s.ModelEvent=o,o.__name__=\"ModelEvent\";let r=class extends a{_to_json(){return{}}};s.DocumentReady=r,r.__name__=\"DocumentReady\",s.DocumentReady=r=n([_(\"document_ready\")],r);let c=class extends o{};s.ButtonClick=c,c.__name__=\"ButtonClick\",s.ButtonClick=c=n([_(\"button_click\")],c);let l=class extends o{constructor(e){super(),this.item=e}_to_json(){const{item:e}=this;return Object.assign(Object.assign({},super._to_json()),{item:e})}};s.MenuItemClick=l,l.__name__=\"MenuItemClick\",s.MenuItemClick=l=n([_(\"menu_item_click\")],l);class i extends o{}s.UIEvent=i,i.__name__=\"UIEvent\";let u=class extends i{};s.LODStart=u,u.__name__=\"LODStart\",s.LODStart=u=n([_(\"lodstart\")],u);let d=class extends i{};s.LODEnd=d,d.__name__=\"LODEnd\",s.LODEnd=d=n([_(\"lodend\")],d);let h=class extends i{constructor(e,t){super(),this.geometry=e,this.final=t}_to_json(){const{geometry:e,final:t}=this;return Object.assign(Object.assign({},super._to_json()),{geometry:e,final:t})}};s.SelectionGeometry=h,h.__name__=\"SelectionGeometry\",s.SelectionGeometry=h=n([_(\"selectiongeometry\")],h);let m=class extends i{};s.Reset=m,m.__name__=\"Reset\",s.Reset=m=n([_(\"reset\")],m);class x extends i{constructor(e,t,s,n){super(),this.sx=e,this.sy=t,this.x=s,this.y=n}_to_json(){const{sx:e,sy:t,x:s,y:n}=this;return Object.assign(Object.assign({},super._to_json()),{sx:e,sy:t,x:s,y:n})}}s.PointEvent=x,x.__name__=\"PointEvent\";let p=class extends x{constructor(e,t,s,n,_,a){super(e,t,s,n),this.sx=e,this.sy=t,this.x=s,this.y=n,this.delta_x=_,this.delta_y=a}_to_json(){const{delta_x:e,delta_y:t}=this;return Object.assign(Object.assign({},super._to_json()),{delta_x:e,delta_y:t})}};s.Pan=p,p.__name__=\"Pan\",s.Pan=p=n([_(\"pan\")],p);let j=class extends x{constructor(e,t,s,n,_){super(e,t,s,n),this.sx=e,this.sy=t,this.x=s,this.y=n,this.scale=_}_to_json(){const{scale:e}=this;return Object.assign(Object.assign({},super._to_json()),{scale:e})}};s.Pinch=j,j.__name__=\"Pinch\",s.Pinch=j=n([_(\"pinch\")],j);let y=class extends x{constructor(e,t,s,n,_){super(e,t,s,n),this.sx=e,this.sy=t,this.x=s,this.y=n,this.rotation=_}_to_json(){const{rotation:e}=this;return Object.assign(Object.assign({},super._to_json()),{rotation:e})}};s.Rotate=y,y.__name__=\"Rotate\",s.Rotate=y=n([_(\"rotate\")],y);let P=class extends x{constructor(e,t,s,n,_){super(e,t,s,n),this.sx=e,this.sy=t,this.x=s,this.y=n,this.delta=_}_to_json(){const{delta:e}=this;return Object.assign(Object.assign({},super._to_json()),{delta:e})}};s.MouseWheel=P,P.__name__=\"MouseWheel\",s.MouseWheel=P=n([_(\"wheel\")],P);let v=class extends x{};s.MouseMove=v,v.__name__=\"MouseMove\",s.MouseMove=v=n([_(\"mousemove\")],v);let O=class extends x{};s.MouseEnter=O,O.__name__=\"MouseEnter\",s.MouseEnter=O=n([_(\"mouseenter\")],O);let b=class extends x{};s.MouseLeave=b,b.__name__=\"MouseLeave\",s.MouseLeave=b=n([_(\"mouseleave\")],b);let g=class extends x{};s.Tap=g,g.__name__=\"Tap\",s.Tap=g=n([_(\"tap\")],g);let E=class extends x{};s.DoubleTap=E,E.__name__=\"DoubleTap\",s.DoubleTap=E=n([_(\"doubletap\")],E);let M=class extends x{};s.Press=M,M.__name__=\"Press\",s.Press=M=n([_(\"press\")],M);let R=class extends x{};s.PressUp=R,R.__name__=\"PressUp\",s.PressUp=R=n([_(\"pressup\")],R);let f=class extends x{};s.PanStart=f,f.__name__=\"PanStart\",s.PanStart=f=n([_(\"panstart\")],f);let S=class extends x{};s.PanEnd=S,S.__name__=\"PanEnd\",s.PanEnd=S=n([_(\"panend\")],S);let D=class extends x{};s.PinchStart=D,D.__name__=\"PinchStart\",s.PinchStart=D=n([_(\"pinchstart\")],D);let k=class extends x{};s.PinchEnd=k,k.__name__=\"PinchEnd\",s.PinchEnd=k=n([_(\"pinchend\")],k);let L=class extends x{};s.RotateStart=L,L.__name__=\"RotateStart\",s.RotateStart=L=n([_(\"rotatestart\")],L);let C=class extends x{};s.RotateEnd=C,C.__name__=\"RotateEnd\",s.RotateEnd=C=n([_(\"rotateend\")],C)},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=t(1),i=n.__importDefault(t(297)),r=t(15),a=t(19),h=t(72),_=n.__importStar(t(313)),o=t(315),c=t(9),l=t(8),p=t(32),u=t(302);class d{constructor(t,e,s){this.plot_view=t,this.toolbar=e,this.hit_area=s,this.pan_start=new r.Signal(this,\"pan:start\"),this.pan=new r.Signal(this,\"pan\"),this.pan_end=new r.Signal(this,\"pan:end\"),this.pinch_start=new r.Signal(this,\"pinch:start\"),this.pinch=new r.Signal(this,\"pinch\"),this.pinch_end=new r.Signal(this,\"pinch:end\"),this.rotate_start=new r.Signal(this,\"rotate:start\"),this.rotate=new r.Signal(this,\"rotate\"),this.rotate_end=new r.Signal(this,\"rotate:end\"),this.tap=new r.Signal(this,\"tap\"),this.doubletap=new r.Signal(this,\"doubletap\"),this.press=new r.Signal(this,\"press\"),this.pressup=new r.Signal(this,\"pressup\"),this.move_enter=new r.Signal(this,\"move:enter\"),this.move=new r.Signal(this,\"move\"),this.move_exit=new r.Signal(this,\"move:exit\"),this.scroll=new r.Signal(this,\"scroll\"),this.keydown=new r.Signal(this,\"keydown\"),this.keyup=new r.Signal(this,\"keyup\"),this.hammer=new i.default(this.hit_area,{touchAction:\"auto\",inputClass:i.default.TouchMouseInput}),this._configure_hammerjs(),this.hit_area.addEventListener(\"mousemove\",t=>this._mouse_move(t)),this.hit_area.addEventListener(\"mouseenter\",t=>this._mouse_enter(t)),this.hit_area.addEventListener(\"mouseleave\",t=>this._mouse_exit(t)),this.hit_area.addEventListener(\"contextmenu\",t=>this._context_menu(t)),this.hit_area.addEventListener(\"wheel\",t=>this._mouse_wheel(t)),document.addEventListener(\"keydown\",this),document.addEventListener(\"keyup\",this),this.menu=new u.ContextMenu([],{prevent_hide:t=>2==t.button&&t.target==this.hit_area}),this.hit_area.appendChild(this.menu.el)}destroy(){this.menu.remove(),this.hammer.destroy(),document.removeEventListener(\"keydown\",this),document.removeEventListener(\"keyup\",this)}handleEvent(t){\"keydown\"==t.type?this._key_down(t):\"keyup\"==t.type&&this._key_up(t)}_configure_hammerjs(){this.hammer.get(\"doubletap\").recognizeWith(\"tap\"),this.hammer.get(\"tap\").requireFailure(\"doubletap\"),this.hammer.get(\"doubletap\").dropRequireFailure(\"tap\"),this.hammer.on(\"doubletap\",t=>this._doubletap(t)),this.hammer.on(\"tap\",t=>this._tap(t)),this.hammer.on(\"press\",t=>this._press(t)),this.hammer.on(\"pressup\",t=>this._pressup(t)),this.hammer.get(\"pan\").set({direction:i.default.DIRECTION_ALL}),this.hammer.on(\"panstart\",t=>this._pan_start(t)),this.hammer.on(\"pan\",t=>this._pan(t)),this.hammer.on(\"panend\",t=>this._pan_end(t)),this.hammer.get(\"pinch\").set({enable:!0}),this.hammer.on(\"pinchstart\",t=>this._pinch_start(t)),this.hammer.on(\"pinch\",t=>this._pinch(t)),this.hammer.on(\"pinchend\",t=>this._pinch_end(t)),this.hammer.get(\"rotate\").set({enable:!0}),this.hammer.on(\"rotatestart\",t=>this._rotate_start(t)),this.hammer.on(\"rotate\",t=>this._rotate(t)),this.hammer.on(\"rotateend\",t=>this._rotate_end(t))}register_tool(t){const e=t.model.event_type;null!=e&&(l.isString(e)?this._register_tool(t,e):e.forEach((e,s)=>this._register_tool(t,e,s<1)))}_register_tool(t,e,s=!0){const n=t,{id:i}=n.model,r=t=>e=>{e.id==i&&t(e.e)},h=t=>e=>{t(e.e)};switch(e){case\"pan\":null!=n._pan_start&&n.connect(this.pan_start,r(n._pan_start.bind(n))),null!=n._pan&&n.connect(this.pan,r(n._pan.bind(n))),null!=n._pan_end&&n.connect(this.pan_end,r(n._pan_end.bind(n)));break;case\"pinch\":null!=n._pinch_start&&n.connect(this.pinch_start,r(n._pinch_start.bind(n))),null!=n._pinch&&n.connect(this.pinch,r(n._pinch.bind(n))),null!=n._pinch_end&&n.connect(this.pinch_end,r(n._pinch_end.bind(n)));break;case\"rotate\":null!=n._rotate_start&&n.connect(this.rotate_start,r(n._rotate_start.bind(n))),null!=n._rotate&&n.connect(this.rotate,r(n._rotate.bind(n))),null!=n._rotate_end&&n.connect(this.rotate_end,r(n._rotate_end.bind(n)));break;case\"move\":null!=n._move_enter&&n.connect(this.move_enter,r(n._move_enter.bind(n))),null!=n._move&&n.connect(this.move,r(n._move.bind(n))),null!=n._move_exit&&n.connect(this.move_exit,r(n._move_exit.bind(n)));break;case\"tap\":null!=n._tap&&n.connect(this.tap,r(n._tap.bind(n)));break;case\"press\":null!=n._press&&n.connect(this.press,r(n._press.bind(n))),null!=n._pressup&&n.connect(this.pressup,r(n._pressup.bind(n)));break;case\"scroll\":null!=n._scroll&&n.connect(this.scroll,r(n._scroll.bind(n)));break;default:throw new Error(\"unsupported event_type: \"+e)}s&&(null!=n._doubletap&&n.connect(this.doubletap,h(n._doubletap.bind(n))),null!=n._keydown&&n.connect(this.keydown,h(n._keydown.bind(n))),null!=n._keyup&&n.connect(this.keyup,h(n._keyup.bind(n))),p.is_mobile&&null!=n._scroll&&\"pinch\"==e&&(a.logger.debug(\"Registering scroll on touch screen\"),n.connect(this.scroll,r(n._scroll.bind(n)))))}_hit_test_renderers(t,e){const s=this.plot_view.get_renderer_views();for(const n of c.reversed(s)){const{level:s}=n.model;if((\"annotation\"==s||\"overlay\"==s)&&null!=n.interactive_hit&&n.interactive_hit(t,e))return n}return null}_hit_test_frame(t,e){return this.plot_view.frame.bbox.contains(t,e)}_hit_test_canvas(t,e){return this.plot_view.layout.bbox.contains(t,e)}_trigger(t,e,s){const n=this.toolbar.gestures,i=t.name.split(\":\")[0],r=this._hit_test_renderers(e.sx,e.sy),a=this._hit_test_canvas(e.sx,e.sy);switch(i){case\"move\":{const s=n[i].active;null!=s&&this.trigger(t,e,s.id);const h=this.toolbar.inspectors.filter(t=>t.active);let _=\"default\";null!=r?(_=r.cursor(e.sx,e.sy)||_,c.is_empty(h)||(t=this.move_exit)):this._hit_test_frame(e.sx,e.sy)&&(c.is_empty(h)||(_=\"crosshair\")),this.plot_view.set_cursor(_),this.plot_view.set_toolbar_visibility(a),h.map(s=>this.trigger(t,e,s.id));break}case\"tap\":{const{target:a}=s;if(null!=a&&a!=this.hit_area)return;null!=r&&null!=r.on_hit&&r.on_hit(e.sx,e.sy);const h=n[i].active;null!=h&&this.trigger(t,e,h.id);break}case\"scroll\":{const i=n[p.is_mobile?\"pinch\":\"scroll\"].active;null!=i&&(s.preventDefault(),s.stopPropagation(),this.trigger(t,e,i.id));break}case\"pan\":{const r=n[i].active;null!=r&&(s.preventDefault(),this.trigger(t,e,r.id));break}default:{const s=n[i].active;null!=s&&this.trigger(t,e,s.id)}}this._trigger_bokeh_event(e)}trigger(t,e,s=null){t.emit({id:s,e})}_trigger_bokeh_event(t){const e=(()=>{const{sx:e,sy:s}=t,n=this.plot_view.frame.x_scale.invert(e),i=this.plot_view.frame.y_scale.invert(s);switch(t.type){case\"wheel\":return new _.MouseWheel(e,s,n,i,t.delta);case\"mousemove\":return new _.MouseMove(e,s,n,i);case\"mouseenter\":return new _.MouseEnter(e,s,n,i);case\"mouseleave\":return new _.MouseLeave(e,s,n,i);case\"tap\":return new _.Tap(e,s,n,i);case\"doubletap\":return new _.DoubleTap(e,s,n,i);case\"press\":return new _.Press(e,s,n,i);case\"pressup\":return new _.PressUp(e,s,n,i);case\"pan\":return new _.Pan(e,s,n,i,t.deltaX,t.deltaY);case\"panstart\":return new _.PanStart(e,s,n,i);case\"panend\":return new _.PanEnd(e,s,n,i);case\"pinch\":return new _.Pinch(e,s,n,i,t.scale);case\"pinchstart\":return new _.PinchStart(e,s,n,i);case\"pinchend\":return new _.PinchEnd(e,s,n,i);case\"rotate\":return new _.Rotate(e,s,n,i,t.rotation);case\"rotatestart\":return new _.RotateStart(e,s,n,i);case\"rotateend\":return new _.RotateEnd(e,s,n,i);default:return}})();null!=e&&this.plot_view.model.trigger_event(e)}_get_sxy(t){const{pageX:e,pageY:s}=function(t){return\"undefined\"!=typeof TouchEvent&&t instanceof TouchEvent}(t)?(0!=t.touches.length?t.touches:t.changedTouches)[0]:t,{left:n,top:i}=h.offset(this.hit_area);return{sx:e-n,sy:s-i}}_pan_event(t){return Object.assign(Object.assign({type:t.type},this._get_sxy(t.srcEvent)),{deltaX:t.deltaX,deltaY:t.deltaY,shiftKey:t.srcEvent.shiftKey,ctrlKey:t.srcEvent.ctrlKey})}_pinch_event(t){return Object.assign(Object.assign({type:t.type},this._get_sxy(t.srcEvent)),{scale:t.scale,shiftKey:t.srcEvent.shiftKey,ctrlKey:t.srcEvent.ctrlKey})}_rotate_event(t){return Object.assign(Object.assign({type:t.type},this._get_sxy(t.srcEvent)),{rotation:t.rotation,shiftKey:t.srcEvent.shiftKey,ctrlKey:t.srcEvent.ctrlKey})}_tap_event(t){return Object.assign(Object.assign({type:t.type},this._get_sxy(t.srcEvent)),{shiftKey:t.srcEvent.shiftKey,ctrlKey:t.srcEvent.ctrlKey})}_move_event(t){return Object.assign(Object.assign({type:t.type},this._get_sxy(t)),{shiftKey:t.shiftKey,ctrlKey:t.ctrlKey})}_scroll_event(t){return Object.assign(Object.assign({type:t.type},this._get_sxy(t)),{delta:o.getDeltaY(t),shiftKey:t.shiftKey,ctrlKey:t.ctrlKey})}_key_event(t){return{type:t.type,keyCode:t.keyCode}}_pan_start(t){const e=this._pan_event(t);e.sx-=t.deltaX,e.sy-=t.deltaY,this._trigger(this.pan_start,e,t.srcEvent)}_pan(t){this._trigger(this.pan,this._pan_event(t),t.srcEvent)}_pan_end(t){this._trigger(this.pan_end,this._pan_event(t),t.srcEvent)}_pinch_start(t){this._trigger(this.pinch_start,this._pinch_event(t),t.srcEvent)}_pinch(t){this._trigger(this.pinch,this._pinch_event(t),t.srcEvent)}_pinch_end(t){this._trigger(this.pinch_end,this._pinch_event(t),t.srcEvent)}_rotate_start(t){this._trigger(this.rotate_start,this._rotate_event(t),t.srcEvent)}_rotate(t){this._trigger(this.rotate,this._rotate_event(t),t.srcEvent)}_rotate_end(t){this._trigger(this.rotate_end,this._rotate_event(t),t.srcEvent)}_tap(t){this._trigger(this.tap,this._tap_event(t),t.srcEvent)}_doubletap(t){const e=this._tap_event(t);this._trigger_bokeh_event(e),this.trigger(this.doubletap,e)}_press(t){this._trigger(this.press,this._tap_event(t),t.srcEvent)}_pressup(t){this._trigger(this.pressup,this._tap_event(t),t.srcEvent)}_mouse_enter(t){this._trigger(this.move_enter,this._move_event(t),t)}_mouse_move(t){this._trigger(this.move,this._move_event(t),t)}_mouse_exit(t){this._trigger(this.move_exit,this._move_event(t),t)}_mouse_wheel(t){this._trigger(this.scroll,this._scroll_event(t),t)}_context_menu(t){!this.menu.is_open&&this.menu.can_open&&t.preventDefault();const{sx:e,sy:s}=this._get_sxy(t);this.menu.toggle({left:e,top:s})}_key_down(t){this.trigger(this.keydown,this._key_event(t))}_key_up(t){this.trigger(this.keyup,this._key_event(t))}}s.UIEvents=d,d.__name__=\"UIEvents\"},\n function _(e,t,n){\n /*!\n * jQuery Mousewheel 3.1.13\n *\n * Copyright jQuery Foundation and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n */\n function r(e){const t=getComputedStyle(e).fontSize;return null!=t?parseInt(t,10):null}Object.defineProperty(n,\"__esModule\",{value:!0}),n.getDeltaY=function(e){let t=-e.deltaY;if(e.target instanceof HTMLElement)switch(e.deltaMode){case e.DOM_DELTA_LINE:t*=r((n=e.target).offsetParent||document.body)||r(n)||16;break;case e.DOM_DELTA_PAGE:t*=function(e){return e.clientHeight}(e.target)}var n;return t}},\n function _(n,e,o){Object.defineProperty(o,\"__esModule\",{value:!0});const t=(\"undefined\"!=typeof window?window.requestAnimationFrame:void 0)||(\"undefined\"!=typeof window?window.webkitRequestAnimationFrame:void 0)||(\"undefined\"!=typeof window?window.mozRequestAnimationFrame:void 0)||(\"undefined\"!=typeof window?window.msRequestAnimationFrame:void 0)||function(n){return n(Date.now()),-1};o.throttle=function(n,e){let o=null,i=0,u=!1;return function(){return new Promise((d,w)=>{const r=function(){i=Date.now(),o=null,u=!1;try{n(),d()}catch(n){w(n)}},a=Date.now(),f=e-(a-i);f<=0&&!u?(null!=o&&clearTimeout(o),u=!0,t(r)):o||u?d():o=setTimeout(()=>t(r),f)})}}},\n function _(t,e,h){Object.defineProperty(h,\"__esModule\",{value:!0});const i=t(213),o=t(214),r=t(79);class s extends o.Layoutable{constructor(){super(...arguments),this.min_border={left:0,top:0,right:0,bottom:0}}_measure(t){t=new i.Sizeable(t).bounded_to(this.sizing.size);const e=this.left_panel.measure({width:0,height:t.height}),h=Math.max(e.width,this.min_border.left),o=this.right_panel.measure({width:0,height:t.height}),r=Math.max(o.width,this.min_border.right),s=this.top_panel.measure({width:t.width,height:0}),n=Math.max(s.height,this.min_border.top),a=this.bottom_panel.measure({width:t.width,height:0}),g=Math.max(a.height,this.min_border.bottom),_=new i.Sizeable(t).shrink_by({left:h,right:r,top:n,bottom:g}),m=this.center_panel.measure(_);return{width:h+m.width+r,height:n+m.height+g,inner:{left:h,right:r,top:n,bottom:g},align:(()=>{const{width_policy:t,height_policy:e}=this.center_panel.sizing;return\"fixed\"!=t&&\"fixed\"!=e})()}}_set_geometry(t,e){super._set_geometry(t,e),this.center_panel.set_geometry(e);const h=this.left_panel.measure({width:0,height:t.height}),i=this.right_panel.measure({width:0,height:t.height}),o=this.top_panel.measure({width:t.width,height:0}),s=this.bottom_panel.measure({width:t.width,height:0}),{left:n,top:a,right:g,bottom:_}=e;this.top_panel.set_geometry(new r.BBox({left:n,right:g,bottom:a,height:o.height})),this.bottom_panel.set_geometry(new r.BBox({left:n,right:g,top:_,height:s.height})),this.left_panel.set_geometry(new r.BBox({top:a,bottom:_,right:n,width:h.width})),this.right_panel.set_geometry(new r.BBox({top:a,bottom:_,left:g,width:i.width}))}}h.BorderLayout=s,s.__name__=\"BorderLayout\"},\n function _(i,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const l=i(213),a=i(214),r=i(8),o=Math.PI/2,h=\"left\",s=\"center\",n={above:{parallel:0,normal:-o,horizontal:0,vertical:-o},below:{parallel:0,normal:o,horizontal:0,vertical:o},left:{parallel:-o,normal:0,horizontal:0,vertical:-o},right:{parallel:o,normal:0,horizontal:0,vertical:o}},d={above:{justified:\"top\",parallel:\"alphabetic\",normal:\"middle\",horizontal:\"alphabetic\",vertical:\"middle\"},below:{justified:\"bottom\",parallel:\"hanging\",normal:\"middle\",horizontal:\"hanging\",vertical:\"middle\"},left:{justified:\"top\",parallel:\"alphabetic\",normal:\"middle\",horizontal:\"middle\",vertical:\"alphabetic\"},right:{justified:\"top\",parallel:\"alphabetic\",normal:\"middle\",horizontal:\"middle\",vertical:\"alphabetic\"}},_={above:{justified:s,parallel:s,normal:h,horizontal:s,vertical:h},below:{justified:s,parallel:s,normal:h,horizontal:s,vertical:h},left:{justified:s,parallel:s,normal:\"right\",horizontal:\"right\",vertical:s},right:{justified:s,parallel:s,normal:h,horizontal:h,vertical:s}},c={above:\"right\",below:h,left:\"right\",right:h},m={above:h,below:\"right\",left:\"right\",right:h};class g extends a.ContentLayoutable{constructor(i,t){switch(super(),this.side=i,this.obj=t,this.side){case\"above\":this._dim=0,this._normals=[0,-1];break;case\"below\":this._dim=0,this._normals=[0,1];break;case\"left\":this._dim=1,this._normals=[-1,0];break;case\"right\":this._dim=1,this._normals=[1,0]}this.is_horizontal?this.set_sizing({width_policy:\"max\",height_policy:\"fixed\"}):this.set_sizing({width_policy:\"fixed\",height_policy:\"max\"})}_content_size(){return new l.Sizeable(this.get_oriented_size())}get_oriented_size(){const{width:i,height:t}=this.obj.get_size();return!this.obj.rotate||this.is_horizontal?{width:i,height:t}:{width:t,height:i}}has_size_changed(){const{width:i,height:t}=this.get_oriented_size();return this.is_horizontal?this.bbox.height!=t:this.bbox.width!=i}get dimension(){return this._dim}get normals(){return this._normals}get is_horizontal(){return 0==this._dim}get is_vertical(){return 1==this._dim}apply_label_text_heuristics(i,t){const e=this.side;let l,a;r.isString(t)?(l=d[e][t],a=_[e][t]):t<0?(l=\"middle\",a=c[e]):(l=\"middle\",a=m[e]),i.textBaseline=l,i.textAlign=a}get_label_angle_heuristic(i){return n[this.side][i]}}e.SidePanel=g,g.__name__=\"SidePanel\"},\n function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=t(15),o=t(72),a=t(37),n=t(312),p=new i.Signal0({},\"gmaps_ready\");class l extends n.PlotView{initialize(){this.pause(),super.initialize(),this._tiles_loaded=!1,this.zoom_count=0;const{zoom:t,lat:e,lng:s}=this.model.map_options;if(this.initial_zoom=t,this.initial_lat=e,this.initial_lng=s,\"undefined\"==typeof google||null==google.maps){if(void 0===window._bokeh_gmaps_callback){!function(t){window._bokeh_gmaps_callback=()=>p.emit();const e=document.createElement(\"script\");e.type=\"text/javascript\",e.src=`https://maps.googleapis.com/maps/api/js?v=3.36&key=${t}&callback=_bokeh_gmaps_callback`,document.body.appendChild(e)}(atob(this.model.api_key))}p.connect(()=>this.request_render())}this.unpause()}remove(){o.remove(this.map_el),super.remove()}update_range(t){if(null==t)this.map.setCenter({lat:this.initial_lat,lng:this.initial_lng}),this.map.setOptions({zoom:this.initial_zoom}),super.update_range(null);else if(null!=t.sdx||null!=t.sdy)this.map.panBy(t.sdx||0,t.sdy||0),super.update_range(t);else if(null!=t.factor){if(10!==this.zoom_count)return void(this.zoom_count+=1);this.zoom_count=0,this.pause(),super.update_range(t);const e=t.factor<0?-1:1,s=this.map.getZoom(),i=s+e;if(i>=2){this.map.setZoom(i);const[t,e,,]=this._get_projected_bounds();e-t<0&&this.map.setZoom(s)}this.unpause()}this._set_bokeh_ranges()}_build_map(){const{maps:t}=google;this.map_types={satellite:t.MapTypeId.SATELLITE,terrain:t.MapTypeId.TERRAIN,roadmap:t.MapTypeId.ROADMAP,hybrid:t.MapTypeId.HYBRID};const e=this.model.map_options,s={center:new t.LatLng(e.lat,e.lng),zoom:e.zoom,disableDefaultUI:!0,mapTypeId:this.map_types[e.map_type],scaleControl:e.scale_control,tilt:e.tilt};null!=e.styles&&(s.styles=JSON.parse(e.styles)),this.map_el=o.div({style:{position:\"absolute\"}}),this.canvas_view.add_underlay(this.map_el),this.map=new t.Map(this.map_el,s),t.event.addListener(this.map,\"idle\",()=>this._set_bokeh_ranges()),t.event.addListener(this.map,\"bounds_changed\",()=>this._set_bokeh_ranges()),t.event.addListenerOnce(this.map,\"tilesloaded\",()=>this._render_finished()),this.connect(this.model.properties.map_options.change,()=>this._update_options()),this.connect(this.model.map_options.properties.styles.change,()=>this._update_styles()),this.connect(this.model.map_options.properties.lat.change,()=>this._update_center(\"lat\")),this.connect(this.model.map_options.properties.lng.change,()=>this._update_center(\"lng\")),this.connect(this.model.map_options.properties.zoom.change,()=>this._update_zoom()),this.connect(this.model.map_options.properties.map_type.change,()=>this._update_map_type()),this.connect(this.model.map_options.properties.scale_control.change,()=>this._update_scale_control()),this.connect(this.model.map_options.properties.tilt.change,()=>this._update_tilt())}_render_finished(){this._tiles_loaded=!0,this.notify_finished()}has_finished(){return super.has_finished()&&!0===this._tiles_loaded}_get_latlon_bounds(){const t=this.map.getBounds(),e=t.getNorthEast(),s=t.getSouthWest();return[s.lng(),e.lng(),s.lat(),e.lat()]}_get_projected_bounds(){const[t,e,s,i]=this._get_latlon_bounds(),[o,n]=a.wgs84_mercator.compute(t,s),[p,l]=a.wgs84_mercator.compute(e,i);return[o,p,n,l]}_set_bokeh_ranges(){const[t,e,s,i]=this._get_projected_bounds();this.frame.x_range.setv({start:t,end:e}),this.frame.y_range.setv({start:s,end:i})}_update_center(t){const e=this.map.getCenter().toJSON();e[t]=this.model.map_options[t],this.map.setCenter(e),this._set_bokeh_ranges()}_update_map_type(){this.map.setOptions({mapTypeId:this.map_types[this.model.map_options.map_type]})}_update_scale_control(){this.map.setOptions({scaleControl:this.model.map_options.scale_control})}_update_tilt(){this.map.setOptions({tilt:this.model.map_options.tilt})}_update_options(){this._update_styles(),this._update_center(\"lat\"),this._update_center(\"lng\"),this._update_zoom(),this._update_map_type()}_update_styles(){this.map.setOptions({styles:JSON.parse(this.model.map_options.styles)})}_update_zoom(){this.map.setOptions({zoom:this.model.map_options.zoom}),this._set_bokeh_ranges()}_map_hook(t,e){if(null==this.map&&\"undefined\"!=typeof google&&null!=google.maps&&this._build_map(),null!=this.map_el){const[t,s,i,o]=e;this.map_el.style.top=s+\"px\",this.map_el.style.left=t+\"px\",this.map_el.style.width=i+\"px\",this.map_el.style.height=o+\"px\"}}_paint_empty(t,e){const s=this.layout.bbox.width,i=this.layout.bbox.height,[o,a,n,p]=e;t.clearRect(0,0,s,i),t.beginPath(),t.moveTo(0,0),t.lineTo(0,i),t.lineTo(s,i),t.lineTo(s,0),t.lineTo(0,0),t.moveTo(o,a),t.lineTo(o+n,a),t.lineTo(o+n,a+p),t.lineTo(o,a+p),t.lineTo(o,a),t.closePath(),null!=this.model.border_fill_color&&(t.fillStyle=this.model.border_fill_color,t.fill())}}s.GMapPlotView=l,l.__name__=\"GMapPlotView\"},\n function _(a,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});var g=a(211);n.DataRange=g.DataRange;var R=a(210);n.DataRange1d=R.DataRange1d;var r=a(98);n.FactorRange=r.FactorRange;var t=a(99);n.Range=t.Range;var d=a(158);n.Range1d=d.Range1d},\n function _(e,r,d){Object.defineProperty(d,\"__esModule\",{value:!0});var n=e(90);d.GlyphRenderer=n.GlyphRenderer;var R=e(116);d.GraphRenderer=R.GraphRenderer;var a=e(178);d.GuideRenderer=a.GuideRenderer;var G=e(70);d.Renderer=G.Renderer},\n function _(a,e,l){Object.defineProperty(l,\"__esModule\",{value:!0});var c=a(209);l.CategoricalScale=c.CategoricalScale;var r=a(146);l.ContinuousScale=r.ContinuousScale;var n=a(145);l.LinearScale=n.LinearScale;var o=a(156);l.LinearInterpolationScale=o.LinearInterpolationScale;var i=a(157);l.LogScale=i.LogScale;var S=a(147);l.Scale=S.Scale},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});e(1).__exportStar(e(118),o);var n=e(88);o.Selection=n.Selection},\n function _(a,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});var o=a(325);r.ServerSentDataSource=o.ServerSentDataSource;var S=a(327);r.AjaxDataSource=S.AjaxDataSource;var u=a(85);r.ColumnDataSource=u.ColumnDataSource;var t=a(86);r.ColumnarDataSource=t.ColumnarDataSource;var c=a(114);r.CDSView=c.CDSView;var D=a(87);r.DataSource=D.DataSource;var v=a(328);r.GeoJSONDataSource=v.GeoJSONDataSource;var n=a(326);r.WebDataSource=n.WebDataSource},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const a=e(326);class s extends a.WebDataSource{constructor(e){super(e),this.initialized=!1}destroy(){super.destroy()}setup(){if(!this.initialized){this.initialized=!0;new EventSource(this.data_url).onmessage=e=>{this.load_data(JSON.parse(e.data),this.mode,this.max_size)}}}}i.ServerSentDataSource=s,s.__name__=\"ServerSentDataSource\"},\n function _(e,t,a){Object.defineProperty(a,\"__esModule\",{value:!0});const r=e(1),s=e(85),i=r.__importStar(e(18));class n extends s.ColumnDataSource{constructor(e){super(e)}get_column(e){const t=this.data[e];return null!=t?t:[]}initialize(){super.initialize(),this.setup()}load_data(e,t,a){const{adapter:r}=this;let s;switch(s=null!=r?r.execute(this,{response:e}):e,t){case\"replace\":this.data=s;break;case\"append\":{const e=this.data;for(const t of this.columns()){const r=Array.from(e[t]),i=Array.from(s[t]);s[t]=r.concat(i).slice(-a)}this.data=s;break}}}static init_WebDataSource(){this.define({mode:[i.UpdateMode,\"replace\"],max_size:[i.Number],adapter:[i.Any,null],data_url:[i.String]})}}a.WebDataSource=n,n.__name__=\"WebDataSource\",n.init_WebDataSource()},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),a=t(326),r=t(19),o=s.__importStar(t(18)),n=t(13);class d extends a.WebDataSource{constructor(t){super(t),this.initialized=!1}static init_AjaxDataSource(){this.define({polling_interval:[o.Number],content_type:[o.String,\"application/json\"],http_headers:[o.Any,{}],method:[o.HTTPMethod,\"POST\"],if_modified:[o.Boolean,!1]})}destroy(){null!=this.interval&&clearInterval(this.interval),super.destroy()}setup(){if(!this.initialized&&(this.initialized=!0,this.get_data(this.mode),this.polling_interval)){const t=()=>this.get_data(this.mode,this.max_size,this.if_modified);this.interval=setInterval(t,this.polling_interval)}}get_data(t,e=0,i=!1){const s=this.prepare_request();s.addEventListener(\"load\",()=>this.do_load(s,t,e)),s.addEventListener(\"error\",()=>this.do_error(s)),s.send()}prepare_request(){const t=new XMLHttpRequest;t.open(this.method,this.data_url,!0),t.withCredentials=!1,t.setRequestHeader(\"Content-Type\",this.content_type);const e=this.http_headers;for(const[i,s]of n.entries(e))t.setRequestHeader(i,s);return t}do_load(t,e,i){if(200===t.status){const s=JSON.parse(t.responseText);this.load_data(s,e,i)}}do_error(t){r.logger.error(`Failed to fetch JSON from ${this.data_url} with code ${t.status}`)}}i.AjaxDataSource=d,d.__name__=\"AjaxDataSource\",d.init_AjaxDataSource()},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const r=e(1),n=e(86),s=e(19),a=r.__importStar(e(18)),i=e(9),l=e(13);function c(e){return null!=e?e:NaN}class _ extends n.ColumnarDataSource{constructor(e){super(e)}static init_GeoJSONDataSource(){this.define({geojson:[a.Any]}),this.internal({data:[a.Any,{}]})}initialize(){super.initialize(),this._update_data()}connect_signals(){super.connect_signals(),this.connect(this.properties.geojson.change,()=>this._update_data())}_update_data(){this.data=this.geojson_to_column_data()}_get_new_list_array(e){return i.range(0,e).map(e=>[])}_get_new_nan_array(e){return i.range(0,e).map(e=>NaN)}_add_properties(e,t,o,r){var n;const s=null!==(n=e.properties)&&void 0!==n?n:{};for(const[e,n]of l.entries(s))t.hasOwnProperty(e)||(t[e]=this._get_new_nan_array(r)),t[e][o]=c(n)}_add_geometry(e,t,o){function r(e,t){return e.concat([[NaN,NaN,NaN]]).concat(t)}switch(e.type){case\"Point\":{const[r,n,s]=e.coordinates;t.x[o]=r,t.y[o]=n,t.z[o]=c(s);break}case\"LineString\":{const{coordinates:r}=e;for(let e=0;e1&&s.logger.warn(\"Bokeh does not support Polygons with holes in, only exterior ring used.\");const r=e.coordinates[0];for(let e=0;e1&&s.logger.warn(\"Bokeh does not support Polygons with holes in, only exterior ring used.\"),n.push(t[0]);const a=n.reduce(r);for(let e=0;ethis.get_resolution(t))}_computed_initial_resolution(){return null!=this.initial_resolution?this.initial_resolution:2*Math.PI*6378137/this.tile_size}is_valid_tile(t,e,i){return!(!this.wrap_around&&(t<0||t>=2**i))&&!(e<0||e>=2**i)}parent_by_tile_xyz(t,e,i){const _=this.tile_xyz_to_quadkey(t,e,i),s=_.substring(0,_.length-1);return this.quadkey_to_tile_xyz(s)}get_resolution(t){return this._computed_initial_resolution()/2**t}get_resolution_by_extent(t,e,i){return[(t[2]-t[0])/i,(t[3]-t[1])/e]}get_level_by_extent(t,e,i){const _=(t[2]-t[0])/i,s=(t[3]-t[1])/e,r=Math.max(_,s);let o=0;for(const t of this._resolutions){if(r>t){if(0==o)return 0;if(o>0)return o-1}o+=1}return o-1}get_closest_level_by_extent(t,e,i){const _=(t[2]-t[0])/i,s=(t[3]-t[1])/e,r=Math.max(_,s),o=this._resolutions.reduce((function(t,e){return Math.abs(e-r)e?(u=o-s,a*=t):(u*=e,a=n-r)}const h=(u-(o-s))/2,c=(a-(n-r))/2;return[s-h,r-c,o+h,n+c]}tms_to_wmts(t,e,i){return[t,2**i-1-e,i]}wmts_to_tms(t,e,i){return[t,2**i-1-e,i]}pixels_to_meters(t,e,i){const _=this.get_resolution(i);return[t*_-this.x_origin_offset,e*_-this.y_origin_offset]}meters_to_pixels(t,e,i){const _=this.get_resolution(i);return[(t+this.x_origin_offset)/_,(e+this.y_origin_offset)/_]}pixels_to_tile(t,e){let i=Math.ceil(t/this.tile_size);i=0===i?i:i-1;return[i,Math.max(Math.ceil(e/this.tile_size)-1,0)]}pixels_to_raster(t,e,i){return[t,(this.tile_size<=l;t--)for(let i=n;i<=u;i++)this.is_valid_tile(i,t,e)&&h.push([i,t,e,this.get_tile_meter_bounds(i,t,e)]);return this.sort_tiles_from_center(h,[n,l,u,a]),h}quadkey_to_tile_xyz(t){let e=0,i=0;const _=t.length;for(let s=_;s>0;s--){const r=1<0;s--){const i=1<0;)if(s=s.substring(0,s.length-1),[t,e,i]=this.quadkey_to_tile_xyz(s),[t,e,i]=this.denormalize_xyz(t,e,i,_),this.tiles.has(this.tile_xyz_to_key(t,e,i)))return[t,e,i];return[0,0,0]}normalize_xyz(t,e,i){if(this.wrap_around){const _=2**i;return[(t%_+_)%_,e,i]}return[t,e,i]}denormalize_xyz(t,e,i,_){return[t+_*2**i,e,i]}denormalize_meters(t,e,i,_){return[t+2*_*Math.PI*6378137,e]}calculate_world_x_by_tile_xyz(t,e,i){return Math.floor(t/2**i)}}i.MercatorTileSource=l,l.__name__=\"MercatorTileSource\",l.init_MercatorTileSource()},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=e(1),n=e(81),s=e(13),l=i.__importStar(e(18));class a extends n.Model{constructor(e){super(e)}static init_TileSource(){this.define({url:[l.String,\"\"],tile_size:[l.Number,256],max_zoom:[l.Number,30],min_zoom:[l.Number,0],extra_url_vars:[l.Any,{}],attribution:[l.String,\"\"],x_origin_offset:[l.Number],y_origin_offset:[l.Number],initial_resolution:[l.Number]})}initialize(){super.initialize(),this.tiles=new Map,this._normalize_case()}connect_signals(){super.connect_signals(),this.connect(this.change,()=>this._clear_cache())}string_lookup_replace(e,t){let r=e;for(const[e,i]of s.entries(t))r=r.replace(`{${e}}`,i);return r}_normalize_case(){const e=this.url.replace(\"{x}\",\"{X}\").replace(\"{y}\",\"{Y}\").replace(\"{z}\",\"{Z}\").replace(\"{q}\",\"{Q}\").replace(\"{xmin}\",\"{XMIN}\").replace(\"{ymin}\",\"{YMIN}\").replace(\"{xmax}\",\"{XMAX}\").replace(\"{ymax}\",\"{YMAX}\");this.url=e}_clear_cache(){this.tiles=new Map}tile_xyz_to_key(e,t,r){return`${e}:${t}:${r}`}key_to_tile_xyz(e){const[t,r,i]=e.split(\":\").map(e=>parseInt(e));return[t,r,i]}sort_tiles_from_center(e,t){const[r,i,n,s]=t,l=(n-r)/2+r,a=(s-i)/2+i;e.sort((function(e,t){return Math.sqrt((l-e[0])**2+(a-e[1])**2)-Math.sqrt((l-t[0])**2+(a-t[1])**2)}))}get_image_url(e,t,r){return this.string_lookup_replace(this.url,this.extra_url_vars).replace(\"{X}\",e.toString()).replace(\"{Y}\",t.toString()).replace(\"{Z}\",r.toString())}}r.TileSource=a,a.__name__=\"TileSource\",a.init_TileSource()},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const n=e(37);function o(e,t){return n.wgs84_mercator.compute(e,t)}function c(e,t){return n.wgs84_mercator.invert(e,t)}r.geographic_to_meters=o,r.meters_to_geographic=c,r.geographic_extent_to_meters=function(e){const[t,r,n,c]=e,[_,u]=o(t,r),[i,g]=o(n,c);return[_,u,i,g]},r.meters_extent_to_geographic=function(e){const[t,r,n,o]=e,[_,u]=c(t,r),[i,g]=c(n,o);return[_,u,i,g]}},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const _=e(333);class s extends _.MercatorTileSource{constructor(e){super(e)}get_image_url(e,t,r){const _=this.string_lookup_replace(this.url,this.extra_url_vars),[s,o,u]=this.tms_to_wmts(e,t,r),c=this.tile_xyz_to_quadkey(s,o,u);return _.replace(\"{Q}\",c)}}r.QUADKEYTileSource=s,s.__name__=\"QUADKEYTileSource\"},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=t(1),_=t(338),n=t(91),a=t(158),r=t(72),o=s.__importStar(t(18)),h=t(251),l=t(9),d=t(8),m=t(89),c=t(85),g=t(339),p=s.__importDefault(t(340));class u extends n.DataRendererView{initialize(){this._tiles=[],super.initialize()}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.request_render()),this.connect(this.model.tile_source.change,()=>this.request_render())}styles(){return[...super.styles(),p.default]}get_extent(){return[this.x_range.start,this.y_range.start,this.x_range.end,this.y_range.end]}get map_plot(){return this.plot_model}get map_canvas(){return this.layer.ctx}get map_frame(){return this.plot_view.frame}get x_range(){return this.map_plot.x_range}get y_range(){return this.map_plot.y_range}_set_data(){this.extent=this.get_extent(),this._last_height=void 0,this._last_width=void 0}_update_attribution(){null!=this.attribution_el&&r.removeElement(this.attribution_el);const{attribution:t}=this.model.tile_source;if(d.isString(t)&&t.length>0){const{layout:e,frame:i}=this.plot_view,s=e.bbox.width-i.bbox.right,_=e.bbox.height-i.bbox.bottom,n=i.bbox.width;this.attribution_el=r.div({class:g.bk_tile_attribution,style:{position:\"absolute\",right:s+\"px\",bottom:_+\"px\",\"max-width\":n-4+\"px\",padding:\"2px\",\"background-color\":\"rgba(255,255,255,0.5)\",\"font-size\":\"9px\",\"line-height\":\"1.05\",\"white-space\":\"nowrap\",overflow:\"hidden\",\"text-overflow\":\"ellipsis\"}}),this.plot_view.canvas_view.add_event(this.attribution_el),this.attribution_el.innerHTML=t,this.attribution_el.title=this.attribution_el.textContent.replace(/\\s*\\n\\s*/g,\" \")}}_map_data(){this.initial_extent=this.get_extent();const t=this.model.tile_source.get_level_by_extent(this.initial_extent,this.map_frame.bbox.height,this.map_frame.bbox.width),e=this.model.tile_source.snap_to_zoom_level(this.initial_extent,this.map_frame.bbox.height,this.map_frame.bbox.width,t);this.x_range.start=e[0],this.y_range.start=e[1],this.x_range.end=e[2],this.y_range.end=e[3],this.x_range instanceof a.Range1d&&(this.x_range.reset_start=e[0],this.x_range.reset_end=e[2]),this.y_range instanceof a.Range1d&&(this.y_range.reset_start=e[1],this.y_range.reset_end=e[3]),this._update_attribution()}_create_tile(t,e,i,s,_=!1){const[n,a,r]=this.model.tile_source.normalize_xyz(t,e,i),o={img:void 0,tile_coords:[t,e,i],normalized_coords:[n,a,r],quadkey:this.model.tile_source.tile_xyz_to_quadkey(t,e,i),cache_key:this.model.tile_source.tile_xyz_to_key(t,e,i),bounds:s,loaded:!1,finished:!1,x_coord:s[0],y_coord:s[3]},l=this.model.tile_source.get_image_url(n,a,r);new h.ImageLoader(l,{loaded:t=>{Object.assign(o,{img:t,loaded:!0}),_?(o.finished=!0,this.notify_finished()):this.request_render()},failed(){o.finished=!0}}),this.model.tile_source.tiles.set(o.cache_key,o),this._tiles.push(o)}_enforce_aspect_ratio(){if(this._last_height!==this.map_frame.bbox.height||this._last_width!==this.map_frame.bbox.width){const t=this.get_extent(),e=this.model.tile_source.get_level_by_extent(t,this.map_frame.bbox.height,this.map_frame.bbox.width),i=this.model.tile_source.snap_to_zoom_level(t,this.map_frame.bbox.height,this.map_frame.bbox.width,e);this.x_range.setv({start:i[0],end:i[2]}),this.y_range.setv({start:i[1],end:i[3]}),this.extent=i,this._last_height=this.map_frame.bbox.height,this._last_width=this.map_frame.bbox.width}}has_finished(){if(!super.has_finished())return!1;if(0===this._tiles.length)return!1;for(const t of this._tiles)if(!t.finished)return!1;return!0}_render(){null==this.map_initialized&&(this._set_data(),this._map_data(),this.map_initialized=!0),this._enforce_aspect_ratio(),this._update(),null!=this.prefetch_timer&&clearTimeout(this.prefetch_timer),this.prefetch_timer=setTimeout(this._prefetch_tiles.bind(this),500),this.has_finished()&&this.notify_finished()}_draw_tile(t){const e=this.model.tile_source.tiles.get(t);if(null!=e&&e.loaded){const[[t],[i]]=this.coordinates.map_to_screen([e.bounds[0]],[e.bounds[3]]),[[s],[_]]=this.coordinates.map_to_screen([e.bounds[2]],[e.bounds[1]]),n=s-t,a=_-i,r=t,o=i,h=this.map_canvas.getImageSmoothingEnabled();this.map_canvas.setImageSmoothingEnabled(this.model.smoothing),this.map_canvas.drawImage(e.img,r,o,n,a),this.map_canvas.setImageSmoothingEnabled(h),e.finished=!0}}_set_rect(){const t=this.plot_model.properties.outline_line_width.value(),e=this.map_frame.bbox.left+t/2,i=this.map_frame.bbox.top+t/2,s=this.map_frame.bbox.width-t,_=this.map_frame.bbox.height-t;this.map_canvas.rect(e,i,s,_),this.map_canvas.clip()}_render_tiles(t){this.map_canvas.save(),this._set_rect(),this.map_canvas.globalAlpha=this.model.alpha;for(const e of t)this._draw_tile(e);this.map_canvas.restore()}_prefetch_tiles(){const{tile_source:t}=this.model,e=this.get_extent(),i=this.map_frame.bbox.height,s=this.map_frame.bbox.width,_=this.model.tile_source.get_level_by_extent(e,i,s),n=this.model.tile_source.get_tiles_by_extent(e,_);for(let e=0,i=Math.min(10,n.length);ei&&(s=this.extent,r=i,o=!0),o&&(this.x_range.setv({x_range:{start:s[0],end:s[2]}}),this.y_range.setv({start:s[1],end:s[3]})),this.extent=s;const h=t.get_tiles_by_extent(s,r),d=[],m=[],c=[],g=[];for(const e of h){const[i,s,n]=e,a=t.tile_xyz_to_key(i,s,n),r=t.tiles.get(a);if(null!=r&&r.loaded)m.push(a);else if(this.model.render_parents){const[e,a,r]=t.get_closest_parent_by_tile_xyz(i,s,n),o=t.tile_xyz_to_key(e,a,r),h=t.tiles.get(o);if(null!=h&&h.loaded&&!l.includes(c,o)&&c.push(o),_){const e=t.children_by_tile_xyz(i,s,n);for(const[i,s,_]of e){const e=t.tile_xyz_to_key(i,s,_);t.tiles.has(e)&&g.push(e)}}}null==r&&d.push(e)}this._render_tiles(c),this._render_tiles(g),this._render_tiles(m),null!=this.render_timer&&clearTimeout(this.render_timer),this.render_timer=setTimeout(()=>this._fetch_tiles(d),65)}}i.TileRendererView=u,u.__name__=\"TileRendererView\";class b extends n.DataRenderer{constructor(t){super(t),this._selection_manager=new m.SelectionManager({source:new c.ColumnDataSource})}static init_TileRenderer(){this.prototype.default_view=u,this.define({alpha:[o.Number,1],smoothing:[o.Boolean,!0],tile_source:[o.Instance,()=>new _.WMTSTileSource],render_parents:[o.Boolean,!0]})}get_selection_manager(){return this._selection_manager}}i.TileRenderer=b,b.__name__=\"TileRenderer\",b.init_TileRenderer()},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const o=e(333);class s extends o.MercatorTileSource{constructor(e){super(e)}get_image_url(e,t,r){const o=this.string_lookup_replace(this.url,this.extra_url_vars),[s,c,_]=this.tms_to_wmts(e,t,r);return o.replace(\"{X}\",s.toString()).replace(\"{Y}\",c.toString()).replace(\"{Z}\",_.toString())}}r.WMTSTileSource=s,s.__name__=\"WMTSTileSource\"},\n function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0}),i.bk_tile_attribution=\"bk-tile-attribution\"},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});n.default=\"\\n.bk-root .bk-tile-attribution a {\\n color: black;\\n}\\n\"},\n function _(e,r,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=e(333);class c extends o.MercatorTileSource{constructor(e){super(e)}get_image_url(e,r,t){return this.string_lookup_replace(this.url,this.extra_url_vars).replace(\"{X}\",e.toString()).replace(\"{Y}\",r.toString()).replace(\"{Z}\",t.toString())}}t.TMSTileSource=c,c.__name__=\"TMSTileSource\"},\n function _(e,r,a){Object.defineProperty(a,\"__esModule\",{value:!0});var t=e(343);a.CanvasTexture=t.CanvasTexture;var u=e(345);a.ImageURLTexture=u.ImageURLTexture;var v=e(344);a.Texture=v.Texture},\n function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const r=t(1),c=t(344),s=r.__importStar(t(18)),i=t(29);class a extends c.Texture{constructor(t){super(t)}static init_CanvasTexture(){this.define({code:[s.String]})}get func(){const t=i.use_strict(this.code);return new Function(\"ctx\",\"color\",\"scale\",\"weight\",t)}get_pattern(t,e,n){return r=>{const c=document.createElement(\"canvas\");c.width=e,c.height=e;const s=c.getContext(\"2d\");return this.func.call(this,s,t,e,n),r.createPattern(c,this.repetition)}}}n.CanvasTexture=a,a.__name__=\"CanvasTexture\",a.init_CanvasTexture()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const r=e(1),n=e(81),o=r.__importStar(e(18));class _ extends n.Model{constructor(e){super(e)}static init_Texture(){this.define({repetition:[o.TextureRepetition,\"repeat\"]})}onload(e){e()}}i.Texture=_,_.__name__=\"Texture\",_.init_Texture()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const r=e(1),a=e(344),n=r.__importStar(e(18)),s=e(251);class o extends a.Texture{constructor(e){super(e)}static init_ImageURLTexture(){this.define({url:[n.String]})}initialize(){super.initialize(),this._loader=new s.ImageLoader(this.url)}get_pattern(e,t,i){return e=>this._loader.finished?e.createPattern(this._loader.image,this.repetition):null}onload(e){this._loader.promise.then(()=>e())}}i.ImageURLTexture=o,o.__name__=\"ImageURLTexture\",o.init_ImageURLTexture()},\n function _(o,l,T){Object.defineProperty(T,\"__esModule\",{value:!0});var a=o(307);T.ActionTool=a.ActionTool;var r=o(347);T.CustomAction=r.CustomAction;var e=o(308);T.HelpTool=e.HelpTool;var v=o(348);T.RedoTool=v.RedoTool;var t=o(349);T.ResetTool=t.ResetTool;var n=o(350);T.SaveTool=n.SaveTool;var s=o(351);T.UndoTool=s.UndoTool;var i=o(352);T.ZoomInTool=i.ZoomInTool;var P=o(355);T.ZoomOutTool=P.ZoomOutTool;var c=o(296);T.ButtonTool=c.ButtonTool;var d=o(356);T.EditTool=d.EditTool;var u=o(357);T.BoxEditTool=u.BoxEditTool;var y=o(358);T.FreehandDrawTool=y.FreehandDrawTool;var m=o(359);T.PointDrawTool=m.PointDrawTool;var x=o(360);T.PolyDrawTool=x.PolyDrawTool;var B=o(361);T.PolyTool=B.PolyTool;var S=o(362);T.PolyEditTool=S.PolyEditTool;var b=o(363);T.BoxSelectTool=b.BoxSelectTool;var h=o(366);T.BoxZoomTool=h.BoxZoomTool;var E=o(306);T.GestureTool=E.GestureTool;var Z=o(367);T.LassoSelectTool=Z.LassoSelectTool;var p=o(369);T.LineEditTool=p.LineEditTool;var w=o(371);T.PanTool=w.PanTool;var C=o(368);T.PolySelectTool=C.PolySelectTool;var D=o(372);T.RangeTool=D.RangeTool;var H=o(364);T.SelectTool=H.SelectTool;var R=o(373);T.TapTool=R.TapTool;var A=o(374);T.WheelPanTool=A.WheelPanTool;var I=o(375);T.WheelZoomTool=I.WheelZoomTool;var L=o(376);T.CrosshairTool=L.CrosshairTool;var W=o(377);T.CustomJSHover=W.CustomJSHover;var O=o(378);T.HoverTool=O.HoverTool;var _=o(295);T.InspectTool=_.InspectTool;var f=o(298);T.Tool=f.Tool;var g=o(379);T.ToolProxy=g.ToolProxy;var F=o(294);T.Toolbar=F.Toolbar;var G=o(305);T.ToolbarBase=G.ToolbarBase;var J=o(380);T.ProxyToolbar=J.ProxyToolbar;var U=o(380);T.ToolbarBox=U.ToolbarBox},\n function _(t,o,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=t(1),s=t(307),e=n.__importStar(t(18)),c=t(299);class _ extends s.ActionToolButtonView{css_classes(){return super.css_classes().concat(c.bk_toolbar_button_custom_action)}}i.CustomActionButtonView=_,_.__name__=\"CustomActionButtonView\";class l extends s.ActionToolView{doit(){null!=this.model.callback&&this.model.callback.execute(this.model)}}i.CustomActionView=l,l.__name__=\"CustomActionView\";class u extends s.ActionTool{constructor(t){super(t),this.tool_name=\"Custom Action\",this.button_view=_}static init_CustomAction(){this.prototype.default_view=l,this.define({action_tooltip:[e.String,\"Perform a Custom Action\"],callback:[e.Any],icon:[e.String]})}get tooltip(){return this.action_tooltip}}i.CustomAction=u,u.__name__=\"CustomAction\",u.init_CustomAction()},\n function _(o,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const i=o(307),s=o(309);class n extends i.ActionToolView{connect_signals(){super.connect_signals(),this.connect(this.plot_view.state_changed,()=>this.model.disabled=!this.plot_view.can_redo())}doit(){this.plot_view.redo()}}t.RedoToolView=n,n.__name__=\"RedoToolView\";class _ extends i.ActionTool{constructor(o){super(o),this.tool_name=\"Redo\",this.icon=s.bk_tool_icon_redo}static init_RedoTool(){this.prototype.default_view=n,this.override({disabled:!0}),this.register_alias(\"redo\",()=>new _)}}t.RedoTool=_,_.__name__=\"RedoTool\",_.init_RedoTool()},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const s=e(307),i=e(309);class _ extends s.ActionToolView{doit(){this.plot_view.reset()}}o.ResetToolView=_,_.__name__=\"ResetToolView\";class l extends s.ActionTool{constructor(e){super(e),this.tool_name=\"Reset\",this.icon=i.bk_tool_icon_reset}static init_ResetTool(){this.prototype.default_view=_,this.register_alias(\"reset\",()=>new l)}}o.ResetTool=l,l.__name__=\"ResetTool\",l.init_ResetTool()},\n function _(e,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});const a=e(307),i=e(309);class n extends a.ActionToolView{async copy(){const e=await this.plot_view.to_blob(),o=new ClipboardItem({[e.type]:e});await navigator.clipboard.write([o])}async save(e){const o=await this.plot_view.to_blob(),t=document.createElement(\"a\");t.href=URL.createObjectURL(o),t.download=e,t.target=\"_blank\",t.dispatchEvent(new MouseEvent(\"click\"))}doit(e=\"save\"){switch(e){case\"save\":this.save(\"bokeh_plot\");break;case\"copy\":this.copy()}}}t.SaveToolView=n,n.__name__=\"SaveToolView\";class s extends a.ActionTool{constructor(e){super(e),this.tool_name=\"Save\",this.icon=i.bk_tool_icon_save}static init_SaveTool(){this.prototype.default_view=n,this.register_alias(\"save\",()=>new s)}get menu(){return[{icon:\"bk-tool-icon-copy-to-clipboard\",tooltip:\"Copy image to clipboard\",if:()=>\"undefined\"!=typeof ClipboardItem,handler:()=>{this.do.emit(\"copy\")}}]}}t.SaveTool=s,s.__name__=\"SaveTool\",s.init_SaveTool()},\n function _(o,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const n=o(307),i=o(309);class s extends n.ActionToolView{connect_signals(){super.connect_signals(),this.connect(this.plot_view.state_changed,()=>this.model.disabled=!this.plot_view.can_undo())}doit(){this.plot_view.undo()}}e.UndoToolView=s,s.__name__=\"UndoToolView\";class _ extends n.ActionTool{constructor(o){super(o),this.tool_name=\"Undo\",this.icon=i.bk_tool_icon_undo}static init_UndoTool(){this.prototype.default_view=s,this.override({disabled:!0}),this.register_alias(\"undo\",()=>new _)}}e.UndoTool=_,_.__name__=\"UndoTool\",_.init_UndoTool()},\n function _(o,i,e){Object.defineProperty(e,\"__esModule\",{value:!0});const n=o(353),s=o(309);class t extends n.ZoomBaseTool{constructor(o){super(o),this.sign=1,this.tool_name=\"Zoom In\",this.icon=s.bk_tool_icon_zoom_in}static init_ZoomInTool(){this.prototype.default_view=n.ZoomBaseToolView,this.register_alias(\"zoom_in\",()=>new t({dimensions:\"both\"})),this.register_alias(\"xzoom_in\",()=>new t({dimensions:\"width\"})),this.register_alias(\"yzoom_in\",()=>new t({dimensions:\"height\"}))}}e.ZoomInTool=t,t.__name__=\"ZoomInTool\",t.init_ZoomInTool()},\n function _(o,t,e){Object.defineProperty(e,\"__esModule\",{value:!0});const i=o(1),s=o(307),n=o(354),_=i.__importStar(o(18));class l extends s.ActionToolView{doit(){const o=this.plot_view.frame,t=this.model.dimensions,e=\"width\"==t||\"both\"==t,i=\"height\"==t||\"both\"==t,s=n.scale_range(o,this.model.sign*this.model.factor,e,i);this.plot_view.push_state(\"zoom_out\",{range:s}),this.plot_view.update_range(s,!1,!0),this.model.document&&this.model.document.interactive_start(this.plot_model)}}e.ZoomBaseToolView=l,l.__name__=\"ZoomBaseToolView\";class a extends s.ActionTool{constructor(o){super(o)}static init_ZoomBaseTool(){this.prototype.default_view=l,this.define({factor:[_.Percent,.1],dimensions:[_.Dimensions,\"both\"]})}get tooltip(){return this._get_dim_tooltip(this.tool_name,this.dimensions)}}e.ZoomBaseTool=a,a.__name__=\"ZoomBaseTool\",a.init_ZoomBaseTool()},\n function _(n,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=n(10);function r(n,e,t){const[o,r]=[n.start,n.end],s=null!=t?t:(r+o)/2;return[o-(o-s)*e,r-(r-s)*e]}function s(n,[e,t]){const o=new Map;for(const[r,s]of n){const[n,c]=s.r_invert(e,t);o.set(r,{start:n,end:c})}return o}t.scale_highlow=r,t.get_info=s,t.scale_range=function(n,e,t=!0,c=!0,l){e=o.clamp(e,-.9,.9);const a=t?e:0,[u,_]=r(n.bbox.h_range,a,null!=l?l.x:void 0),i=s(n.x_scales,[u,_]),f=c?e:0,[d,b]=r(n.bbox.v_range,f,null!=l?l.y:void 0);return{xrs:i,yrs:s(n.y_scales,[d,b]),factor:e}}},\n function _(o,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const e=o(353),s=o(309);class n extends e.ZoomBaseTool{constructor(o){super(o),this.sign=-1,this.tool_name=\"Zoom Out\",this.icon=s.bk_tool_icon_zoom_out}static init_ZoomOutTool(){this.prototype.default_view=e.ZoomBaseToolView,this.register_alias(\"zoom_out\",()=>new n({dimensions:\"both\"})),this.register_alias(\"xzoom_out\",()=>new n({dimensions:\"width\"})),this.register_alias(\"yzoom_out\",()=>new n({dimensions:\"height\"}))}}i.ZoomOutTool=n,n.__name__=\"ZoomOutTool\",n.init_ZoomOutTool()},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const s=e(1).__importStar(e(18)),i=e(9),n=e(8),r=e(11),_=e(306);class c extends _.GestureToolView{constructor(){super(...arguments),this._mouse_in_frame=!0}_select_mode(e){const{shiftKey:t,ctrlKey:o}=e;return t||o?t&&!o?\"append\":!t&&o?\"intersect\":t&&o?\"subtract\":void r.unreachable():\"replace\"}_move_enter(e){this._mouse_in_frame=!0}_move_exit(e){this._mouse_in_frame=!1}_map_drag(e,t,o){if(!this.plot_view.frame.bbox.contains(e,t))return null;const s=this.plot_view.renderer_views.get(o);return[s.coordinates.x_scale.invert(e),s.coordinates.y_scale.invert(t)]}_delete_selected(e){const t=e.data_source,o=t.selected.indices;o.sort();for(const e of t.columns()){const s=t.get_array(e);for(let e=0;ethis._show_vertices())}this._initialized=!0}}deactivate(){this._drawing&&(this._remove(),this._drawing=!1),this.model.vertex_renderer&&this._hide_vertices()}}s.PolyDrawToolView=d,d.__name__=\"PolyDrawToolView\";class l extends n.PolyTool{constructor(e){super(e),this.tool_name=\"Polygon Draw Tool\",this.icon=_.bk_tool_icon_poly_draw,this.event_type=[\"pan\",\"tap\",\"move\"],this.default_order=3}static init_PolyDrawTool(){this.prototype.default_view=d,this.define({drag:[a.Boolean,!0],num_objects:[a.Int,0]})}}s.PolyDrawTool=l,l.__name__=\"PolyDrawTool\",l.init_PolyDrawTool()},\n function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const o=e(1).__importStar(e(18)),i=e(8),s=e(356);class _ extends s.EditToolView{_set_vertices(e,t){const r=this.model.vertex_renderer.glyph,o=this.model.vertex_renderer.data_source,[s,_]=[r.x.field,r.y.field];s&&(i.isArray(e)?o.data[s]=e:r.x={value:e}),_&&(i.isArray(t)?o.data[_]=t:r.y={value:t}),this._emit_cds_changes(o,!0,!0,!1)}_hide_vertices(){this._set_vertices([],[])}_snap_to_vertex(e,t,r){if(this.model.vertex_renderer){const o=this._select_event(e,\"replace\",[this.model.vertex_renderer]),i=this.model.vertex_renderer.data_source,s=this.model.vertex_renderer.glyph,[_,l]=[s.x.field,s.y.field];if(o.length){const e=i.selected.indices[0];_&&(t=i.data[_][e]),l&&(r=i.data[l][e]),i.selection_manager.clear()}}return[t,r]}}r.PolyToolView=_,_.__name__=\"PolyToolView\";class l extends s.EditTool{constructor(e){super(e)}static init_PolyTool(){this.prototype.default_view=_,this.define({vertex_renderer:[o.Instance]})}}r.PolyTool=l,l.__name__=\"PolyTool\",l.init_PolyTool()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const r=e(72),i=e(8),_=e(361),d=e(309);class n extends _.PolyToolView{constructor(){super(...arguments),this._drawing=!1}_doubletap(e){if(!this.model.active)return;const t=this._map_drag(e.sx,e.sy,this.model.vertex_renderer);if(null==t)return;const[s,r]=t,i=this._select_event(e,\"replace\",[this.model.vertex_renderer]),_=this.model.vertex_renderer.data_source,d=this.model.vertex_renderer.glyph,[n,l]=[d.x.field,d.y.field];if(i.length&&null!=this._selected_renderer){const e=_.selected.indices[0];this._drawing?(this._drawing=!1,_.selection_manager.clear()):(_.selected.indices=[e+1],n&&_.get_array(n).splice(e+1,0,s),l&&_.get_array(l).splice(e+1,0,r),this._drawing=!0),_.change.emit(),this._emit_cds_changes(this._selected_renderer.data_source)}else this._show_vertices(e)}_show_vertices(e){if(!this.model.active)return;const t=this._select_event(e,\"replace\",this.model.renderers);if(!t.length)return this._set_vertices([],[]),this._selected_renderer=null,void(this._drawing=!1);const s=t[0],r=s.glyph,_=s.data_source,d=_.selected.indices[0],[n,l]=[r.xs.field,r.ys.field];let a,c;n?(a=_.data[n][d],i.isArray(a)||(_.data[n][d]=a=Array.from(a))):a=r.xs.value,l?(c=_.data[l][d],i.isArray(c)||(_.data[l][d]=c=Array.from(c))):c=r.ys.value,this._selected_renderer=s,this._set_vertices(a,c)}_move(e){if(this._drawing&&null!=this._selected_renderer){const t=this.model.vertex_renderer,s=t.data_source,r=t.glyph,i=this._map_drag(e.sx,e.sy,t);if(null==i)return;let[_,d]=i;const n=s.selected.indices;[_,d]=this._snap_to_vertex(e,_,d),s.selected.indices=n;const[l,a]=[r.x.field,r.y.field],c=n[0];l&&(s.data[l][c]=_),a&&(s.data[a][c]=d),s.change.emit(),this._selected_renderer.data_source.change.emit()}}_tap(e){const t=this.model.vertex_renderer,s=this._map_drag(e.sx,e.sy,t);if(null==s)return;if(this._drawing&&this._selected_renderer){let[r,i]=s;const _=t.data_source,d=t.glyph,[n,l]=[d.x.field,d.y.field],a=_.selected.indices;[r,i]=this._snap_to_vertex(e,r,i);const c=a[0];if(_.selected.indices=[c+1],n){const e=_.get_array(n),t=e[c];e[c]=r,e.splice(c+1,0,t)}if(l){const e=_.get_array(l),t=e[c];e[c]=i,e.splice(c+1,0,t)}return _.change.emit(),void this._emit_cds_changes(this._selected_renderer.data_source,!0,!1,!0)}const r=this._select_mode(e);this._select_event(e,r,[t]),this._select_event(e,r,this.model.renderers)}_remove_vertex(){if(!this._drawing||!this._selected_renderer)return;const e=this.model.vertex_renderer,t=e.data_source,s=e.glyph,r=t.selected.indices[0],[i,_]=[s.x.field,s.y.field];i&&t.get_array(i).splice(r,1),_&&t.get_array(_).splice(r,1),t.change.emit(),this._emit_cds_changes(this._selected_renderer.data_source)}_pan_start(e){this._select_event(e,\"append\",[this.model.vertex_renderer]),this._basepoint=[e.sx,e.sy]}_pan(e){null!=this._basepoint&&(this._drag_points(e,[this.model.vertex_renderer]),this._selected_renderer&&this._selected_renderer.data_source.change.emit())}_pan_end(e){null!=this._basepoint&&(this._drag_points(e,[this.model.vertex_renderer]),this._emit_cds_changes(this.model.vertex_renderer.data_source,!1,!0,!0),this._selected_renderer&&this._emit_cds_changes(this._selected_renderer.data_source),this._basepoint=null)}_keyup(e){if(!this.model.active||!this._mouse_in_frame)return;let t;t=this._selected_renderer?[this.model.vertex_renderer]:this.model.renderers;for(const s of t)e.keyCode===r.Keys.Backspace?(this._delete_selected(s),this._selected_renderer&&this._emit_cds_changes(this._selected_renderer.data_source)):e.keyCode==r.Keys.Esc&&(this._drawing?(this._remove_vertex(),this._drawing=!1):this._selected_renderer&&this._hide_vertices(),s.data_source.selection_manager.clear())}deactivate(){this._selected_renderer&&(this._drawing&&(this._remove_vertex(),this._drawing=!1),this._hide_vertices())}}s.PolyEditToolView=n,n.__name__=\"PolyEditToolView\";class l extends _.PolyTool{constructor(e){super(e),this.tool_name=\"Poly Edit Tool\",this.icon=d.bk_tool_icon_poly_edit,this.event_type=[\"tap\",\"pan\",\"move\"],this.default_order=4}static init_PolyEditTool(){this.prototype.default_view=n}}s.PolyEditTool=l,l.__name__=\"PolyEditTool\",l.init_PolyEditTool()},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const s=e(1),i=e(364),l=e(124),_=s.__importStar(e(18)),n=e(309);class c extends i.SelectToolView{_compute_limits(e){const t=this.plot_view.frame,o=this.model.dimensions;let s=this._base_point;if(\"center\"==this.model.origin){const[t,o]=s,[i,l]=e;s=[t-(i-t),o-(l-o)]}return this.model._get_dim_limits(s,e,t,o)}_pan_start(e){const{sx:t,sy:o}=e;this._base_point=[t,o]}_pan(e){const{sx:t,sy:o}=e,s=[t,o],[i,l]=this._compute_limits(s);this.model.overlay.update({left:i[0],right:i[1],top:l[0],bottom:l[1]}),this.model.select_every_mousemove&&this._do_select(i,l,!1,this._select_mode(e))}_pan_end(e){const{sx:t,sy:o}=e,s=[t,o],[i,l]=this._compute_limits(s);this._do_select(i,l,!0,this._select_mode(e)),this.model.overlay.update({left:null,right:null,top:null,bottom:null}),this._base_point=null,this.plot_view.push_state(\"box_select\",{selection:this.plot_view.get_selection()})}_do_select([e,t],[o,s],i,l=\"replace\"){const _={type:\"rect\",sx0:e,sx1:t,sy0:o,sy1:s};this._select(_,i,l)}}o.BoxSelectToolView=c,c.__name__=\"BoxSelectToolView\";const r=()=>new l.BoxAnnotation({level:\"overlay\",top_units:\"screen\",left_units:\"screen\",bottom_units:\"screen\",right_units:\"screen\",fill_color:\"lightgrey\",fill_alpha:.5,line_color:\"black\",line_alpha:1,line_width:2,line_dash:[4,4]});class h extends i.SelectTool{constructor(e){super(e),this.tool_name=\"Box Select\",this.icon=n.bk_tool_icon_box_select,this.event_type=\"pan\",this.default_order=30}static init_BoxSelectTool(){this.prototype.default_view=c,this.define({dimensions:[_.Dimensions,\"both\"],select_every_mousemove:[_.Boolean,!1],overlay:[_.Instance,r],origin:[_.BoxOrigin,\"corner\"]}),this.register_alias(\"box_select\",()=>new h),this.register_alias(\"xbox_select\",()=>new h({dimensions:\"width\"})),this.register_alias(\"ybox_select\",()=>new h({dimensions:\"height\"}))}get tooltip(){return this._get_dim_tooltip(this.tool_name,this.dimensions)}}o.BoxSelectTool=h,h.__name__=\"BoxSelectTool\",h.init_BoxSelectTool()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=e(1),o=e(306),r=e(90),c=e(116),i=e(365),l=n.__importStar(e(18)),a=e(72),_=e(313),d=e(15),h=e(11);class p extends o.GestureToolView{connect_signals(){super.connect_signals(),this.model.clear.connect(()=>this._clear())}get computed_renderers(){const e=this.model.renderers,t=this.plot_model.renderers,s=this.model.names;return i.compute_renderers(e,t,s)}_computed_renderers_by_data_source(){var e;const t=new Map;for(const s of this.computed_renderers){let n;if(s instanceof r.GlyphRenderer)n=s.data_source;else{if(!(s instanceof c.GraphRenderer))continue;n=s.node_renderer.data_source}const o=null!==(e=t.get(n))&&void 0!==e?e:[];t.set(n,[...o,s])}return t}_select_mode(e){const{shiftKey:t,ctrlKey:s}=e;return t||s?t&&!s?\"append\":!t&&s?\"intersect\":t&&s?\"subtract\":void h.unreachable():this.model.mode}_keyup(e){e.keyCode==a.Keys.Esc&&this._clear()}_clear(){for(const e of this.computed_renderers)e.get_selection_manager().clear();this.plot_view.request_render()}_select(e,t,s){const n=this._computed_renderers_by_data_source();for(const[,o]of n){const n=o[0].get_selection_manager(),r=[];for(const e of o){const t=this.plot_view.renderer_views.get(e);null!=t&&r.push(t)}n.select(r,e,t,s)}null!=this.model.callback&&this._emit_callback(e),this._emit_selection_event(e,t)}_emit_selection_event(e,t=!0){const{x_scale:s,y_scale:n}=this.plot_view.frame;let o;switch(e.type){case\"point\":{const{sx:t,sy:r}=e,c=s.invert(t),i=n.invert(r);o=Object.assign(Object.assign({},e),{x:c,y:i});break}case\"span\":{const{sx:t,sy:r}=e,c=s.invert(t),i=n.invert(r);o=Object.assign(Object.assign({},e),{x:c,y:i});break}case\"rect\":{const{sx0:t,sx1:r,sy0:c,sy1:i}=e,[l,a]=s.r_invert(t,r),[_,d]=n.r_invert(c,i);o=Object.assign(Object.assign({},e),{x0:l,y0:_,x1:a,y1:d});break}case\"poly\":{const{sx:t,sy:r}=e,c=s.v_invert(t),i=n.v_invert(r);o=Object.assign(Object.assign({},e),{x:c,y:i});break}}this.plot_model.trigger_event(new _.SelectionGeometry(o,t))}}s.SelectToolView=p,p.__name__=\"SelectToolView\";class u extends o.GestureTool{constructor(e){super(e)}initialize(){super.initialize(),this.clear=new d.Signal0(this,\"clear\")}static init_SelectTool(){this.define({renderers:[l.Any,\"auto\"],names:[l.Array,[]],mode:[l.Any,\"replace\"]})}get menu(){return[{icon:\"bk-tool-icon-replace-mode\",tooltip:\"Replace the current selection\",active:()=>\"replace\"==this.mode,handler:()=>{this.mode=\"replace\",this.active=!0}},{icon:\"bk-tool-icon-append-mode\",tooltip:\"Append to the current selection (Shift)\",active:()=>\"append\"==this.mode,handler:()=>{this.mode=\"append\",this.active=!0}},{icon:\"bk-tool-icon-intersect-mode\",tooltip:\"Intersect with the current selection (Ctrl)\",active:()=>\"intersect\"==this.mode,handler:()=>{this.mode=\"intersect\",this.active=!0}},{icon:\"bk-tool-icon-subtract-mode\",tooltip:\"Subtract from the current selection (Shift+Ctrl)\",active:()=>\"subtract\"==this.mode,handler:()=>{this.mode=\"subtract\",this.active=!0}},null,{icon:\"bk-tool-icon-clear-selection\",tooltip:\"Clear the current selection (Esc)\",handler:()=>{this.clear.emit()}}]}}s.SelectTool=u,u.__name__=\"SelectTool\",u.init_SelectTool()},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});const r=e(9);t.compute_renderers=function(e,n,t){if(null==e)return[];let u=\"auto\"==e?n:e;return t.length>0&&(u=u.filter(e=>r.includes(t,e.name))),u}},\n function _(t,o,e){Object.defineProperty(e,\"__esModule\",{value:!0});const s=t(1),i=t(306),n=t(124),_=s.__importStar(t(18)),a=t(309);class l extends i.GestureToolView{_match_aspect(t,o,e){const s=e.bbox.aspect,i=e.bbox.h_range.end,n=e.bbox.h_range.start,_=e.bbox.v_range.end,a=e.bbox.v_range.start;let l=Math.abs(t[0]-o[0]),r=Math.abs(t[1]-o[1]);const h=0==r?0:l/r,[c]=h>=s?[1,h/s]:[s/h,1];let m,p,d,b;return t[0]<=o[0]?(m=t[0],p=t[0]+l*c,p>i&&(p=i)):(p=t[0],m=t[0]-l*c,m_&&(d=_)):(d=t[1],b=t[1]-l/s,bnew n.BoxAnnotation({level:\"overlay\",top_units:\"screen\",left_units:\"screen\",bottom_units:\"screen\",right_units:\"screen\",fill_color:\"lightgrey\",fill_alpha:.5,line_color:\"black\",line_alpha:1,line_width:2,line_dash:[4,4]});class h extends i.GestureTool{constructor(t){super(t),this.tool_name=\"Box Zoom\",this.icon=a.bk_tool_icon_box_zoom,this.event_type=\"pan\",this.default_order=20}static init_BoxZoomTool(){this.prototype.default_view=l,this.define({dimensions:[_.Dimensions,\"both\"],overlay:[_.Instance,r],match_aspect:[_.Boolean,!1],origin:[_.BoxOrigin,\"corner\"]}),this.register_alias(\"box_zoom\",()=>new h({dimensions:\"both\"})),this.register_alias(\"xbox_zoom\",()=>new h({dimensions:\"width\"})),this.register_alias(\"ybox_zoom\",()=>new h({dimensions:\"height\"}))}get tooltip(){return this._get_dim_tooltip(this.tool_name,this.dimensions)}}e.BoxZoomTool=h,h.__name__=\"BoxZoomTool\",h.init_BoxZoomTool()},\n function _(e,s,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=e(1),a=e(364),i=e(368),l=e(72),_=o.__importStar(e(18)),c=e(309);class n extends a.SelectToolView{initialize(){super.initialize(),this.data=null}connect_signals(){super.connect_signals(),this.connect(this.model.properties.active.change,()=>this._active_change())}_active_change(){this.model.active||this._clear_overlay()}_keyup(e){e.keyCode==l.Keys.Enter&&this._clear_overlay()}_pan_start(e){const{sx:s,sy:t}=e;this.data={sx:[s],sy:[t]}}_pan(e){const{sx:s,sy:t}=e,[o,a]=this.plot_view.frame.bbox.clip(s,t);this.data.sx.push(o),this.data.sy.push(a);this.model.overlay.update({xs:this.data.sx,ys:this.data.sy}),this.model.select_every_mousemove&&this._do_select(this.data.sx,this.data.sy,!1,this._select_mode(e))}_pan_end(e){this._clear_overlay(),this._do_select(this.data.sx,this.data.sy,!0,this._select_mode(e)),this.plot_view.push_state(\"lasso_select\",{selection:this.plot_view.get_selection()})}_clear_overlay(){this.model.overlay.update({xs:[],ys:[]})}_do_select(e,s,t,o){const a={type:\"poly\",sx:e,sy:s};this._select(a,t,o)}}t.LassoSelectToolView=n,n.__name__=\"LassoSelectToolView\";class h extends a.SelectTool{constructor(e){super(e),this.tool_name=\"Lasso Select\",this.icon=c.bk_tool_icon_lasso_select,this.event_type=\"pan\",this.default_order=12}static init_LassoSelectTool(){this.prototype.default_view=n,this.define({select_every_mousemove:[_.Boolean,!0],overlay:[_.Instance,i.DEFAULT_POLY_OVERLAY]}),this.register_alias(\"lasso_select\",()=>new h)}}t.LassoSelectTool=h,h.__name__=\"LassoSelectTool\",h.init_LassoSelectTool()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const l=e(1),i=e(364),o=e(166),a=e(72),_=l.__importStar(e(18)),c=e(9),n=e(309);class h extends i.SelectToolView{initialize(){super.initialize(),this.data={sx:[],sy:[]}}connect_signals(){super.connect_signals(),this.connect(this.model.properties.active.change,()=>this._active_change())}_active_change(){this.model.active||this._clear_data()}_keyup(e){e.keyCode==a.Keys.Enter&&this._clear_data()}_doubletap(e){this._do_select(this.data.sx,this.data.sy,!0,this._select_mode(e)),this.plot_view.push_state(\"poly_select\",{selection:this.plot_view.get_selection()}),this._clear_data()}_clear_data(){this.data={sx:[],sy:[]},this.model.overlay.update({xs:[],ys:[]})}_tap(e){const{sx:t,sy:s}=e;this.plot_view.frame.bbox.contains(t,s)&&(this.data.sx.push(t),this.data.sy.push(s),this.model.overlay.update({xs:c.copy(this.data.sx),ys:c.copy(this.data.sy)}))}_do_select(e,t,s,l){const i={type:\"poly\",sx:e,sy:t};this._select(i,s,l)}}s.PolySelectToolView=h,h.__name__=\"PolySelectToolView\",s.DEFAULT_POLY_OVERLAY=()=>new o.PolyAnnotation({level:\"overlay\",xs_units:\"screen\",ys_units:\"screen\",fill_color:\"lightgrey\",fill_alpha:.5,line_color:\"black\",line_alpha:1,line_width:2,line_dash:[4,4]});class y extends i.SelectTool{constructor(e){super(e),this.tool_name=\"Poly Select\",this.icon=n.bk_tool_icon_polygon_select,this.event_type=\"tap\",this.default_order=11}static init_PolySelectTool(){this.prototype.default_view=h,this.define({overlay:[_.Instance,s.DEFAULT_POLY_OVERLAY]}),this.register_alias(\"poly_select\",()=>new y)}}s.PolySelectTool=y,y.__name__=\"PolySelectTool\",y.init_PolySelectTool()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),n=e(370),r=s.__importStar(e(18)),_=e(309);class d extends n.LineToolView{constructor(){super(...arguments),this._drawing=!1}_doubletap(e){if(!this.model.active)return;const t=this.model.renderers;for(const i of t){1==this._select_event(e,\"replace\",[i]).length&&(this._selected_renderer=i)}this._show_intersections(),this._update_line_cds()}_show_intersections(){if(!this.model.active)return;if(null==this._selected_renderer)return;if(!this.model.renderers.length)return this._set_intersection([],[]),this._selected_renderer=null,void(this._drawing=!1);const e=this._selected_renderer.data_source,t=this._selected_renderer.glyph,[i,s]=[t.x.field,t.y.field],n=e.get_array(i),r=e.get_array(s);this._set_intersection(n,r)}_tap(e){const t=this.model.intersection_renderer;if(null==this._map_drag(e.sx,e.sy,t))return;if(this._drawing&&this._selected_renderer){const i=this._select_mode(e);if(0==this._select_event(e,i,[t]).length)return}const i=this._select_mode(e);this._select_event(e,i,[t]),this._select_event(e,i,this.model.renderers)}_update_line_cds(){if(null==this._selected_renderer)return;const e=this.model.intersection_renderer.glyph,t=this.model.intersection_renderer.data_source,[i,s]=[e.x.field,e.y.field];if(i&&s){const e=t.data[i],n=t.data[s];this._selected_renderer.data_source.data[i]=e,this._selected_renderer.data_source.data[s]=n}this._emit_cds_changes(this._selected_renderer.data_source,!0,!0,!1)}_pan_start(e){this._select_event(e,\"append\",[this.model.intersection_renderer]),this._basepoint=[e.sx,e.sy]}_pan(e){null!=this._basepoint&&(this._drag_points(e,[this.model.intersection_renderer],this.model.dimensions),this._selected_renderer&&this._selected_renderer.data_source.change.emit())}_pan_end(e){null!=this._basepoint&&(this._drag_points(e,[this.model.intersection_renderer]),this._emit_cds_changes(this.model.intersection_renderer.data_source,!1,!0,!0),this._selected_renderer&&this._emit_cds_changes(this._selected_renderer.data_source),this._basepoint=null)}activate(){this._drawing=!0}deactivate(){this._selected_renderer&&(this._drawing&&(this._drawing=!1),this._hide_intersections())}}i.LineEditToolView=d,d.__name__=\"LineEditToolView\";class o extends n.LineTool{constructor(e){super(e),this.tool_name=\"Line Edit Tool\",this.icon=_.bk_tool_icon_line_edit,this.event_type=[\"tap\",\"pan\",\"move\"],this.default_order=4}static init_LineEditTool(){this.prototype.default_view=d,this.define({dimensions:[r.Dimensions,\"both\"]})}get tooltip(){return this._get_dim_tooltip(this.tool_name,this.dimensions)}}i.LineEditTool=o,o.__name__=\"LineEditTool\",o.init_LineEditTool()},\n function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1).__importStar(e(18)),o=e(8),s=e(356);class _ extends s.EditToolView{_set_intersection(e,i){const t=this.model.intersection_renderer.glyph,n=this.model.intersection_renderer.data_source,[s,_]=[t.x.field,t.y.field];s&&(o.isArray(e)?n.data[s]=e:t.x={value:e}),_&&(o.isArray(i)?n.data[_]=i:t.y={value:i}),this._emit_cds_changes(n,!0,!0,!1)}_hide_intersections(){this._set_intersection([],[])}}t.LineToolView=_,_.__name__=\"LineToolView\";class r extends s.EditTool{constructor(e){super(e)}static init_LineTool(){this.prototype.default_view=_,this.define({intersection_renderer:[n.Instance]})}}t.LineTool=r,r.__name__=\"LineTool\",r.init_LineTool()},\n function _(t,s,e){Object.defineProperty(e,\"__esModule\",{value:!0});const n=t(1),i=t(306),o=n.__importStar(t(18)),a=t(309);function _(t,s,e){const n=new Map;for(const[i,o]of t){const[t,a]=o.r_invert(s,e);n.set(i,{start:t,end:a})}return n}e.update_ranges=_;class h extends i.GestureToolView{_pan_start(t){this.last_dx=0,this.last_dy=0;const{sx:s,sy:e}=t,n=this.plot_view.frame.bbox;if(!n.contains(s,e)){const t=n.h_range,i=n.v_range;(st.end)&&(this.v_axis_only=!0),(ei.end)&&(this.h_axis_only=!0)}null!=this.model.document&&this.model.document.interactive_start(this.plot_model)}_pan(t){this._update(t.deltaX,t.deltaY),null!=this.model.document&&this.model.document.interactive_start(this.plot_model)}_pan_end(t){this.h_axis_only=!1,this.v_axis_only=!1,null!=this.pan_info&&this.plot_view.push_state(\"pan\",{range:this.pan_info})}_update(t,s){const e=this.plot_view.frame,n=t-this.last_dx,i=s-this.last_dy,o=e.bbox.h_range,a=o.start-n,h=o.end-n,l=e.bbox.v_range,r=l.start-i,d=l.end-i,p=this.model.dimensions;let c,u,m,x,y,g;\"width\"!=p&&\"both\"!=p||this.v_axis_only?(c=o.start,u=o.end,m=0):(c=a,u=h,m=-n),\"height\"!=p&&\"both\"!=p||this.h_axis_only?(x=l.start,y=l.end,g=0):(x=r,y=d,g=-i),this.last_dx=t,this.last_dy=s;const{x_scales:w,y_scales:b}=e,f=_(w,c,u),v=_(b,x,y);this.pan_info={xrs:f,yrs:v,sdx:m,sdy:g},this.plot_view.update_range(this.pan_info,!0)}}e.PanToolView=h,h.__name__=\"PanToolView\";class l extends i.GestureTool{constructor(t){super(t),this.tool_name=\"Pan\",this.event_type=\"pan\",this.default_order=10}static init_PanTool(){this.prototype.default_view=h,this.define({dimensions:[o.Dimensions,\"both\"]}),this.register_alias(\"pan\",()=>new l({dimensions:\"both\"})),this.register_alias(\"xpan\",()=>new l({dimensions:\"width\"})),this.register_alias(\"ypan\",()=>new l({dimensions:\"height\"}))}get tooltip(){return this._get_dim_tooltip(\"Pan\",this.dimensions)}get icon(){switch(this.dimensions){case\"both\":return a.bk_tool_icon_pan;case\"width\":return a.bk_tool_icon_xpan;case\"height\":return a.bk_tool_icon_ypan}}}e.PanTool=l,l.__name__=\"PanTool\",l.init_PanTool()},\n function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),n=e(124),l=e(19),a=s.__importStar(e(18)),r=e(306),o=e(309);function _(e){switch(e){case 1:return 2;case 2:return 1;case 4:return 5;case 5:return 4;default:return e}}function h(e,t,i,s){if(null==t)return!1;const n=i.compute(t);return Math.abs(e-n)n.right)&&(l=!1)}if(null!=n.bottom&&null!=n.top){const e=s.invert(t);(en.top)&&(l=!1)}return l}function u(e,t,i){let s=0;return e>=i.start&&e<=i.end&&(s+=1),t>=i.start&&t<=i.end&&(s+=1),s}function c(e,t,i,s){const n=t.compute(e),l=t.invert(n+i);return l>=s.start&&l<=s.end?l:e}function g(e,t,i){return e>t.start?(t.end=e,i):(t.end=t.start,t.start=e,_(i))}function y(e,t,i){return e=o&&(e.start=a,e.end=r)}i.flip_side=_,i.is_near=h,i.is_inside=d,i.sides_inside=u,i.compute_value=c,i.update_range_end_side=g,i.update_range_start_side=y,i.update_range=f;class p extends r.GestureToolView{initialize(){super.initialize(),this.side=0,this.model.update_overlay_from_ranges()}connect_signals(){super.connect_signals(),null!=this.model.x_range&&this.connect(this.model.x_range.change,()=>this.model.update_overlay_from_ranges()),null!=this.model.y_range&&this.connect(this.model.y_range.change,()=>this.model.update_overlay_from_ranges())}_pan_start(e){this.last_dx=0,this.last_dy=0;const t=this.model.x_range,i=this.model.y_range,{frame:s}=this.plot_view,l=s.x_scale,a=s.y_scale,r=this.model.overlay,{left:o,right:_,top:u,bottom:c}=r,g=this.model.overlay.properties.line_width.value()+n.EDGE_TOLERANCE;null!=t&&this.model.x_interaction&&(h(e.sx,o,l,g)?this.side=1:h(e.sx,_,l,g)?this.side=2:d(e.sx,e.sy,l,a,r)&&(this.side=3)),null!=i&&this.model.y_interaction&&(0==this.side&&h(e.sy,c,a,g)&&(this.side=4),0==this.side&&h(e.sy,u,a,g)?this.side=5:d(e.sx,e.sy,l,a,this.model.overlay)&&(3==this.side?this.side=7:this.side=6))}_pan(e){const t=this.plot_view.frame,i=e.deltaX-this.last_dx,s=e.deltaY-this.last_dy,n=this.model.x_range,l=this.model.y_range,a=t.x_scale,r=t.y_scale;if(null!=n)if(3==this.side||7==this.side)f(n,a,i,t.x_range);else if(1==this.side){const e=c(n.start,a,i,t.x_range);this.side=y(e,n,this.side)}else if(2==this.side){const e=c(n.end,a,i,t.x_range);this.side=g(e,n,this.side)}if(null!=l)if(6==this.side||7==this.side)f(l,r,s,t.y_range);else if(4==this.side){const e=c(l.start,r,s,t.y_range);this.side=y(e,l,this.side)}else if(5==this.side){const e=c(l.end,r,s,t.y_range);this.side=g(e,l,this.side)}this.last_dx=e.deltaX,this.last_dy=e.deltaY}_pan_end(e){this.side=0}}i.RangeToolView=p,p.__name__=\"RangeToolView\";const m=()=>new n.BoxAnnotation({level:\"overlay\",fill_color:\"lightgrey\",fill_alpha:.5,line_color:\"black\",line_alpha:1,line_width:.5,line_dash:[2,2]});class v extends r.GestureTool{constructor(e){super(e),this.tool_name=\"Range Tool\",this.icon=o.bk_tool_icon_range,this.event_type=\"pan\",this.default_order=1}static init_RangeTool(){this.prototype.default_view=p,this.define({x_range:[a.Instance,null],x_interaction:[a.Boolean,!0],y_range:[a.Instance,null],y_interaction:[a.Boolean,!0],overlay:[a.Instance,m]})}initialize(){super.initialize(),this.overlay.in_cursor=\"grab\",this.overlay.ew_cursor=null!=this.x_range&&this.x_interaction?\"ew-resize\":null,this.overlay.ns_cursor=null!=this.y_range&&this.y_interaction?\"ns-resize\":null}update_overlay_from_ranges(){null==this.x_range&&null==this.y_range&&(this.overlay.left=null,this.overlay.right=null,this.overlay.bottom=null,this.overlay.top=null,l.logger.warn(\"RangeTool not configured with any Ranges.\")),null==this.x_range?(this.overlay.left=null,this.overlay.right=null):(this.overlay.left=this.x_range.start,this.overlay.right=this.x_range.end),null==this.y_range?(this.overlay.bottom=null,this.overlay.top=null):(this.overlay.bottom=this.y_range.start,this.overlay.top=this.y_range.end)}}i.RangeTool=v,v.__name__=\"RangeTool\",v.init_RangeTool()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const o=e(1),i=e(364),c=o.__importStar(e(18)),n=e(309);class a extends i.SelectToolView{_tap(e){const{sx:t,sy:s}=e,o={type:\"point\",sx:t,sy:s};this._select(o,!0,this._select_mode(e))}_select(e,t,s){const o=this.model.callback;if(\"select\"==this.model.behavior){const i=this._computed_renderers_by_data_source();for(const[,c]of i){const i=c[0].get_selection_manager(),n=c.map(e=>this.plot_view.renderer_views.get(e));if(i.select(n,e,t,s)&&null!=o){const t=n[0].coordinates.x_scale.invert(e.sx),s=n[0].coordinates.y_scale.invert(e.sy),c={geometries:Object.assign(Object.assign({},e),{x:t,y:s}),source:i.source};o.execute(this.model,c)}}this._emit_selection_event(e),this.plot_view.push_state(\"tap\",{selection:this.plot_view.get_selection()})}else for(const t of this.computed_renderers){const s=this.plot_view.renderer_views.get(t),i=t.get_selection_manager();if(i.inspect(s,e)&&null!=o){const t=s.coordinates.x_scale.invert(e.sx),c=s.coordinates.y_scale.invert(e.sy),n={geometries:Object.assign(Object.assign({},e),{x:t,y:c}),source:i.source};o.execute(this.model,n)}}}}s.TapToolView=a,a.__name__=\"TapToolView\";class _ extends i.SelectTool{constructor(e){super(e),this.tool_name=\"Tap\",this.icon=n.bk_tool_icon_tap_select,this.event_type=\"tap\",this.default_order=10}static init_TapTool(){this.prototype.default_view=a,this.define({behavior:[c.TapBehavior,\"select\"],callback:[c.Any]}),this.register_alias(\"click\",()=>new _({behavior:\"inspect\"})),this.register_alias(\"tap\",()=>new _)}}s.TapTool=_,_.__name__=\"TapTool\",_.init_TapTool()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),o=e(306),n=i.__importStar(e(18)),a=e(309),l=e(371);class _ extends o.GestureToolView{_scroll(e){let t=this.model.speed*e.delta;t>.9?t=.9:t<-.9&&(t=-.9),this._update_ranges(t)}_update_ranges(e){const{frame:t}=this.plot_view,s=t.bbox.h_range,i=t.bbox.v_range,[o,n]=[s.start,s.end],[a,_]=[i.start,i.end];let h,r,d,p;switch(this.model.dimension){case\"height\":{const t=Math.abs(_-a);h=o,r=n,d=a-t*e,p=_-t*e;break}case\"width\":{const t=Math.abs(n-o);h=o-t*e,r=n-t*e,d=a,p=_;break}default:throw new Error(\"this shouldn't have happened\")}const{x_scales:c,y_scales:u}=t,m={xrs:l.update_ranges(c,h,r),yrs:l.update_ranges(u,d,p),factor:e};this.plot_view.push_state(\"wheel_pan\",{range:m}),this.plot_view.update_range(m,!1,!0),null!=this.model.document&&this.model.document.interactive_start(this.plot_model)}}s.WheelPanToolView=_,_.__name__=\"WheelPanToolView\";class h extends o.GestureTool{constructor(e){super(e),this.tool_name=\"Wheel Pan\",this.icon=a.bk_tool_icon_wheel_pan,this.event_type=\"scroll\",this.default_order=12}static init_WheelPanTool(){this.prototype.default_view=_,this.define({dimension:[n.Dimension,\"width\"]}),this.internal({speed:[n.Number,.001]}),this.register_alias(\"xwheel_pan\",()=>new h({dimension:\"width\"})),this.register_alias(\"ywheel_pan\",()=>new h({dimension:\"height\"}))}get tooltip(){return this._get_dim_tooltip(this.tool_name,this.dimension)}}s.WheelPanTool=h,h.__name__=\"WheelPanTool\",h.init_WheelPanTool()},\n function _(e,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=e(1),i=e(306),l=e(354),n=s.__importStar(e(18)),_=e(32),h=e(309);class a extends i.GestureToolView{_pinch(e){const{sx:o,sy:t,scale:s,ctrlKey:i,shiftKey:l}=e;let n;n=s>=1?20*(s-1):-20/s,this._scroll({type:\"wheel\",sx:o,sy:t,delta:n,ctrlKey:i,shiftKey:l})}_scroll(e){const{frame:o}=this.plot_view,t=o.bbox.h_range,s=o.bbox.v_range,{sx:i,sy:n}=e,_=this.model.dimensions,h=(\"width\"==_||\"both\"==_)&&t.startnew m({dimensions:\"both\"})),this.register_alias(\"xwheel_zoom\",()=>new m({dimensions:\"width\"})),this.register_alias(\"ywheel_zoom\",()=>new m({dimensions:\"height\"}))}get tooltip(){return this._get_dim_tooltip(this.tool_name,this.dimensions)}}t.WheelZoomTool=m,m.__name__=\"WheelZoomTool\",m.init_WheelZoomTool()},\n function _(i,s,e){Object.defineProperty(e,\"__esModule\",{value:!0});const t=i(1),o=i(295),n=i(168),l=t.__importStar(i(18)),h=i(13),a=i(309);class r extends o.InspectToolView{_move(i){if(!this.model.active)return;const{sx:s,sy:e}=i;this.plot_view.frame.bbox.contains(s,e)?this._update_spans(s,e):this._update_spans(null,null)}_move_exit(i){this._update_spans(null,null)}_update_spans(i,s){const e=this.model.dimensions;\"width\"!=e&&\"both\"!=e||(this.model.spans.width.location=s),\"height\"!=e&&\"both\"!=e||(this.model.spans.height.location=i)}}e.CrosshairToolView=r,r.__name__=\"CrosshairToolView\";class _ extends o.InspectTool{constructor(i){super(i),this.tool_name=\"Crosshair\",this.icon=a.bk_tool_icon_crosshair}static init_CrosshairTool(){this.prototype.default_view=r,this.define({dimensions:[l.Dimensions,\"both\"],line_color:[l.Color,\"black\"],line_width:[l.Number,1],line_alpha:[l.Number,1]}),this.internal({spans:[l.Any]}),this.register_alias(\"crosshair\",()=>new _)}get tooltip(){return this._get_dim_tooltip(\"Crosshair\",this.dimensions)}get synthetic_renderers(){return h.values(this.spans)}initialize(){super.initialize(),this.spans={width:new n.Span({for_hover:!0,dimension:\"width\",location_units:\"screen\",level:\"overlay\",line_color:this.line_color,line_width:this.line_width,line_alpha:this.line_alpha}),height:new n.Span({for_hover:!0,dimension:\"height\",location_units:\"screen\",level:\"overlay\",line_color:this.line_color,line_width:this.line_width,line_alpha:this.line_alpha})}}}e.CrosshairTool=_,_.__name__=\"CrosshairTool\",_.init_CrosshairTool()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const r=e(1),o=e(81),i=r.__importStar(e(18)),a=e(13),n=e(29);class u extends o.Model{constructor(e){super(e)}static init_CustomJSHover(){this.define({args:[i.Any,{}],code:[i.String,\"\"]})}get values(){return a.values(this.args)}_make_code(e,t,s,r){return new Function(...a.keys(this.args),e,t,s,n.use_strict(r))}format(e,t,s){return this._make_code(\"value\",\"format\",\"special_vars\",this.code)(...this.values,e,t,s)}}s.CustomJSHover=u,u.__name__=\"CustomJSHover\",u.init_CustomJSHover()},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const o=e(1),n=e(295),i=e(171),r=e(90),l=e(116),c=e(365),a=o.__importStar(e(101)),_=e(187),d=e(72),p=o.__importStar(e(18)),h=e(22),m=e(13),u=e(303),y=e(8),f=e(115),x=e(309),v=e(172);function w(e,t,s,o,n,i){const r={x:n[e],y:i[e]},l={x:n[e+1],y:i[e+1]};let c,_;if(\"span\"==t.type)\"h\"==t.direction?(c=Math.abs(r.x-s),_=Math.abs(l.x-s)):(c=Math.abs(r.y-o),_=Math.abs(l.y-o));else{const e={x:s,y:o};c=a.dist_2_pts(r,e),_=a.dist_2_pts(l,e)}return c<_?[[r.x,r.y],e]:[[l.x,l.y],e+1]}function g(e,t,s){return[[e[s],t[s]],s]}s._nearest_line_hit=w,s._line_hit=g;class b extends n.InspectToolView{initialize(){super.initialize(),this._ttmodels=null,this._ttviews=new Map;const{tooltips:e}=this.model;y.isArray(e)&&(this._template_el=this._create_template(e))}remove(){f.remove_views(this._ttviews),super.remove()}connect_signals(){super.connect_signals();for(const e of this.computed_renderers)e instanceof r.GlyphRenderer?this.connect(e.data_source.inspect,this._update):e instanceof l.GraphRenderer&&(this.connect(e.node_renderer.data_source.inspect,this._update),this.connect(e.edge_renderer.data_source.inspect,this._update));this.connect(this.model.properties.renderers.change,()=>this._computed_renderers=this._ttmodels=null),this.connect(this.model.properties.names.change,()=>this._computed_renderers=this._ttmodels=null),this.connect(this.model.properties.tooltips.change,()=>this._ttmodels=null)}_compute_ttmodels(){const e=new Map,t=this.model.tooltips;if(null!=t)for(const s of this.computed_renderers){const o=new i.Tooltip({custom:y.isString(t)||y.isFunction(t),attachment:this.model.attachment,show_arrow:this.model.show_arrow});s instanceof r.GlyphRenderer?e.set(s,o):s instanceof l.GraphRenderer&&(e.set(s.node_renderer,o),e.set(s.edge_renderer,o))}return(async()=>{const t=await f.build_views(this._ttviews,[...e.values()],{parent:this.plot_view});for(const e of t)e.render()})(),e}get computed_renderers(){if(null==this._computed_renderers){const e=this.model.renderers,t=this.plot_model.renderers,s=this.model.names;this._computed_renderers=c.compute_renderers(e,t,s)}return this._computed_renderers}get ttmodels(){return null==this._ttmodels&&(this._ttmodels=this._compute_ttmodels()),this._ttmodels}_clear(){this._inspect(1/0,1/0);for(const[,e]of this.ttmodels)e.clear()}_move(e){if(!this.model.active)return;const{sx:t,sy:s}=e;this.plot_view.frame.bbox.contains(t,s)?this._inspect(t,s):this._clear()}_move_exit(){this._clear()}_inspect(e,t){let s;if(\"mouse\"==this.model.mode)s={type:\"point\",sx:e,sy:t};else{s={type:\"span\",direction:\"vline\"==this.model.mode?\"h\":\"v\",sx:e,sy:t}}for(const e of this.computed_renderers){e.get_selection_manager().inspect(this.plot_view.renderer_views.get(e),s)}null!=this.model.callback&&this._emit_callback(s)}_update([e,{geometry:t}]){if(!this.model.active)return;if(!(e instanceof r.GlyphRendererView))return;const{model:s}=e;if(\"ignore\"==this.model.muted_policy&&s instanceof r.GlyphRenderer&&s.muted)return;const o=this.ttmodels.get(s);if(null==o)return;const n=s.get_selection_manager();let i=n.inspectors.get(s);if(s instanceof r.GlyphRenderer&&(i=s.view.convert_selection_to_subset(i)),i.is_empty())return void o.clear();const l=n.source,{sx:c,sy:a}=t,_=e.coordinates.x_scale,p=e.coordinates.y_scale,h=_.invert(c),u=p.invert(a),y=e.glyph,f=[];for(const s of i.line_indices){let o,n,r=y._x[s+1],d=y._y[s+1],m=s;switch(this.model.line_policy){case\"interp\":[r,d]=y.get_interpolation_hit(s,t),o=_.compute(r),n=p.compute(d);break;case\"prev\":[[o,n],m]=g(y.sx,y.sy,s);break;case\"next\":[[o,n],m]=g(y.sx,y.sy,s+1);break;case\"nearest\":[[o,n],m]=w(s,t,c,a,y.sx,y.sy),r=y._x[m],d=y._y[m];break;default:[o,n]=[c,a]}const x={index:m,x:h,y:u,sx:c,sy:a,data_x:r,data_y:d,rx:o,ry:n,indices:i.line_indices,name:e.model.name};f.push([o,n,this._render_tooltips(l,m,x)])}for(const t of i.image_indices){const s={index:t.index,x:h,y:u,sx:c,sy:a,name:e.model.name},o=this._render_tooltips(l,t,s);f.push([c,a,o])}for(const o of i.indices)if(m.isEmpty(i.multiline_indices)){const t=null!=y._x?y._x[o]:void 0,n=null!=y._y?y._y[o]:void 0;let _,d,p;if(\"snap_to_data\"==this.model.point_policy){let e=y.get_anchor_point(this.model.anchor,o,[c,a]);null==e&&(e=y.get_anchor_point(\"center\",o,[c,a])),_=e.x,d=e.y}else[_,d]=[c,a];p=s instanceof r.GlyphRenderer?s.view.convert_indices_from_subset([o])[0]:o;const m={index:p,x:h,y:u,sx:c,sy:a,data_x:t,data_y:n,indices:i.indices,name:e.model.name};f.push([_,d,this._render_tooltips(l,p,m)])}else for(const n of i.multiline_indices[o.toString()]){let d,m,x,v=y._xs[o][n],b=y._ys[o][n],k=n;switch(this.model.line_policy){case\"interp\":[v,b]=y.get_interpolation_hit(o,n,t),d=_.compute(v),m=p.compute(b);break;case\"prev\":[[d,m],k]=g(y.sxs[o],y.sys[o],n);break;case\"next\":[[d,m],k]=g(y.sxs[o],y.sys[o],n+1);break;case\"nearest\":[[d,m],k]=w(n,t,c,a,y.sxs[o],y.sys[o]),v=y._xs[o][k],b=y._ys[o][k];break;default:throw new Error(\"should't have happened\")}x=s instanceof r.GlyphRenderer?s.view.convert_indices_from_subset([o])[0]:o;const A={index:x,x:h,y:u,sx:c,sy:a,data_x:v,data_y:b,segment_index:k,indices:i.multiline_indices,name:e.model.name};f.push([d,m,this._render_tooltips(l,x,A)])}if(0==f.length)o.clear();else{const{content:e}=o;d.empty(o.content);for(const[,,t]of f)e.appendChild(t);const[t,s]=f[f.length-1];o.setv({position:[t,s]},{check_eq:!1})}}_emit_callback(e){for(const t of this.computed_renderers){const s=this.plot_view.renderer_views.get(t),o=s.coordinates.x_scale.invert(e.sx),n=s.coordinates.y_scale.invert(e.sy),i=t.data_source.inspected,r=Object.assign({x:o,y:n},e);this.model.callback.execute(this.model,{index:i,geometry:r,renderer:t})}}_create_template(e){const t=d.div({style:{display:\"table\",borderSpacing:\"2px\"}});for(const[s]of e){const e=d.div({style:{display:\"table-row\"}});t.appendChild(e);const o=d.div({style:{display:\"table-cell\"},class:v.bk_tooltip_row_label},0!=s.length?s+\": \":\"\");e.appendChild(o);const n=d.span();n.dataset.value=\"\";const i=d.span({class:v.bk_tooltip_color_block},\" \");i.dataset.swatch=\"\",d.undisplay(i);const r=d.div({style:{display:\"table-cell\"},class:v.bk_tooltip_row_value},n,i);e.appendChild(r)}return t}_render_template(e,t,s,o,n){const i=e.cloneNode(!0),r=i.querySelectorAll(\"[data-value]\"),l=i.querySelectorAll(\"[data-swatch]\"),c=/\\$color(\\[.*\\])?:(\\w*)/;for(const[[,e],i]of u.enumerate(t)){const t=e.match(c);if(null!=t){const[,e=\"\",n]=t,c=s.get_column(n);if(null==c){r[i].textContent=n+\" unknown\";continue}const a=e.indexOf(\"hex\")>=0,_=e.indexOf(\"swatch\")>=0;let p=y.isNumber(o)?c[o]:null;if(null==p){r[i].textContent=\"(null)\";continue}a&&(p=h.color2hex(p)),r[i].textContent=p,_&&(l[i].style.backgroundColor=p,d.display(l[i]))}else{const t=_.replace_placeholders(e.replace(\"$~\",\"$data_\"),s,o,this.model.formatters,n);if(y.isString(t))r[i].textContent=t;else for(const e of t)r[i].appendChild(e)}}return i}_render_tooltips(e,t,s){const o=this.model.tooltips;if(y.isString(o)){const n=_.replace_placeholders({html:o},e,t,this.model.formatters,s);return d.div({},n)}return y.isFunction(o)?o(e,s):this._render_template(this._template_el,o,e,t,s)}}s.HoverToolView=b,b.__name__=\"HoverToolView\";class k extends n.InspectTool{constructor(e){super(e),this.tool_name=\"Hover\",this.icon=x.bk_tool_icon_hover}static init_HoverTool(){this.prototype.default_view=b,this.define({tooltips:[p.Any,[[\"index\",\"$index\"],[\"data (x, y)\",\"($x, $y)\"],[\"screen (x, y)\",\"($sx, $sy)\"]]],formatters:[p.Any,{}],renderers:[p.Any,\"auto\"],names:[p.Array,[]],mode:[p.HoverMode,\"mouse\"],muted_policy:[p.MutedPolicy,\"show\"],point_policy:[p.PointPolicy,\"snap_to_data\"],line_policy:[p.LinePolicy,\"nearest\"],show_arrow:[p.Boolean,!0],anchor:[p.Anchor,\"center\"],attachment:[p.TooltipAttachment,\"horizontal\"],callback:[p.Any]}),this.register_alias(\"hover\",()=>new k)}}s.HoverTool=k,k.__name__=\"HoverTool\",k.init_HoverTool()},\n function _(t,o,e){Object.defineProperty(e,\"__esModule\",{value:!0});const i=t(1).__importStar(t(18)),n=t(15),s=t(81),l=t(295),c=t(303);class r extends s.Model{constructor(t){super(t)}static init_ToolProxy(){this.define({tools:[i.Array,[]],active:[i.Boolean,!1],disabled:[i.Boolean,!1]})}get button_view(){return this.tools[0].button_view}get event_type(){return this.tools[0].event_type}get tooltip(){return this.tools[0].tooltip}get tool_name(){return this.tools[0].tool_name}get icon(){return this.tools[0].computed_icon}get computed_icon(){return this.icon}get toggleable(){const t=this.tools[0];return t instanceof l.InspectTool&&t.toggleable}initialize(){super.initialize(),this.do=new n.Signal0(this,\"do\")}connect_signals(){super.connect_signals(),this.connect(this.do,()=>this.doit()),this.connect(this.properties.active.change,()=>this.set_active());for(const t of this.tools)this.connect(t.properties.active.change,()=>{this.active=t.active})}doit(){for(const t of this.tools)t.do.emit()}set_active(){for(const t of this.tools)t.active=this.active}get menu(){const{menu:t}=this.tools[0];if(null==t)return null;const o=[];for(const[e,i]of c.enumerate(t))if(null==e)o.push(null);else{const t=()=>{var t,o;for(const e of this.tools)null===(o=null===(t=e.menu)||void 0===t?void 0:t[i])||void 0===o||o.handler()};o.push(Object.assign(Object.assign({},e),{handler:t}))}return o}}e.ToolProxy=r,r.__name__=\"ToolProxy\",r.init_ToolProxy()},\n function _(o,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=o(1).__importStar(o(18)),e=o(9),n=o(13),r=o(305),l=o(379),c=o(272),h=o(212);class a extends r.ToolbarBase{constructor(o){super(o)}static init_ProxyToolbar(){this.define({toolbars:[i.Array,[]]})}initialize(){super.initialize(),this._merge_tools()}_merge_tools(){this._proxied_tools=[];const o={},t={},s={},i=[],r=[];for(const o of this.help)e.includes(r,o.redirect)||(i.push(o),r.push(o.redirect));this._proxied_tools.push(...i),this.help=i;for(const[o,t]of n.entries(this.gestures)){o in s||(s[o]={});for(const i of t.tools)i.type in s[o]||(s[o][i.type]=[]),s[o][i.type].push(i)}for(const t of this.inspectors)t.type in o||(o[t.type]=[]),o[t.type].push(t);for(const o of this.actions)o.type in t||(t[o.type]=[]),t[o.type].push(o);const c=(o,t=!1)=>{const s=new l.ToolProxy({tools:o,active:t});return this._proxied_tools.push(s),s};for(const o of n.keys(s)){const t=this.gestures[o];t.tools=[];for(const i of n.keys(s[o])){const e=s[o][i];if(e.length>0)if(\"multi\"==o)for(const o of e){const s=c([o]);t.tools.push(s),this.connect(s.properties.active.change,()=>this._active_change(s))}else{const o=c(e);t.tools.push(o),this.connect(o.properties.active.change,()=>this._active_change(o))}}}this.actions=[];for(const[o,s]of n.entries(t))if(\"CustomAction\"==o)for(const o of s)this.actions.push(c([o]));else s.length>0&&this.actions.push(c(s));this.inspectors=[];for(const t of n.values(o))t.length>0&&this.inspectors.push(c(t,!0));for(const[o,t]of n.entries(this.gestures))0!=t.tools.length&&(t.tools=e.sort_by(t.tools,o=>o.default_order),\"pinch\"!=o&&\"scroll\"!=o&&\"multi\"!=o&&(t.tools[0].active=!0))}}s.ProxyToolbar=a,a.__name__=\"ProxyToolbar\",a.init_ProxyToolbar();class _ extends c.LayoutDOMView{initialize(){this.model.toolbar.toolbar_location=this.model.toolbar_location,super.initialize()}get child_models(){return[this.model.toolbar]}_update_layout(){this.layout=new h.ContentBox(this.child_views[0].el);const{toolbar:o}=this.model;o.horizontal?this.layout.set_sizing({width_policy:\"fit\",min_width:100,height_policy:\"fixed\"}):this.layout.set_sizing({width_policy:\"fixed\",height_policy:\"fit\",min_height:100})}}s.ToolbarBoxView=_,_.__name__=\"ToolbarBoxView\";class p extends c.LayoutDOM{constructor(o){super(o)}static init_ToolbarBox(){this.prototype.default_view=_,this.define({toolbar:[i.Instance],toolbar_location:[i.Location,\"right\"]})}}s.ToolbarBox=p,p.__name__=\"ToolbarBox\",p.init_ToolbarBox()},\n function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=e(5),i=e(78),d=e(115),c=e(72),l=e(382);t.index={},t.add_document_standalone=async function(e,n,s=[],a=!1){const u=new Map;async function r(o){let a;const r=e.roots().indexOf(o),f=s[r];null!=f?a=f:n.classList.contains(l.BOKEH_ROOT)?a=n:(a=c.div({class:l.BOKEH_ROOT}),n.appendChild(a));const v=await d.build_view(o,{parent:null});return v instanceof i.DOMView&&v.renderTo(a),u.set(o,v),t.index[o.id]=v,v}for(const n of e.roots())await r(n);return a&&(window.document.title=e.title()),e.on_change(e=>{e instanceof o.RootAddedEvent?r(e.model):e instanceof o.RootRemovedEvent?function(e){const n=u.get(e);null!=n&&(n.remove(),u.delete(e),delete t.index[e.id])}(e.model):a&&e instanceof o.TitleChangedEvent&&(window.document.title=e.title)}),[...u.values()]}},\n function _(e,o,n){Object.defineProperty(n,\"__esModule\",{value:!0});const t=e(72),r=e(273);function l(e){let o=document.getElementById(e);if(null==o)throw new Error(`Error rendering Bokeh model: could not find #${e} HTML tag`);if(!document.body.contains(o))throw new Error(`Error rendering Bokeh model: element #${e} must be under `);if(\"SCRIPT\"==o.tagName){const e=t.div({class:n.BOKEH_ROOT});t.replaceWith(o,e),o=e}return o}n.BOKEH_ROOT=r.bk_root,n._resolve_element=function(e){const{elementid:o}=e;return null!=o?l(o):document.body},n._resolve_root_elements=function(e){const o=[];if(null!=e.root_ids&&null!=e.roots)for(const n of e.root_ids)o.push(l(e.roots[n]));return o}},\n function _(n,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});const e=n(384),s=n(19),c=n(381);t._get_ws_url=function(n,o){let t,e=\"ws:\";return\"https:\"==window.location.protocol&&(e=\"wss:\"),null!=o?(t=document.createElement(\"a\"),t.href=o):t=window.location,null!=n?\"/\"==n&&(n=\"\"):n=t.pathname.replace(/\\/+$/,\"\"),e+\"//\"+t.host+n+\"/ws\"};const r={};t.add_document_from_session=async function(n,o,t,a=[],i=!1){const l=window.location.search.substr(1);let d;try{d=await function(n,o,t){const s=e.parse_token(o).session_id;n in r||(r[n]={});const c=r[n];return s in c||(c[s]=e.pull_session(n,o,t)),c[s]}(n,o,l)}catch(n){const t=e.parse_token(o).session_id;throw s.logger.error(`Failed to load Bokeh session ${t}: ${n}`),n}return c.add_document_standalone(d.document,t,a,i)}},\n function _(e,s,n){Object.defineProperty(n,\"__esModule\",{value:!0});const t=e(19),o=e(5),r=e(385),i=e(386),c=e(387);n.DEFAULT_SERVER_WEBSOCKET_URL=\"ws://localhost:5006/ws\",n.DEFAULT_TOKEN=\"eyJzZXNzaW9uX2lkIjogImRlZmF1bHQifQ\";let l=0;function _(e){let s=e.split(\".\")[0];const n=s.length%4;return 0!=n&&(s+=\"=\".repeat(4-n)),JSON.parse(atob(s.replace(/_/g,\"/\").replace(/-/g,\"+\")))}n.parse_token=_;class h{constructor(e=n.DEFAULT_SERVER_WEBSOCKET_URL,s=n.DEFAULT_TOKEN,o=null){this.url=e,this.token=s,this.args_string=o,this._number=l++,this.socket=null,this.session=null,this.closed_permanently=!1,this._current_handler=null,this._pending_replies=new Map,this._pending_messages=[],this._receiver=new i.Receiver,this.id=_(s).session_id.split(\".\")[0],t.logger.debug(`Creating websocket ${this._number} to '${this.url}' session '${this.id}'`)}async connect(){if(this.closed_permanently)throw new Error(\"Cannot connect() a closed ClientConnection\");if(null!=this.socket)throw new Error(\"Already connected\");this._current_handler=null,this._pending_replies.clear(),this._pending_messages=[];try{let e=\"\"+this.url;return null!=this.args_string&&this.args_string.length>0&&(e+=\"?\"+this.args_string),this.socket=new WebSocket(e,[\"bokeh\",this.token]),new Promise((e,s)=>{this.socket.binaryType=\"arraybuffer\",this.socket.onopen=()=>this._on_open(e,s),this.socket.onmessage=e=>this._on_message(e),this.socket.onclose=e=>this._on_close(e,s),this.socket.onerror=()=>this._on_error(s)})}catch(e){throw t.logger.error(\"websocket creation failed to url: \"+this.url),t.logger.error(\" - \"+e),e}}close(){this.closed_permanently||(t.logger.debug(\"Permanently closing websocket connection \"+this._number),this.closed_permanently=!0,null!=this.socket&&this.socket.close(1e3,\"close method called on ClientConnection \"+this._number),this.session._connection_closed())}_schedule_reconnect(e){setTimeout(()=>{this.closed_permanently||t.logger.info(`Websocket connection ${this._number} disconnected, will not attempt to reconnect`)},e)}send(e){if(null==this.socket)throw new Error(\"not connected so cannot send \"+e);e.send(this.socket)}async send_with_reply(e){const s=await new Promise((s,n)=>{this._pending_replies.set(e.msgid(),{resolve:s,reject:n}),this.send(e)});if(\"ERROR\"===s.msgtype())throw new Error(\"Error reply \"+s.content.text);return s}async _pull_doc_json(){const e=r.Message.create(\"PULL-DOC-REQ\",{}),s=await this.send_with_reply(e);if(!(\"doc\"in s.content))throw new Error(\"No 'doc' field in PULL-DOC-REPLY\");return s.content.doc}async _repull_session_doc(e,s){var n;t.logger.debug(this.session?\"Repulling session\":\"Pulling session for first time\");try{const n=await this._pull_doc_json();if(null==this.session)if(this.closed_permanently)t.logger.debug(\"Got new document after connection was already closed\"),s(new Error(\"The connection has been closed\"));else{const s=o.Document.from_json(n),i=o.Document._compute_patch_since_json(n,s);if(i.events.length>0){t.logger.debug(`Sending ${i.events.length} changes from model construction back to server`);const e=r.Message.create(\"PATCH-DOC\",{},i);this.send(e)}this.session=new c.ClientSession(this,s,this.id);for(const e of this._pending_messages)this.session.handle(e);this._pending_messages=[],t.logger.debug(\"Created a new session from new pulled doc\"),e(this.session)}else this.session.document.replace_with_json(n),t.logger.debug(\"Updated existing session with new pulled doc\")}catch(e){null===(n=console.trace)||void 0===n||n.call(console,e),t.logger.error(\"Failed to repull session \"+e),s(e)}}_on_open(e,s){t.logger.info(`Websocket connection ${this._number} is now open`),this._current_handler=n=>{this._awaiting_ack_handler(n,e,s)}}_on_message(e){null==this._current_handler&&t.logger.error(\"Got a message with no current handler set\");try{this._receiver.consume(e.data)}catch(e){this._close_bad_protocol(e.toString())}const s=this._receiver.message;if(null!=s){const e=s.problem();null!=e&&this._close_bad_protocol(e),this._current_handler(s)}}_on_close(e,s){t.logger.info(`Lost websocket ${this._number} connection, ${e.code} (${e.reason})`),this.socket=null,this._pending_replies.forEach(e=>e.reject(\"Disconnected\")),this._pending_replies.clear(),this.closed_permanently||this._schedule_reconnect(2e3),s(new Error(`Lost websocket connection, ${e.code} (${e.reason})`))}_on_error(e){t.logger.debug(\"Websocket error on socket \"+this._number);const s=\"Could not open websocket\";t.logger.error(\"Failed to connect to Bokeh server: \"+s),e(new Error(s))}_close_bad_protocol(e){t.logger.error(\"Closing connection: \"+e),null!=this.socket&&this.socket.close(1002,e)}_awaiting_ack_handler(e,s,n){\"ACK\"===e.msgtype()?(this._current_handler=e=>this._steady_state_handler(e),this._repull_session_doc(s,n)):this._close_bad_protocol(\"First message was not an ACK\")}_steady_state_handler(e){const s=e.reqid(),n=this._pending_replies.get(s);n?(this._pending_replies.delete(s),n.resolve(e)):this.session?this.session.handle(e):\"PATCH-DOC\"!=e.msgtype()&&this._pending_messages.push(e)}}n.ClientConnection=h,h.__name__=\"ClientConnection\",n.pull_session=function(e,s,n){return new h(e,s,n).connect()}},\n function _(e,s,t){Object.defineProperty(t,\"__esModule\",{value:!0});const r=e(29);class n{constructor(e,s,t){this.header=e,this.metadata=s,this.content=t,this.buffers=new Map}static assemble(e,s,t){const r=JSON.parse(e),i=JSON.parse(s),a=JSON.parse(t);return new n(r,i,a)}assemble_buffer(e,s){const t=null!=this.header.num_buffers?this.header.num_buffers:0;if(t<=this.buffers.size)throw new Error(\"too many buffers received, expecting \"+t);const{id:r}=JSON.parse(e);this.buffers.set(r,s)}static create(e,s,t={}){const r=n.create_header(e);return new n(r,s,t)}static create_header(e){return{msgid:r.uniqueId(),msgtype:e}}complete(){return null!=this.header&&null!=this.metadata&&null!=this.content&&(null==this.header.num_buffers||this.buffers.size==this.header.num_buffers)}send(e){if((null!=this.header.num_buffers?this.header.num_buffers:0)>0)throw new Error(\"BokehJS only supports receiving buffers, not sending\");const s=JSON.stringify(this.header),t=JSON.stringify(this.metadata),r=JSON.stringify(this.content);e.send(s),e.send(t),e.send(r)}msgid(){return this.header.msgid}msgtype(){return this.header.msgtype}reqid(){return this.header.reqid}problem(){return\"msgid\"in this.header?\"msgtype\"in this.header?null:\"No msgtype in header\":\"No msgid in header\"}}t.Message=n,n.__name__=\"Message\"},\n function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const _=e(385),r=e(8);class i{constructor(){this.message=null,this._partial=null,this._fragments=[],this._buf_header=null,this._current_consumer=this._HEADER}consume(e){this._current_consumer(e)}_HEADER(e){this._assume_text(e),this.message=null,this._partial=null,this._fragments=[e],this._buf_header=null,this._current_consumer=this._METADATA}_METADATA(e){this._assume_text(e),this._fragments.push(e),this._current_consumer=this._CONTENT}_CONTENT(e){this._assume_text(e),this._fragments.push(e);const[t,s,r]=this._fragments.slice(0,3);this._partial=_.Message.assemble(t,s,r),this._check_complete()}_BUFFER_HEADER(e){this._assume_text(e),this._buf_header=e,this._current_consumer=this._BUFFER_PAYLOAD}_BUFFER_PAYLOAD(e){this._assume_binary(e),this._partial.assemble_buffer(this._buf_header,e),this._check_complete()}_assume_text(e){if(!r.isString(e))throw new Error(\"Expected text fragment but received binary fragment\")}_assume_binary(e){if(!(e instanceof ArrayBuffer))throw new Error(\"Expected binary fragment but received text fragment\")}_check_complete(){this._partial.complete()?(this.message=this._partial,this._current_consumer=this._HEADER):this._current_consumer=this._BUFFER_HEADER}}s.Receiver=i,i.__name__=\"Receiver\"},\n function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const o=e(5),s=e(385),c=e(19);class i{constructor(e,t,n){this._connection=e,this.document=t,this.id=n,this._document_listener=e=>{this._document_changed(e)},this.document.on_change(this._document_listener,!0)}handle(e){const t=e.msgtype();\"PATCH-DOC\"===t?this._handle_patch(e):\"OK\"===t?this._handle_ok(e):\"ERROR\"===t?this._handle_error(e):c.logger.debug(\"Doing nothing with message \"+e.msgtype())}close(){this._connection.close()}_connection_closed(){this.document.remove_on_change(this._document_listener)}async request_server_info(){const e=s.Message.create(\"SERVER-INFO-REQ\",{});return(await this._connection.send_with_reply(e)).content}async force_roundtrip(){await this.request_server_info()}_document_changed(e){if(e.setter_id===this.id)return;const t=e instanceof o.DocumentEventBatch?e.events:[e],n=this.document.create_json_patch(t),c=s.Message.create(\"PATCH-DOC\",{},n);this._connection.send(c)}_handle_patch(e){this.document.apply_json_patch(e.content,e.buffers,this.id)}_handle_ok(e){c.logger.trace(\"Unhandled OK reply to \"+e.reqid())}_handle_error(e){c.logger.error(`Unhandled ERROR reply to ${e.reqid()}: ${e.content.text}`)}}n.ClientSession=i,i.__name__=\"ClientSession\"},\n function _(e,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1);var r=this&&this.__asyncValues||function(e){if(!Symbol.asyncIterator)throw new TypeError(\"Symbol.asyncIterator is not defined.\");var o,t=e[Symbol.asyncIterator];return t?t.call(e):(e=\"function\"==typeof __values?__values(e):e[Symbol.iterator](),o={},n(\"next\"),n(\"throw\"),n(\"return\"),o[Symbol.asyncIterator]=function(){return this},o);function n(t){o[t]=e[t]&&function(o){return new Promise((function(n,r){(function(e,o,t,n){Promise.resolve(n).then((function(o){e({value:o,done:t})}),o)})(n,r,(o=e[t](o)).done,o.value)}))}}};const s=e(5),i=e(386),l=e(19),a=e(72),c=e(13),u=e(381),f=e(382),g=n.__importDefault(e(73)),m=n.__importDefault(e(311)),d=n.__importDefault(e(389));function p(e,o){o.buffers.length>0?e.consume(o.buffers[0].buffer):e.consume(o.content.data);const t=e.message;null!=t&&this.apply_json_patch(t.content,t.buffers)}function _(e,o){if(\"undefined\"!=typeof Jupyter&&null!=Jupyter.notebook.kernel){l.logger.info(\"Registering Jupyter comms for target \"+e);const t=Jupyter.notebook.kernel.comm_manager;try{t.register_target(e,t=>{l.logger.info(\"Registering Jupyter comms for target \"+e);const n=new i.Receiver;t.on_msg(p.bind(o,n))})}catch(e){l.logger.warn(`Jupyter comms failed to register. push_notebook() will not function. (exception reported: ${e})`)}}else if(o.roots()[0].id in t.kernels){l.logger.info(\"Registering JupyterLab comms for target \"+e);const n=t.kernels[o.roots()[0].id];try{n.registerCommTarget(e,t=>{l.logger.info(\"Registering JupyterLab comms for target \"+e);const n=new i.Receiver;t.onMsg=p.bind(o,n)})}catch(e){l.logger.warn(`Jupyter comms failed to register. push_notebook() will not function. (exception reported: ${e})`)}}else if(\"undefined\"!=typeof google&&null!=google.colab.kernel){l.logger.info(\"Registering Google Colab comms for target \"+e);const t=google.colab.kernel.comms;try{t.registerTarget(e,async t=>{var n,s,a;l.logger.info(\"Registering Google Colab comms for target \"+e);const c=new i.Receiver;try{for(var u,f=r(t.messages);!(u=await f.next()).done;){const e=u.value,t={data:e.data},n=[];for(const o of null!==(a=e.buffers)&&void 0!==a?a:[])n.push(new DataView(o));const r={content:t,buffers:n};p.bind(o)(c,r)}}catch(e){n={error:e}}finally{try{u&&!u.done&&(s=f.return)&&await s.call(f)}finally{if(n)throw n.error}}})}catch(e){l.logger.warn(`Google Colab comms failed to register. push_notebook() will not function. (exception reported: ${e})`)}}else console.warn(\"Jupyter notebooks comms not available. push_notebook() will not function. If running JupyterLab ensure the latest @bokeh/jupyter_bokeh extension is installed. In an exported notebook this warning is expected.\")}a.stylesheet.append(g.default),a.stylesheet.append(m.default),a.stylesheet.append(d.default),t.kernels={},t.embed_items_notebook=function(e,o){if(1!=c.size(e))throw new Error(\"embed_items_notebook expects exactly one document in docs_json\");const t=s.Document.from_json(c.values(e)[0]);for(const e of o){null!=e.notebook_comms_target&&_(e.notebook_comms_target,t);const o=f._resolve_element(e),n=f._resolve_root_elements(e);u.add_document_standalone(t,o,n)}}},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});o.default=\"\\n/* notebook specific tweaks so no black outline and matching padding\\n/* can't be wrapped inside bk-root. here are the offending jupyter lines:\\n/* https://github.com/jupyter/notebook/blob/master/notebook/static/notebook/less/renderedhtml.less#L59-L76 */\\n.rendered_html .bk-root .bk-tooltip table,\\n.rendered_html .bk-root .bk-tooltip tr,\\n.rendered_html .bk-root .bk-tooltip th,\\n.rendered_html .bk-root .bk-tooltip td {\\n border: none;\\n padding: 1px;\\n}\\n\"},\n function _(e,t,_){Object.defineProperty(_,\"__esModule\",{value:!0});const o=e(1);o.__exportStar(e(385),_),o.__exportStar(e(386),_)},\n function _(e,t,n){function s(){const e=document.getElementsByTagName(\"body\")[0],t=document.getElementsByClassName(\"bokeh-test-div\");1==t.length&&(e.removeChild(t[0]),delete t[0]);const n=document.createElement(\"div\");n.classList.add(\"bokeh-test-div\"),n.style.display=\"none\",e.insertBefore(n,e.firstChild)}Object.defineProperty(n,\"__esModule\",{value:!0}),n.results={},n.init=function(){s()},n.record0=function(e,t){n.results[e]=t},n.record=function(e,t){n.results[e]=t,s()},n.count=function(e){null==n.results[e]&&(n.results[e]=0),n.results[e]+=1,s()}},\n function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0}),o.safely=function(e,t=!1){try{return e()}catch(e){if(function(e){const t=document.createElement(\"div\");t.style.backgroundColor=\"#f2dede\",t.style.border=\"1px solid #a94442\",t.style.borderRadius=\"4px\",t.style.display=\"inline-block\",t.style.fontFamily=\"sans-serif\",t.style.marginTop=\"5px\",t.style.minWidth=\"200px\",t.style.padding=\"5px 5px 5px 10px\",t.classList.add(\"bokeh-error-box-into-flames\");const o=document.createElement(\"span\");o.style.backgroundColor=\"#a94442\",o.style.borderRadius=\"0px 4px 0px 0px\",o.style.color=\"white\",o.style.cursor=\"pointer\",o.style.cssFloat=\"right\",o.style.fontSize=\"0.8em\",o.style.margin=\"-6px -6px 0px 0px\",o.style.padding=\"2px 5px 4px 5px\",o.title=\"close\",o.setAttribute(\"aria-label\",\"close\"),o.appendChild(document.createTextNode(\"x\")),o.addEventListener(\"click\",()=>r.removeChild(t));const n=document.createElement(\"h3\");n.style.color=\"#a94442\",n.style.margin=\"8px 0px 0px 0px\",n.style.padding=\"0px\",n.appendChild(document.createTextNode(\"Bokeh Error\"));const l=document.createElement(\"pre\");l.style.whiteSpace=\"unset\",l.style.overflowX=\"auto\";const s=e instanceof Error?e.message:e;l.appendChild(document.createTextNode(s)),t.appendChild(o),t.appendChild(n),t.appendChild(l);const r=document.getElementsByTagName(\"body\")[0];r.insertBefore(t,r.firstChild)}(e),t)return;throw e}}},\n ], 0, {\"main\":0,\"tslib\":1,\"index\":2,\"version\":3,\"embed/index\":4,\"document/index\":5,\"document/document\":6,\"base\":7,\"core/util/types\":8,\"core/util/array\":9,\"core/util/math\":10,\"core/util/assert\":11,\"core/util/arrayable\":12,\"core/util/object\":13,\"core/has_props\":14,\"core/signaling\":15,\"core/util/callback\":16,\"core/util/refs\":17,\"core/properties\":18,\"core/logging\":19,\"core/enums\":20,\"core/kinds\":21,\"core/util/color\":22,\"core/util/svg_colors\":23,\"core/types\":24,\"core/util/eq\":25,\"core/util/data_structures\":26,\"core/settings\":27,\"core/property_mixins\":28,\"core/util/string\":29,\"core/util/ndarray\":30,\"core/util/serialization\":31,\"core/util/compat\":32,\"core/util/pretty\":33,\"models/index\":34,\"models/annotations/index\":35,\"models/annotations/annotation\":36,\"core/util/projections\":37,\"models/renderers/renderer\":70,\"core/view\":71,\"core/dom\":72,\"styles/root.css\":73,\"core/visuals\":74,\"core/util/svg\":75,\"core/util/affine\":76,\"models/canvas/canvas\":77,\"core/dom_view\":78,\"core/util/bbox\":79,\"core/util/canvas\":80,\"model\":81,\"models/canvas/coordinates\":82,\"models/annotations/arrow\":83,\"models/annotations/arrow_head\":84,\"models/sources/column_data_source\":85,\"models/sources/columnar_data_source\":86,\"models/sources/data_source\":87,\"models/selections/selection\":88,\"core/selection_manager\":89,\"models/renderers/glyph_renderer\":90,\"models/renderers/data_renderer\":91,\"models/glyphs/line\":92,\"models/glyphs/xy_glyph\":93,\"models/glyphs/glyph\":94,\"core/util/spatial\":95,\"models/ranges/factor_range\":98,\"models/ranges/range\":99,\"models/glyphs/utils\":100,\"core/hittest\":101,\"models/glyphs/webgl/line\":102,\"models/glyphs/webgl/utils/index\":103,\"models/glyphs/webgl/utils/program\":104,\"models/glyphs/webgl/utils/buffer\":105,\"models/glyphs/webgl/utils/texture\":106,\"models/glyphs/webgl/base\":107,\"models/glyphs/webgl/line.vert\":108,\"models/glyphs/webgl/line.frag\":109,\"models/glyphs/patch\":110,\"models/glyphs/harea\":111,\"models/glyphs/area\":112,\"models/glyphs/varea\":113,\"models/sources/cds_view\":114,\"core/build_views\":115,\"models/renderers/graph_renderer\":116,\"models/graphs/graph_hit_test_policy\":117,\"models/selections/interaction_policy\":118,\"core/util/typed_array\":119,\"core/util/set\":120,\"document/events\":121,\"models/annotations/band\":122,\"models/annotations/upper_lower\":123,\"models/annotations/box_annotation\":124,\"models/annotations/color_bar\":125,\"models/tickers/basic_ticker\":126,\"models/tickers/adaptive_ticker\":127,\"models/tickers/continuous_ticker\":128,\"models/tickers/ticker\":129,\"models/formatters/basic_tick_formatter\":130,\"models/formatters/tick_formatter\":131,\"models/mappers/index\":132,\"models/mappers/categorical_color_mapper\":133,\"models/mappers/categorical_mapper\":134,\"models/mappers/color_mapper\":135,\"models/mappers/mapper\":136,\"models/transforms/transform\":137,\"models/mappers/categorical_marker_mapper\":138,\"models/mappers/categorical_pattern_mapper\":139,\"models/mappers/continuous_color_mapper\":140,\"models/mappers/linear_color_mapper\":141,\"models/mappers/log_color_mapper\":142,\"models/mappers/scanning_color_mapper\":143,\"models/mappers/eqhist_color_mapper\":144,\"models/scales/linear_scale\":145,\"models/scales/continuous_scale\":146,\"models/scales/scale\":147,\"models/transforms/index\":148,\"models/transforms/customjs_transform\":149,\"models/transforms/dodge\":150,\"models/transforms/range_transform\":151,\"models/transforms/interpolator\":152,\"models/transforms/jitter\":153,\"models/transforms/linear_interpolator\":154,\"models/transforms/step_interpolator\":155,\"models/scales/linear_interpolation_scale\":156,\"models/scales/log_scale\":157,\"models/ranges/range1d\":158,\"core/util/text\":159,\"models/annotations/label\":160,\"models/annotations/text_annotation\":161,\"models/annotations/label_set\":162,\"models/annotations/legend\":163,\"models/annotations/legend_item\":164,\"core/vectorization\":165,\"models/annotations/poly_annotation\":166,\"models/annotations/slope\":167,\"models/annotations/span\":168,\"models/annotations/title\":169,\"models/annotations/toolbar_panel\":170,\"models/annotations/tooltip\":171,\"styles/tooltips\":172,\"styles/mixins\":173,\"styles/tooltips.css\":174,\"models/annotations/whisker\":175,\"models/axes/index\":176,\"models/axes/axis\":177,\"models/renderers/guide_renderer\":178,\"models/axes/categorical_axis\":179,\"models/tickers/categorical_ticker\":180,\"models/formatters/categorical_tick_formatter\":181,\"models/axes/continuous_axis\":182,\"models/axes/datetime_axis\":183,\"models/axes/linear_axis\":184,\"models/formatters/datetime_tick_formatter\":185,\"core/util/templating\":187,\"models/tickers/datetime_ticker\":190,\"models/tickers/composite_ticker\":191,\"models/tickers/days_ticker\":192,\"models/tickers/single_interval_ticker\":193,\"models/tickers/util\":194,\"models/tickers/months_ticker\":195,\"models/tickers/years_ticker\":196,\"models/axes/log_axis\":197,\"models/formatters/log_tick_formatter\":198,\"models/tickers/log_ticker\":199,\"models/axes/mercator_axis\":200,\"models/formatters/mercator_tick_formatter\":201,\"models/tickers/mercator_ticker\":202,\"models/callbacks/index\":203,\"models/callbacks/customjs\":204,\"models/callbacks/callback\":205,\"models/callbacks/open_url\":206,\"models/canvas/index\":207,\"models/canvas/cartesian_frame\":208,\"models/scales/categorical_scale\":209,\"models/ranges/data_range1d\":210,\"models/ranges/data_range\":211,\"core/layout/index\":212,\"core/layout/types\":213,\"core/layout/layoutable\":214,\"core/layout/alignments\":215,\"core/layout/grid\":216,\"core/layout/html\":217,\"models/expressions/index\":218,\"models/expressions/expression\":219,\"models/expressions/stack\":220,\"models/expressions/cumsum\":221,\"models/filters/index\":222,\"models/filters/boolean_filter\":223,\"models/filters/filter\":224,\"models/filters/customjs_filter\":225,\"models/filters/group_filter\":226,\"models/filters/index_filter\":227,\"models/formatters/index\":228,\"models/formatters/func_tick_formatter\":229,\"models/formatters/numeral_tick_formatter\":230,\"models/formatters/printf_tick_formatter\":231,\"models/glyphs/index\":232,\"models/glyphs/annular_wedge\":233,\"models/glyphs/annulus\":234,\"models/glyphs/arc\":235,\"models/glyphs/bezier\":236,\"models/glyphs/circle\":237,\"models/glyphs/webgl/markers\":238,\"models/glyphs/webgl/markers.vert\":239,\"models/glyphs/webgl/markers.frag\":240,\"models/glyphs/center_rotatable\":241,\"models/glyphs/ellipse\":242,\"models/glyphs/ellipse_oval\":243,\"models/glyphs/hbar\":244,\"models/glyphs/box\":245,\"models/glyphs/hex_tile\":246,\"models/glyphs/image\":247,\"models/glyphs/image_base\":248,\"models/glyphs/image_rgba\":249,\"models/glyphs/image_url\":250,\"core/util/image\":251,\"models/glyphs/multi_line\":252,\"models/glyphs/multi_polygons\":253,\"models/glyphs/oval\":254,\"models/glyphs/patches\":255,\"models/glyphs/quad\":256,\"models/glyphs/quadratic\":257,\"models/glyphs/ray\":258,\"models/glyphs/rect\":259,\"models/glyphs/segment\":260,\"models/glyphs/step\":261,\"models/glyphs/text\":262,\"models/glyphs/vbar\":263,\"models/glyphs/wedge\":264,\"models/graphs/index\":265,\"models/graphs/layout_provider\":266,\"models/graphs/static_layout_provider\":267,\"models/grids/index\":268,\"models/grids/grid\":269,\"models/layouts/index\":270,\"models/layouts/box\":271,\"models/layouts/layout_dom\":272,\"styles/root\":273,\"models/layouts/column\":274,\"models/layouts/grid_box\":275,\"models/layouts/html_box\":276,\"models/layouts/row\":277,\"models/layouts/spacer\":278,\"models/layouts/tabs\":279,\"styles/tabs\":280,\"styles/buttons\":281,\"styles/menus\":282,\"styles/buttons.css\":283,\"styles/menus.css\":284,\"styles/tabs.css\":285,\"models/layouts/widget_box\":286,\"models/markers/index\":287,\"models/markers/defs\":288,\"models/markers/marker\":289,\"models/markers/scatter\":290,\"models/plots/index\":291,\"models/plots/gmap_plot\":292,\"models/plots/plot\":293,\"models/tools/toolbar\":294,\"models/tools/inspectors/inspect_tool\":295,\"models/tools/button_tool\":296,\"models/tools/tool\":298,\"styles/toolbar\":299,\"styles/toolbar.css\":300,\"styles/icons.css\":301,\"core/util/menus\":302,\"core/util/iterator\":303,\"models/tools/on_off_button\":304,\"models/tools/toolbar_base\":305,\"models/tools/gestures/gesture_tool\":306,\"models/tools/actions/action_tool\":307,\"models/tools/actions/help_tool\":308,\"styles/icons\":309,\"styles/logo\":310,\"styles/logo.css\":311,\"models/plots/plot_canvas\":312,\"core/bokeh_events\":313,\"core/ui_events\":314,\"core/util/wheel\":315,\"core/util/throttle\":316,\"core/layout/border\":317,\"core/layout/side_panel\":318,\"models/plots/gmap_plot_canvas\":319,\"models/ranges/index\":320,\"models/renderers/index\":321,\"models/scales/index\":322,\"models/selections/index\":323,\"models/sources/index\":324,\"models/sources/server_sent_data_source\":325,\"models/sources/web_data_source\":326,\"models/sources/ajax_data_source\":327,\"models/sources/geojson_data_source\":328,\"models/tickers/index\":329,\"models/tickers/fixed_ticker\":330,\"models/tiles/index\":331,\"models/tiles/bbox_tile_source\":332,\"models/tiles/mercator_tile_source\":333,\"models/tiles/tile_source\":334,\"models/tiles/tile_utils\":335,\"models/tiles/quadkey_tile_source\":336,\"models/tiles/tile_renderer\":337,\"models/tiles/wmts_tile_source\":338,\"styles/tiles\":339,\"styles/tiles.css\":340,\"models/tiles/tms_tile_source\":341,\"models/textures/index\":342,\"models/textures/canvas_texture\":343,\"models/textures/texture\":344,\"models/textures/image_url_texture\":345,\"models/tools/index\":346,\"models/tools/actions/custom_action\":347,\"models/tools/actions/redo_tool\":348,\"models/tools/actions/reset_tool\":349,\"models/tools/actions/save_tool\":350,\"models/tools/actions/undo_tool\":351,\"models/tools/actions/zoom_in_tool\":352,\"models/tools/actions/zoom_base_tool\":353,\"core/util/zoom\":354,\"models/tools/actions/zoom_out_tool\":355,\"models/tools/edit/edit_tool\":356,\"models/tools/edit/box_edit_tool\":357,\"models/tools/edit/freehand_draw_tool\":358,\"models/tools/edit/point_draw_tool\":359,\"models/tools/edit/poly_draw_tool\":360,\"models/tools/edit/poly_tool\":361,\"models/tools/edit/poly_edit_tool\":362,\"models/tools/gestures/box_select_tool\":363,\"models/tools/gestures/select_tool\":364,\"models/tools/util\":365,\"models/tools/gestures/box_zoom_tool\":366,\"models/tools/gestures/lasso_select_tool\":367,\"models/tools/gestures/poly_select_tool\":368,\"models/tools/edit/line_edit_tool\":369,\"models/tools/edit/line_tool\":370,\"models/tools/gestures/pan_tool\":371,\"models/tools/gestures/range_tool\":372,\"models/tools/gestures/tap_tool\":373,\"models/tools/gestures/wheel_pan_tool\":374,\"models/tools/gestures/wheel_zoom_tool\":375,\"models/tools/inspectors/crosshair_tool\":376,\"models/tools/inspectors/customjs_hover\":377,\"models/tools/inspectors/hover_tool\":378,\"models/tools/tool_proxy\":379,\"models/tools/toolbar_box\":380,\"embed/standalone\":381,\"embed/dom\":382,\"embed/server\":383,\"client/connection\":384,\"protocol/message\":385,\"protocol/receiver\":386,\"client/session\":387,\"embed/notebook\":388,\"styles/notebook.css\":389,\"protocol/index\":390,\"testing\":391,\"safely\":392}, {});\n })\n\n\n /* END bokeh.min.js */\n },\n \n function(Bokeh) {\n /* BEGIN bokeh-widgets.min.js */\n /*!\n * Copyright (c) 2012 - 2020, Anaconda, Inc., and Bokeh Contributors\n * All rights reserved.\n * \n * Redistribution and use in source and binary forms, with or without modification,\n * are permitted provided that the following conditions are met:\n * \n * Redistributions of source code must retain the above copyright notice,\n * this list of conditions and the following disclaimer.\n * \n * Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the documentation\n * and/or other materials provided with the distribution.\n * \n * Neither the name of Anaconda nor the names of any contributors\n * may be used to endorse or promote products derived from this software\n * without specific prior written permission.\n * \n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\n * THE POSSIBILITY OF SUCH DAMAGE.\n */\n (function(root, factory) {\n factory(root[\"Bokeh\"], \"2.2.3\");\n })(this, function(Bokeh, version) {\n var define;\n return (function(modules, entry, aliases, externals) {\n const bokeh = typeof Bokeh !== \"undefined\" && (version != null ? Bokeh[version] : Bokeh);\n if (bokeh != null) {\n return bokeh.register_plugin(modules, entry, aliases);\n } else {\n throw new Error(\"Cannot find Bokeh \" + version + \". You have to load it prior to loading plugins.\");\n }\n })\n ({\n 402: function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const r=e(1).__importStar(e(403));o.Widgets=r;e(7).register_models(r)},\n 403: function _(r,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});var a=r(404);t.AbstractButton=a.AbstractButton;var o=r(407);t.AbstractIcon=o.AbstractIcon;var u=r(408);t.AutocompleteInput=u.AutocompleteInput;var n=r(413);t.Button=n.Button;var i=r(414);t.CheckboxButtonGroup=i.CheckboxButtonGroup;var v=r(416);t.CheckboxGroup=v.CheckboxGroup;var p=r(418);t.ColorPicker=p.ColorPicker;var c=r(419);t.DatePicker=c.DatePicker;var l=r(422);t.DateRangeSlider=l.DateRangeSlider;var d=r(428);t.DateSlider=d.DateSlider;var I=r(429);t.Div=I.Div;var g=r(433);t.Dropdown=g.Dropdown;var S=r(434);t.FileInput=S.FileInput;var P=r(410);t.InputWidget=P.InputWidget;var k=r(430);t.Markup=k.Markup;var x=r(435);t.MultiSelect=x.MultiSelect;var D=r(436);t.Paragraph=D.Paragraph;var b=r(437);t.PasswordInput=b.PasswordInput;var s=r(438);t.MultiChoice=s.MultiChoice;var h=r(441);t.NumericInput=h.NumericInput;var A=r(444);t.PreText=A.PreText;var B=r(445);t.RadioButtonGroup=B.RadioButtonGroup;var C=r(446);t.RadioGroup=C.RadioGroup;var G=r(447);t.RangeSlider=G.RangeSlider;var R=r(448);t.Select=R.Select;var T=r(449);t.Slider=T.Slider;var M=r(450);t.Spinner=M.Spinner;var m=r(409);t.TextInput=m.TextInput;var w=r(451);t.TextAreaInput=w.TextAreaInput;var W=r(452);t.Toggle=W.Toggle;var _=r(472);t.Widget=_.Widget},\n 404: function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const i=t(1),s=i.__importStar(t(18)),o=t(72),l=t(115),r=t(405),_=t(281),c=i.__importDefault(t(283));class u extends r.ControlView{*controls(){yield this.button_el}async lazy_initialize(){await super.lazy_initialize();const{icon:t}=this.model;null!=t&&(this.icon_view=await l.build_view(t,{parent:this}))}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.render())}remove(){null!=this.icon_view&&this.icon_view.remove(),super.remove()}styles(){return[...super.styles(),c.default]}_render_button(...t){return o.button({type:\"button\",disabled:this.model.disabled,class:[_.bk_btn,_.bk_btn_type(this.model.button_type)]},...t)}render(){super.render(),this.button_el=this._render_button(this.model.label),this.button_el.addEventListener(\"click\",()=>this.click()),null!=this.icon_view&&(o.prepend(this.button_el,this.icon_view.el,o.nbsp()),this.icon_view.render()),this.group_el=o.div({class:_.bk_btn_group},this.button_el),this.el.appendChild(this.group_el)}click(){}}n.AbstractButtonView=u,u.__name__=\"AbstractButtonView\";class a extends r.Control{constructor(t){super(t)}static init_AbstractButton(){this.define({label:[s.String,\"Button\"],icon:[s.Instance],button_type:[s.ButtonType,\"default\"]})}}n.AbstractButton=a,a.__name__=\"AbstractButton\",a.init_AbstractButton()},\n 405: function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const s=e(472),n=e(72);class i extends s.WidgetView{connect_signals(){super.connect_signals();const e=this.model.properties;this.on_change(e.disabled,()=>{for(const e of this.controls())n.toggle_attribute(e,\"disabled\",this.model.disabled)})}}o.ControlView=i,i.__name__=\"ControlView\";class l extends s.Widget{constructor(e){super(e)}}o.Control=l,l.__name__=\"Control\"},\n 472: function _(i,e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const o=i(1),n=i(276),r=o.__importStar(i(18));class _ extends n.HTMLBoxView{_width_policy(){return\"horizontal\"==this.model.orientation?super._width_policy():\"fixed\"}_height_policy(){return\"horizontal\"==this.model.orientation?\"fixed\":super._height_policy()}box_sizing(){const i=super.box_sizing();return\"horizontal\"==this.model.orientation?null==i.width&&(i.width=this.model.default_size):null==i.height&&(i.height=this.model.default_size),i}}t.WidgetView=_,_.__name__=\"WidgetView\";class s extends n.HTMLBox{constructor(i){super(i)}static init_Widget(){this.define({orientation:[r.Orientation,\"horizontal\"],default_size:[r.Number,300]}),this.override({margin:[5,5,5,5]})}}t.Widget=s,s.__name__=\"Widget\",s.init_Widget()},\n 407: function _(e,t,c){Object.defineProperty(c,\"__esModule\",{value:!0});const s=e(81),n=e(78);class o extends n.DOMView{}c.AbstractIconView=o,o.__name__=\"AbstractIconView\";class _ extends s.Model{constructor(e){super(e)}}c.AbstractIcon=_,_.__name__=\"AbstractIcon\"},\n 408: function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const i=e(1),s=e(409),h=e(72),_=i.__importStar(e(18)),o=e(10),u=e(173),r=e(282),c=i.__importDefault(e(284));class l extends s.TextInputView{constructor(){super(...arguments),this._open=!1,this._last_value=\"\",this._hover_index=0}styles(){return[...super.styles(),c.default]}render(){super.render(),this.input_el.addEventListener(\"keydown\",e=>this._keydown(e)),this.input_el.addEventListener(\"keyup\",e=>this._keyup(e)),this.menu=h.div({class:[r.bk_menu,u.bk_below]}),this.menu.addEventListener(\"click\",e=>this._menu_click(e)),this.menu.addEventListener(\"mouseover\",e=>this._menu_hover(e)),this.el.appendChild(this.menu),h.undisplay(this.menu)}change_input(){this._open&&this.menu.children.length>0&&(this.model.value=this.menu.children[this._hover_index].textContent,this.input_el.focus(),this._hide_menu())}_update_completions(e){h.empty(this.menu);for(const t of e){const e=h.div({},t);this.menu.appendChild(e)}e.length>0&&this.menu.children[0].classList.add(u.bk_active)}_show_menu(){if(!this._open){this._open=!0,this._hover_index=0,this._last_value=this.model.value,h.display(this.menu);const e=t=>{const{target:n}=t;n instanceof HTMLElement&&!this.el.contains(n)&&(document.removeEventListener(\"click\",e),this._hide_menu())};document.addEventListener(\"click\",e)}}_hide_menu(){this._open&&(this._open=!1,h.undisplay(this.menu))}_menu_click(e){e.target!=e.currentTarget&&e.target instanceof Element&&(this.model.value=e.target.textContent,this.input_el.focus(),this._hide_menu())}_menu_hover(e){if(e.target!=e.currentTarget&&e.target instanceof Element){let t=0;for(t=0;t0&&(this.menu.children[this._hover_index].classList.remove(u.bk_active),this._hover_index=o.clamp(e,0,t-1),this.menu.children[this._hover_index].classList.add(u.bk_active))}_keydown(e){}_keyup(e){switch(e.keyCode){case h.Keys.Enter:this.change_input();break;case h.Keys.Esc:this._hide_menu();break;case h.Keys.Up:this._bump_hover(this._hover_index-1);break;case h.Keys.Down:this._bump_hover(this._hover_index+1);break;default:{const e=this.input_el.value;if(e.lengthe:e=>e.toLowerCase();for(const n of this.model.completions)i(n).startsWith(i(e))&&t.push(n);this._update_completions(t),0==t.length?this._hide_menu():this._show_menu()}}}}n.AutocompleteInputView=l,l.__name__=\"AutocompleteInputView\";class a extends s.TextInput{constructor(e){super(e)}static init_AutocompleteInput(){this.prototype.default_view=l,this.define({completions:[_.Array,[]],min_characters:[_.Int,2],case_sensitive:[_.Boolean,!0]})}}n.AutocompleteInput=a,a.__name__=\"AutocompleteInput\",a.init_AutocompleteInput()},\n 409: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=e(410),l=e(72),p=n.__importStar(e(18)),u=e(412);class a extends s.InputWidgetView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.name.change,()=>this.input_el.name=this.model.name||\"\"),this.connect(this.model.properties.value.change,()=>this.input_el.value=this.model.value),this.connect(this.model.properties.value_input.change,()=>this.input_el.value=this.model.value_input),this.connect(this.model.properties.disabled.change,()=>this.input_el.disabled=this.model.disabled),this.connect(this.model.properties.placeholder.change,()=>this.input_el.placeholder=this.model.placeholder)}render(){super.render(),this.input_el=l.input({type:\"text\",class:u.bk_input,name:this.model.name,value:this.model.value,disabled:this.model.disabled,placeholder:this.model.placeholder}),this.input_el.addEventListener(\"change\",()=>this.change_input()),this.input_el.addEventListener(\"input\",()=>this.change_input_oninput()),this.group_el.appendChild(this.input_el)}change_input(){this.model.value=this.input_el.value,super.change_input()}change_input_oninput(){this.model.value_input=this.input_el.value,super.change_input()}}i.TextInputView=a,a.__name__=\"TextInputView\";class h extends s.InputWidget{constructor(e){super(e)}static init_TextInput(){this.prototype.default_view=a,this.define({value:[p.String,\"\"],value_input:[p.String,\"\"],placeholder:[p.String,\"\"]})}}i.TextInput=h,h.__name__=\"TextInput\",h.init_TextInput()},\n 410: function _(t,e,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=t(1),l=t(405),s=t(72),_=n.__importStar(t(18)),o=n.__importDefault(t(411)),r=t(412);class p extends l.ControlView{*controls(){yield this.input_el}connect_signals(){super.connect_signals(),this.connect(this.model.properties.title.change,()=>{this.label_el.textContent=this.model.title})}styles(){return[...super.styles(),o.default]}render(){super.render();const{title:t}=this.model;this.label_el=s.label({style:{display:0==t.length?\"none\":\"\"}},t),this.group_el=s.div({class:r.bk_input_group},this.label_el),this.el.appendChild(this.group_el)}change_input(){}}i.InputWidgetView=p,p.__name__=\"InputWidgetView\";class u extends l.Control{constructor(t){super(t)}static init_InputWidget(){this.define({title:[_.String,\"\"]})}}i.InputWidget=u,u.__name__=\"InputWidget\",u.init_InputWidget()},\n 411: function _(n,o,t){Object.defineProperty(t,\"__esModule\",{value:!0});t.default='\\n.bk-root .bk-input {\\n display: inline-block;\\n width: 100%;\\n flex-grow: 1;\\n -webkit-flex-grow: 1;\\n min-height: 31px;\\n padding: 0 12px;\\n background-color: #fff;\\n border: 1px solid #ccc;\\n border-radius: 4px;\\n}\\n.bk-root .bk-input:focus {\\n border-color: #66afe9;\\n outline: 0;\\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);\\n}\\n.bk-root .bk-input::placeholder,\\n.bk-root .bk-input:-ms-input-placeholder,\\n.bk-root .bk-input::-moz-placeholder,\\n.bk-root .bk-input::-webkit-input-placeholder {\\n color: #999;\\n opacity: 1;\\n}\\n.bk-root .bk-input[disabled] {\\n cursor: not-allowed;\\n background-color: #eee;\\n opacity: 1;\\n}\\n.bk-root select:not([multiple]).bk-input,\\n.bk-root select:not([size]).bk-input {\\n height: auto;\\n appearance: none;\\n -webkit-appearance: none;\\n background-image: url(\\'data:image/svg+xml;utf8,\\');\\n background-position: right 0.5em center;\\n background-size: 8px 6px;\\n background-repeat: no-repeat;\\n}\\n.bk-root select[multiple].bk-input,\\n.bk-root select[size].bk-input,\\n.bk-root textarea.bk-input {\\n height: auto;\\n}\\n.bk-root .bk-input-group {\\n width: 100%;\\n height: 100%;\\n display: inline-flex;\\n display: -webkit-inline-flex;\\n flex-wrap: nowrap;\\n -webkit-flex-wrap: nowrap;\\n align-items: start;\\n -webkit-align-items: start;\\n flex-direction: column;\\n -webkit-flex-direction: column;\\n white-space: nowrap;\\n}\\n.bk-root .bk-input-group.bk-inline {\\n flex-direction: row;\\n -webkit-flex-direction: row;\\n}\\n.bk-root .bk-input-group.bk-inline > *:not(:first-child) {\\n margin-left: 5px;\\n}\\n.bk-root .bk-input-group input[type=\"checkbox\"] + span,\\n.bk-root .bk-input-group input[type=\"radio\"] + span {\\n position: relative;\\n top: -2px;\\n margin-left: 3px;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper {\\n display: inherit;\\n width: inherit;\\n height: inherit;\\n position: relative;\\n overflow: hidden;\\n padding: 0;\\n vertical-align: middle;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper input {\\n padding-right: 20px;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn {\\n position: absolute;\\n display: block;\\n height: 50%;\\n min-height: 0;\\n min-width: 0;\\n width: 30px;\\n padding: 0;\\n margin: 0;\\n right: 0;\\n border: none;\\n background: none;\\n cursor: pointer;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn:before {\\n content: \"\";\\n display: inline-block;\\n transform: translateY(-50%);\\n border-left: 5px solid transparent;\\n border-right: 5px solid transparent;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-up {\\n top: 0;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-up:before {\\n border-bottom: 5px solid black;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-up:disabled:before {\\n border-bottom-color: grey;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-down {\\n bottom: 0;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-down:before {\\n border-top: 5px solid black;\\n}\\n.bk-root .bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-down:disabled:before {\\n border-top-color: grey;\\n}\\n'},\n 412: function _(u,e,n){Object.defineProperty(n,\"__esModule\",{value:!0}),n.bk_input=\"bk-input\",n.bk_input_group=\"bk-input-group\"},\n 413: function _(t,e,n){Object.defineProperty(n,\"__esModule\",{value:!0});const o=t(404),i=t(313);class s extends o.AbstractButtonView{click(){this.model.trigger_event(new i.ButtonClick),super.click()}}n.ButtonView=s,s.__name__=\"ButtonView\";class u extends o.AbstractButton{constructor(t){super(t)}static init_Button(){this.prototype.default_view=s,this.override({label:\"Button\"})}}n.Button=u,u.__name__=\"Button\",u.init_Button()},\n 414: function _(t,e,o){Object.defineProperty(o,\"__esModule\",{value:!0});const i=t(1),c=t(415),s=t(72),n=i.__importStar(t(18)),a=t(173);class u extends c.ButtonGroupView{get active(){return new Set(this.model.active)}change_active(t){const{active:e}=this;e.has(t)?e.delete(t):e.add(t),this.model.active=[...e].sort()}_update_active(){const{active:t}=this;this._buttons.forEach((e,o)=>{s.classes(e).toggle(a.bk_active,t.has(o))})}}o.CheckboxButtonGroupView=u,u.__name__=\"CheckboxButtonGroupView\";class r extends c.ButtonGroup{constructor(t){super(t)}static init_CheckboxButtonGroup(){this.prototype.default_view=u,this.define({active:[n.Array,[]]})}}o.CheckboxButtonGroup=r,r.__name__=\"CheckboxButtonGroup\",r.init_CheckboxButtonGroup()},\n 415: function _(t,e,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=t(1),o=t(405),i=t(72),r=n.__importStar(t(18)),_=t(281),u=n.__importDefault(t(283));class a extends o.ControlView{*controls(){yield*this._buttons}connect_signals(){super.connect_signals();const t=this.model.properties;this.on_change(t.button_type,()=>this.render()),this.on_change(t.labels,()=>this.render()),this.on_change(t.active,()=>this._update_active())}styles(){return[...super.styles(),u.default]}render(){super.render(),this._buttons=this.model.labels.map((t,e)=>{const s=i.div({class:[_.bk_btn,_.bk_btn_type(this.model.button_type)],disabled:this.model.disabled},t);return s.addEventListener(\"click\",()=>this.change_active(e)),s}),this._update_active();const t=i.div({class:_.bk_btn_group},this._buttons);this.el.appendChild(t)}}s.ButtonGroupView=a,a.__name__=\"ButtonGroupView\";class l extends o.Control{constructor(t){super(t)}static init_ButtonGroup(){this.define({labels:[r.Array,[]],button_type:[r.ButtonType,\"default\"]})}}s.ButtonGroup=l,l.__name__=\"ButtonGroup\",l.init_ButtonGroup()},\n 416: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=e(417),o=e(72),c=e(9),a=n.__importStar(e(18)),l=e(173),d=e(412);class r extends s.InputGroupView{render(){super.render();const e=o.div({class:[d.bk_input_group,this.model.inline?l.bk_inline:null]});this.el.appendChild(e);const{active:t,labels:i}=this.model;this._inputs=[];for(let n=0;nthis.change_active(n)),this._inputs.push(s),this.model.disabled&&(s.disabled=!0),c.includes(t,n)&&(s.checked=!0);const a=o.label({},s,o.span({},i[n]));e.appendChild(a)}}change_active(e){const t=new Set(this.model.active);t.has(e)?t.delete(e):t.add(e),this.model.active=[...t].sort()}}i.CheckboxGroupView=r,r.__name__=\"CheckboxGroupView\";class p extends s.InputGroup{constructor(e){super(e)}static init_CheckboxGroup(){this.prototype.default_view=r,this.define({active:[a.Array,[]],labels:[a.Array,[]],inline:[a.Boolean,!1]})}}i.CheckboxGroup=p,p.__name__=\"CheckboxGroup\",p.init_CheckboxGroup()},\n 417: function _(e,t,n){Object.defineProperty(n,\"__esModule\",{value:!0});const s=e(1),o=e(405),r=s.__importDefault(e(411));class u extends o.ControlView{*controls(){yield*this._inputs}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.render())}styles(){return[...super.styles(),r.default]}}n.InputGroupView=u,u.__name__=\"InputGroupView\";class _ extends o.Control{constructor(e){super(e)}}n.InputGroup=_,_.__name__=\"InputGroup\"},\n 418: function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1),o=e(410),s=e(72),l=n.__importStar(e(18)),r=e(412);class c extends o.InputWidgetView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.name.change,()=>this.input_el.name=this.model.name||\"\"),this.connect(this.model.properties.color.change,()=>this.input_el.value=this.model.color),this.connect(this.model.properties.disabled.change,()=>this.input_el.disabled=this.model.disabled)}render(){super.render(),this.input_el=s.input({type:\"color\",class:r.bk_input,name:this.model.name,value:this.model.color,disabled:this.model.disabled}),this.input_el.addEventListener(\"change\",()=>this.change_input()),this.group_el.appendChild(this.input_el)}change_input(){this.model.color=this.input_el.value,super.change_input()}}t.ColorPickerView=c,c.__name__=\"ColorPickerView\";class d extends o.InputWidget{constructor(e){super(e)}static init_ColorPicker(){this.prototype.default_view=c,this.define({color:[l.Color,\"#000000\"]})}}t.ColorPicker=d,d.__name__=\"ColorPicker\",d.init_ColorPicker()},\n 419: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=n.__importDefault(e(420)),a=e(410),l=e(72),o=n.__importStar(e(18)),r=e(8),d=e(412),c=n.__importDefault(e(421));function u(e){const t=[];for(const i of e)if(r.isString(i))t.push(i);else{const[e,n]=i;t.push({from:e,to:n})}return t}class _ extends a.InputWidgetView{connect_signals(){super.connect_signals();const{value:e,min_date:t,max_date:i,disabled_dates:n,enabled_dates:s,position:a,inline:l}=this.model.properties;this.connect(e.change,()=>{var t;return null===(t=this._picker)||void 0===t?void 0:t.setDate(e.value())}),this.connect(t.change,()=>{var e;return null===(e=this._picker)||void 0===e?void 0:e.set(\"minDate\",t.value())}),this.connect(i.change,()=>{var e;return null===(e=this._picker)||void 0===e?void 0:e.set(\"maxDate\",i.value())}),this.connect(n.change,()=>{var e;return null===(e=this._picker)||void 0===e?void 0:e.set(\"disable\",n.value())}),this.connect(s.change,()=>{var e;return null===(e=this._picker)||void 0===e?void 0:e.set(\"enable\",s.value())}),this.connect(a.change,()=>{var e;return null===(e=this._picker)||void 0===e?void 0:e.set(\"position\",a.value())}),this.connect(l.change,()=>{var e;return null===(e=this._picker)||void 0===e?void 0:e.set(\"inline\",l.value())})}remove(){var e;null===(e=this._picker)||void 0===e||e.destroy(),super.remove()}styles(){return[...super.styles(),c.default]}render(){null==this._picker&&(super.render(),this.input_el=l.input({type:\"text\",class:d.bk_input,disabled:this.model.disabled}),this.group_el.appendChild(this.input_el),this._picker=s.default(this.input_el,{defaultDate:this.model.value,minDate:this.model.min_date,maxDate:this.model.max_date,inline:this.model.inline,position:this.model.position,disable:u(this.model.disabled_dates),enable:u(this.model.enabled_dates),onChange:(e,t,i)=>this._on_change(e,t,i)}))}_on_change(e,t,i){this.model.value=t,this.change_input()}}i.DatePickerView=_,_.__name__=\"DatePickerView\";class h extends a.InputWidget{constructor(e){super(e)}static init_DatePicker(){this.prototype.default_view=_,this.define({value:[o.Any],min_date:[o.Any],max_date:[o.Any],disabled_dates:[o.Any,[]],enabled_dates:[o.Any,[]],position:[o.CalendarPosition,\"auto\"],inline:[o.Boolean,!1]})}}i.DatePicker=h,h.__name__=\"DatePicker\",h.init_DatePicker()},\n 420: function _(e,t,n){\n /* flatpickr v4.6.3, @license MIT */var a,i;a=this,i=function(){\"use strict\";\n /*! *****************************************************************************\n Copyright (c) Microsoft Corporation. All rights reserved.\n Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\n this file except in compliance with the License. You may obtain a copy of the\n License at http://www.apache.org/licenses/LICENSE-2.0\n \n THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\n WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\n MERCHANTABLITY OR NON-INFRINGEMENT.\n \n See the Apache Version 2.0 License for specific language governing permissions\n and limitations under the License.\n ***************************************************************************** */var e=function(){return(e=Object.assign||function(e){for(var t,n=1,a=arguments.length;n\",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:\"auto\",positionElement:void 0,prevArrow:\"\",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},a={weekdays:{shorthand:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],longhand:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"]},months:{shorthand:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],longhand:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(e){var t=e%100;if(t>3&&t<21)return\"th\";switch(t%10){case 1:return\"st\";case 2:return\"nd\";case 3:return\"rd\";default:return\"th\"}},rangeSeparator:\" to \",weekAbbreviation:\"Wk\",scrollTitle:\"Scroll to increment\",toggleTitle:\"Click to toggle\",amPM:[\"AM\",\"PM\"],yearAriaLabel:\"Year\",hourAriaLabel:\"Hour\",minuteAriaLabel:\"Minute\",time_24hr:!1},i=function(e){return(\"0\"+e).slice(-2)},o=function(e){return!0===e?1:0};function r(e,t,n){var a;return void 0===n&&(n=!1),function(){var i=this,o=arguments;null!==a&&clearTimeout(a),a=window.setTimeout((function(){a=null,n||e.apply(i,o)}),t),n&&!a&&e.apply(i,o)}}var l=function(e){return e instanceof Array?e:[e]};function c(e,t,n){if(!0===n)return e.classList.add(t);e.classList.remove(t)}function d(e,t,n){var a=window.document.createElement(e);return t=t||\"\",n=n||\"\",a.className=t,void 0!==n&&(a.textContent=n),a}function s(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function u(e,t){var n=d(\"div\",\"numInputWrapper\"),a=d(\"input\",\"numInput \"+e),i=d(\"span\",\"arrowUp\"),o=d(\"span\",\"arrowDown\");if(-1===navigator.userAgent.indexOf(\"MSIE 9.0\")?a.type=\"number\":(a.type=\"text\",a.pattern=\"\\\\d*\"),void 0!==t)for(var r in t)a.setAttribute(r,t[r]);return n.appendChild(a),n.appendChild(i),n.appendChild(o),n}var f=function(){},m=function(e,t,n){return n.months[t?\"shorthand\":\"longhand\"][e]},g={D:f,F:function(e,t,n){e.setMonth(n.months.longhand.indexOf(t))},G:function(e,t){e.setHours(parseFloat(t))},H:function(e,t){e.setHours(parseFloat(t))},J:function(e,t){e.setDate(parseFloat(t))},K:function(e,t,n){e.setHours(e.getHours()%12+12*o(new RegExp(n.amPM[1],\"i\").test(t)))},M:function(e,t,n){e.setMonth(n.months.shorthand.indexOf(t))},S:function(e,t){e.setSeconds(parseFloat(t))},U:function(e,t){return new Date(1e3*parseFloat(t))},W:function(e,t,n){var a=parseInt(t),i=new Date(e.getFullYear(),0,2+7*(a-1),0,0,0,0);return i.setDate(i.getDate()-i.getDay()+n.firstDayOfWeek),i},Y:function(e,t){e.setFullYear(parseFloat(t))},Z:function(e,t){return new Date(t)},d:function(e,t){e.setDate(parseFloat(t))},h:function(e,t){e.setHours(parseFloat(t))},i:function(e,t){e.setMinutes(parseFloat(t))},j:function(e,t){e.setDate(parseFloat(t))},l:f,m:function(e,t){e.setMonth(parseFloat(t)-1)},n:function(e,t){e.setMonth(parseFloat(t)-1)},s:function(e,t){e.setSeconds(parseFloat(t))},u:function(e,t){return new Date(parseFloat(t))},w:f,y:function(e,t){e.setFullYear(2e3+parseFloat(t))}},p={D:\"(\\\\w+)\",F:\"(\\\\w+)\",G:\"(\\\\d\\\\d|\\\\d)\",H:\"(\\\\d\\\\d|\\\\d)\",J:\"(\\\\d\\\\d|\\\\d)\\\\w+\",K:\"\",M:\"(\\\\w+)\",S:\"(\\\\d\\\\d|\\\\d)\",U:\"(.+)\",W:\"(\\\\d\\\\d|\\\\d)\",Y:\"(\\\\d{4})\",Z:\"(.+)\",d:\"(\\\\d\\\\d|\\\\d)\",h:\"(\\\\d\\\\d|\\\\d)\",i:\"(\\\\d\\\\d|\\\\d)\",j:\"(\\\\d\\\\d|\\\\d)\",l:\"(\\\\w+)\",m:\"(\\\\d\\\\d|\\\\d)\",n:\"(\\\\d\\\\d|\\\\d)\",s:\"(\\\\d\\\\d|\\\\d)\",u:\"(.+)\",w:\"(\\\\d\\\\d|\\\\d)\",y:\"(\\\\d{2})\"},h={Z:function(e){return e.toISOString()},D:function(e,t,n){return t.weekdays.shorthand[h.w(e,t,n)]},F:function(e,t,n){return m(h.n(e,t,n)-1,!1,t)},G:function(e,t,n){return i(h.h(e,t,n))},H:function(e){return i(e.getHours())},J:function(e,t){return void 0!==t.ordinal?e.getDate()+t.ordinal(e.getDate()):e.getDate()},K:function(e,t){return t.amPM[o(e.getHours()>11)]},M:function(e,t){return m(e.getMonth(),!0,t)},S:function(e){return i(e.getSeconds())},U:function(e){return e.getTime()/1e3},W:function(e,t,n){return n.getWeek(e)},Y:function(e){return e.getFullYear()},d:function(e){return i(e.getDate())},h:function(e){return e.getHours()%12?e.getHours()%12:12},i:function(e){return i(e.getMinutes())},j:function(e){return e.getDate()},l:function(e,t){return t.weekdays.longhand[e.getDay()]},m:function(e){return i(e.getMonth()+1)},n:function(e){return e.getMonth()+1},s:function(e){return e.getSeconds()},u:function(e){return e.getTime()},w:function(e){return e.getDay()},y:function(e){return String(e.getFullYear()).substring(2)}},v=function(e){var t=e.config,i=void 0===t?n:t,o=e.l10n,r=void 0===o?a:o;return function(e,t,n){var a=n||r;return void 0!==i.formatDate?i.formatDate(e,t,a):t.split(\"\").map((function(t,n,o){return h[t]&&\"\\\\\"!==o[n-1]?h[t](e,a,i):\"\\\\\"!==t?t:\"\"})).join(\"\")}},D=function(e){var t=e.config,i=void 0===t?n:t,o=e.l10n,r=void 0===o?a:o;return function(e,t,a,o){if(0===e||e){var l,c=o||r,d=e;if(e instanceof Date)l=new Date(e.getTime());else if(\"string\"!=typeof e&&void 0!==e.toFixed)l=new Date(e);else if(\"string\"==typeof e){var s=t||(i||n).dateFormat,u=String(e).trim();if(\"today\"===u)l=new Date,a=!0;else if(/Z$/.test(u)||/GMT$/.test(u))l=new Date(e);else if(i&&i.parseDate)l=i.parseDate(e,s);else{l=i&&i.noCalendar?new Date((new Date).setHours(0,0,0,0)):new Date((new Date).getFullYear(),0,1,0,0,0,0);for(var f=void 0,m=[],h=0,v=0,D=\"\";hr&&(s=n===h.hourElement?s-r-o(!h.amPM):a,f&&Y(void 0,1,h.hourElement)),h.amPM&&u&&(1===l?s+c===23:Math.abs(s-c)>l)&&(h.amPM.textContent=h.l10n.amPM[o(h.amPM.textContent===h.l10n.amPM[0])]),n.value=i(s)}}(e);var t=h._input.value;E(),ve(),h._input.value!==t&&h._debouncedChange()}function E(){if(void 0!==h.hourElement&&void 0!==h.minuteElement){var e,t,n=(parseInt(h.hourElement.value.slice(-2),10)||0)%24,a=(parseInt(h.minuteElement.value,10)||0)%60,i=void 0!==h.secondElement?(parseInt(h.secondElement.value,10)||0)%60:0;void 0!==h.amPM&&(e=n,t=h.amPM.textContent,n=e%12+12*o(t===h.l10n.amPM[1]));var r=void 0!==h.config.minTime||h.config.minDate&&h.minDateHasTime&&h.latestSelectedDateObj&&0===w(h.latestSelectedDateObj,h.config.minDate,!0);if(void 0!==h.config.maxTime||h.config.maxDate&&h.maxDateHasTime&&h.latestSelectedDateObj&&0===w(h.latestSelectedDateObj,h.config.maxDate,!0)){var l=void 0!==h.config.maxTime?h.config.maxTime:h.config.maxDate;(n=Math.min(n,l.getHours()))===l.getHours()&&(a=Math.min(a,l.getMinutes())),a===l.getMinutes()&&(i=Math.min(i,l.getSeconds()))}if(r){var c=void 0!==h.config.minTime?h.config.minTime:h.config.minDate;(n=Math.max(n,c.getHours()))===c.getHours()&&(a=Math.max(a,c.getMinutes())),a===c.getMinutes()&&(i=Math.max(i,c.getSeconds()))}I(n,a,i)}}function T(e){var t=e||h.latestSelectedDateObj;t&&I(t.getHours(),t.getMinutes(),t.getSeconds())}function k(){var e=h.config.defaultHour,t=h.config.defaultMinute,n=h.config.defaultSeconds;if(void 0!==h.config.minDate){var a=h.config.minDate.getHours(),i=h.config.minDate.getMinutes();(e=Math.max(e,a))===a&&(t=Math.max(i,t)),e===a&&t===i&&(n=h.config.minDate.getSeconds())}if(void 0!==h.config.maxDate){var o=h.config.maxDate.getHours(),r=h.config.maxDate.getMinutes();(e=Math.min(e,o))===o&&(t=Math.min(r,t)),e===o&&t===r&&(n=h.config.maxDate.getSeconds())}I(e,t,n)}function I(e,t,n){void 0!==h.latestSelectedDateObj&&h.latestSelectedDateObj.setHours(e%24,t,n||0,0),h.hourElement&&h.minuteElement&&!h.isMobile&&(h.hourElement.value=i(h.config.time_24hr?e:(12+e)%12+12*o(e%12==0)),h.minuteElement.value=i(t),void 0!==h.amPM&&(h.amPM.textContent=h.l10n.amPM[o(e>=12)]),void 0!==h.secondElement&&(h.secondElement.value=i(n)))}function S(e){var t=parseInt(e.target.value)+(e.delta||0);(t/1e3>1||\"Enter\"===e.key&&!/[^\\d]/.test(t.toString()))&&V(t)}function O(e,t,n,a){return t instanceof Array?t.forEach((function(t){return O(e,t,n,a)})):e instanceof Array?e.forEach((function(e){return O(e,t,n,a)})):(e.addEventListener(t,n,a),void h._handlers.push({element:e,event:t,handler:n,options:a}))}function _(e){return function(t){1===t.which&&e(t)}}function F(){fe(\"onChange\")}function N(e,t){var n=void 0!==e?h.parseDate(e):h.latestSelectedDateObj||(h.config.minDate&&h.config.minDate>h.now?h.config.minDate:h.config.maxDate&&h.config.maxDate=0&&w(e,h.selectedDates[1])<=0}(t)&&!ge(t)&&o.classList.add(\"inRange\"),h.weekNumbers&&1===h.config.showMonths&&\"prevMonthDay\"!==e&&n%7==1&&h.weekNumbers.insertAdjacentHTML(\"beforeend\",\"\"+h.config.getWeek(t)+\"\"),fe(\"onDayCreate\",o),o}function j(e){e.focus(),\"range\"===h.config.mode&&ee(e)}function H(e){for(var t=e>0?0:h.config.showMonths-1,n=e>0?h.config.showMonths:-1,a=t;a!=n;a+=e)for(var i=h.daysContainer.children[a],o=e>0?0:i.children.length-1,r=e>0?i.children.length:-1,l=o;l!=r;l+=e){var c=i.children[l];if(-1===c.className.indexOf(\"hidden\")&&Z(c.dateObj))return c}}function L(e,t){var n=Q(document.activeElement||document.body),a=void 0!==e?e:n?document.activeElement:void 0!==h.selectedDateElem&&Q(h.selectedDateElem)?h.selectedDateElem:void 0!==h.todayDateElem&&Q(h.todayDateElem)?h.todayDateElem:H(t>0?1:-1);return void 0===a?h._input.focus():n?void function(e,t){for(var n=-1===e.className.indexOf(\"Month\")?e.dateObj.getMonth():h.currentMonth,a=t>0?h.config.showMonths:-1,i=t>0?1:-1,o=n-h.currentMonth;o!=a;o+=i)for(var r=h.daysContainer.children[o],l=n-h.currentMonth===o?e.$i+t:t<0?r.children.length-1:0,c=r.children.length,d=l;d>=0&&d0?c:-1);d+=i){var s=r.children[d];if(-1===s.className.indexOf(\"hidden\")&&Z(s.dateObj)&&Math.abs(e.$i-d)>=Math.abs(t))return j(s)}h.changeMonth(i),L(H(i),0)}(a,t):j(a)}function W(e,t){for(var n=(new Date(e,t,1).getDay()-h.l10n.firstDayOfWeek+7)%7,a=h.utils.getDaysInMonth((t-1+12)%12),i=h.utils.getDaysInMonth(t),o=window.document.createDocumentFragment(),r=h.config.showMonths>1,l=r?\"prevMonthDay hidden\":\"prevMonthDay\",c=r?\"nextMonthDay hidden\":\"nextMonthDay\",s=a+1-n,u=0;s<=a;s++,u++)o.appendChild(A(l,new Date(e,t-1,s),s,u));for(s=1;s<=i;s++,u++)o.appendChild(A(\"\",new Date(e,t,s),s,u));for(var f=i+1;f<=42-n&&(1===h.config.showMonths||u%7!=0);f++,u++)o.appendChild(A(c,new Date(e,t+1,f%i),f,u));var m=d(\"div\",\"dayContainer\");return m.appendChild(o),m}function R(){if(void 0!==h.daysContainer){s(h.daysContainer),h.weekNumbers&&s(h.weekNumbers);for(var e=document.createDocumentFragment(),t=0;t1||\"dropdown\"!==h.config.monthSelectorType)){var e=function(e){return!(void 0!==h.config.minDate&&h.currentYear===h.config.minDate.getFullYear()&&eh.config.maxDate.getMonth())};h.monthsDropdownContainer.tabIndex=-1,h.monthsDropdownContainer.innerHTML=\"\";for(var t=0;t<12;t++)if(e(t)){var n=d(\"option\",\"flatpickr-monthDropdown-month\");n.value=new Date(h.currentYear,t).getMonth().toString(),n.textContent=m(t,h.config.shorthandCurrentMonth,h.l10n),n.tabIndex=-1,h.currentMonth===t&&(n.selected=!0),h.monthsDropdownContainer.appendChild(n)}}}function J(){var e,t=d(\"div\",\"flatpickr-month\"),n=window.document.createDocumentFragment();h.config.showMonths>1||\"static\"===h.config.monthSelectorType?e=d(\"span\",\"cur-month\"):(h.monthsDropdownContainer=d(\"select\",\"flatpickr-monthDropdown-months\"),O(h.monthsDropdownContainer,\"change\",(function(e){var t=e.target,n=parseInt(t.value,10);h.changeMonth(n-h.currentMonth),fe(\"onMonthChange\")})),B(),e=h.monthsDropdownContainer);var a=u(\"cur-year\",{tabindex:\"-1\"}),i=a.getElementsByTagName(\"input\")[0];i.setAttribute(\"aria-label\",h.l10n.yearAriaLabel),h.config.minDate&&i.setAttribute(\"min\",h.config.minDate.getFullYear().toString()),h.config.maxDate&&(i.setAttribute(\"max\",h.config.maxDate.getFullYear().toString()),i.disabled=!!h.config.minDate&&h.config.minDate.getFullYear()===h.config.maxDate.getFullYear());var o=d(\"div\",\"flatpickr-current-month\");return o.appendChild(e),o.appendChild(a),n.appendChild(o),t.appendChild(n),{container:t,yearElement:i,monthElement:e}}function K(){s(h.monthNav),h.monthNav.appendChild(h.prevMonthNav),h.config.showMonths&&(h.yearElements=[],h.monthElements=[]);for(var e=h.config.showMonths;e--;){var t=J();h.yearElements.push(t.yearElement),h.monthElements.push(t.monthElement),h.monthNav.appendChild(t.container)}h.monthNav.appendChild(h.nextMonthNav)}function U(){h.weekdayContainer?s(h.weekdayContainer):h.weekdayContainer=d(\"div\",\"flatpickr-weekdays\");for(var e=h.config.showMonths;e--;){var t=d(\"div\",\"flatpickr-weekdaycontainer\");h.weekdayContainer.appendChild(t)}return q(),h.weekdayContainer}function q(){if(h.weekdayContainer){var e=h.l10n.firstDayOfWeek,t=h.l10n.weekdays.shorthand.slice();e>0&&e\\n \"+t.join(\"\")+\"\\n \\n \"}}function $(e,t){void 0===t&&(t=!0);var n=t?e:e-h.currentMonth;n<0&&!0===h._hidePrevMonthArrow||n>0&&!0===h._hideNextMonthArrow||(h.currentMonth+=n,(h.currentMonth<0||h.currentMonth>11)&&(h.currentYear+=h.currentMonth>11?1:-1,h.currentMonth=(h.currentMonth+12)%12,fe(\"onYearChange\"),B()),R(),fe(\"onMonthChange\"),pe())}function z(e){return!(!h.config.appendTo||!h.config.appendTo.contains(e))||h.calendarContainer.contains(e)}function G(e){if(h.isOpen&&!h.config.inline){var t=\"function\"==typeof(r=e).composedPath?r.composedPath()[0]:r.target,n=z(t),a=t===h.input||t===h.altInput||h.element.contains(t)||e.path&&e.path.indexOf&&(~e.path.indexOf(h.input)||~e.path.indexOf(h.altInput)),i=\"blur\"===e.type?a&&e.relatedTarget&&!z(e.relatedTarget):!a&&!n&&!z(e.relatedTarget),o=!h.config.ignoredFocusElements.some((function(e){return e.contains(t)}));i&&o&&(void 0!==h.timeContainer&&void 0!==h.minuteElement&&void 0!==h.hourElement&&x(),h.close(),\"range\"===h.config.mode&&1===h.selectedDates.length&&(h.clear(!1),h.redraw()))}var r}function V(e){if(!(!e||h.config.minDate&&eh.config.maxDate.getFullYear())){var t=e,n=h.currentYear!==t;h.currentYear=t||h.currentYear,h.config.maxDate&&h.currentYear===h.config.maxDate.getFullYear()?h.currentMonth=Math.min(h.config.maxDate.getMonth(),h.currentMonth):h.config.minDate&&h.currentYear===h.config.minDate.getFullYear()&&(h.currentMonth=Math.max(h.config.minDate.getMonth(),h.currentMonth)),n&&(h.redraw(),fe(\"onYearChange\"),B())}}function Z(e,t){void 0===t&&(t=!0);var n=h.parseDate(e,void 0,t);if(h.config.minDate&&n&&w(n,h.config.minDate,void 0!==t?t:!h.minDateHasTime)<0||h.config.maxDate&&n&&w(n,h.config.maxDate,void 0!==t?t:!h.maxDateHasTime)>0)return!1;if(0===h.config.enable.length&&0===h.config.disable.length)return!0;if(void 0===n)return!1;for(var a=h.config.enable.length>0,i=a?h.config.enable:h.config.disable,o=0,r=void 0;o=r.from.getTime()&&n.getTime()<=r.to.getTime())return a}return!a}function Q(e){return void 0!==h.daysContainer&&-1===e.className.indexOf(\"hidden\")&&h.daysContainer.contains(e)}function X(e){var t=e.target===h._input,n=h.config.allowInput,a=h.isOpen&&(!n||!t),i=h.config.inline&&t&&!n;if(13===e.keyCode&&t){if(n)return h.setDate(h._input.value,!0,e.target===h.altInput?h.config.altFormat:h.config.dateFormat),e.target.blur();h.open()}else if(z(e.target)||a||i){var o=!!h.timeContainer&&h.timeContainer.contains(e.target);switch(e.keyCode){case 13:o?(e.preventDefault(),x(),le()):ce(e);break;case 27:e.preventDefault(),le();break;case 8:case 46:t&&!h.config.allowInput&&(e.preventDefault(),h.clear());break;case 37:case 39:if(o||t)h.hourElement&&h.hourElement.focus();else if(e.preventDefault(),void 0!==h.daysContainer&&(!1===n||document.activeElement&&Q(document.activeElement))){var r=39===e.keyCode?1:-1;e.ctrlKey?(e.stopPropagation(),$(r),L(H(1),0)):L(void 0,r)}break;case 38:case 40:e.preventDefault();var l=40===e.keyCode?1:-1;h.daysContainer&&void 0!==e.target.$i||e.target===h.input||e.target===h.altInput?e.ctrlKey?(e.stopPropagation(),V(h.currentYear-l),L(H(1),0)):o||L(void 0,7*l):e.target===h.currentYearElement?V(h.currentYear-l):h.config.enableTime&&(!o&&h.hourElement&&h.hourElement.focus(),x(e),h._debouncedChange());break;case 9:if(o){var c=[h.hourElement,h.minuteElement,h.secondElement,h.amPM].concat(h.pluginElements).filter((function(e){return e})),d=c.indexOf(e.target);if(-1!==d){var s=c[d+(e.shiftKey?-1:1)];e.preventDefault(),(s||h._input).focus()}}else!h.config.noCalendar&&h.daysContainer&&h.daysContainer.contains(e.target)&&e.shiftKey&&(e.preventDefault(),h._input.focus())}}if(void 0!==h.amPM&&e.target===h.amPM)switch(e.key){case h.l10n.amPM[0].charAt(0):case h.l10n.amPM[0].charAt(0).toLowerCase():h.amPM.textContent=h.l10n.amPM[0],E(),ve();break;case h.l10n.amPM[1].charAt(0):case h.l10n.amPM[1].charAt(0).toLowerCase():h.amPM.textContent=h.l10n.amPM[1],E(),ve()}(t||z(e.target))&&fe(\"onKeyDown\",e)}function ee(e){if(1===h.selectedDates.length&&(!e||e.classList.contains(\"flatpickr-day\")&&!e.classList.contains(\"flatpickr-disabled\"))){for(var t=e?e.dateObj.getTime():h.days.firstElementChild.dateObj.getTime(),n=h.parseDate(h.selectedDates[0],void 0,!0).getTime(),a=Math.min(t,h.selectedDates[0].getTime()),i=Math.max(t,h.selectedDates[0].getTime()),o=!1,r=0,l=0,c=a;ca&&cr)?r=c:c>n&&(!l||c0&&m0&&m>l;return g?(f.classList.add(\"notAllowed\"),[\"inRange\",\"startRange\",\"endRange\"].forEach((function(e){f.classList.remove(e)})),\"continue\"):o&&!g?\"continue\":([\"startRange\",\"inRange\",\"endRange\",\"notAllowed\"].forEach((function(e){f.classList.remove(e)})),void(void 0!==e&&(e.classList.add(t<=h.selectedDates[0].getTime()?\"startRange\":\"endRange\"),nt&&m===n&&f.classList.add(\"endRange\"),m>=r&&(0===l||m<=l)&&(d=n,u=t,(c=m)>Math.min(d,u)&&c0||n.getMinutes()>0||n.getSeconds()>0),h.selectedDates&&(h.selectedDates=h.selectedDates.filter((function(e){return Z(e)})),h.selectedDates.length||\"min\"!==e||T(n),ve()),h.daysContainer&&(re(),void 0!==n?h.currentYearElement[e]=n.getFullYear().toString():h.currentYearElement.removeAttribute(e),h.currentYearElement.disabled=!!a&&void 0!==n&&a.getFullYear()===n.getFullYear())}}function ie(){\"object\"!=typeof h.config.locale&&void 0===y.l10ns[h.config.locale]&&h.config.errorHandler(new Error(\"flatpickr: invalid locale \"+h.config.locale)),h.l10n=e({},y.l10ns.default,\"object\"==typeof h.config.locale?h.config.locale:\"default\"!==h.config.locale?y.l10ns[h.config.locale]:void 0),p.K=\"(\"+h.l10n.amPM[0]+\"|\"+h.l10n.amPM[1]+\"|\"+h.l10n.amPM[0].toLowerCase()+\"|\"+h.l10n.amPM[1].toLowerCase()+\")\",void 0===e({},g,JSON.parse(JSON.stringify(f.dataset||{}))).time_24hr&&void 0===y.defaultConfig.time_24hr&&(h.config.time_24hr=h.l10n.time_24hr),h.formatDate=v(h),h.parseDate=D({config:h.config,l10n:h.l10n})}function oe(e){if(void 0!==h.calendarContainer){fe(\"onPreCalendarPosition\");var t=e||h._positionElement,n=Array.prototype.reduce.call(h.calendarContainer.children,(function(e,t){return e+t.offsetHeight}),0),a=h.calendarContainer.offsetWidth,i=h.config.position.split(\" \"),o=i[0],r=i.length>1?i[1]:null,l=t.getBoundingClientRect(),d=window.innerHeight-l.bottom,s=\"above\"===o||\"below\"!==o&&dn,u=window.pageYOffset+l.top+(s?-n-2:t.offsetHeight+2);if(c(h.calendarContainer,\"arrowTop\",!s),c(h.calendarContainer,\"arrowBottom\",s),!h.config.inline){var f=window.pageXOffset+l.left-(null!=r&&\"center\"===r?(a-l.width)/2:0),m=window.document.body.offsetWidth-(window.pageXOffset+l.right),g=f+a>window.document.body.offsetWidth,p=m+a>window.document.body.offsetWidth;if(c(h.calendarContainer,\"rightMost\",g),!h.config.static)if(h.calendarContainer.style.top=u+\"px\",g)if(p){var v=document.styleSheets[0];if(void 0===v)return;var D=window.document.body.offsetWidth,w=Math.max(0,D/2-a/2),b=v.cssRules.length,C=\"{left:\"+l.left+\"px;right:auto;}\";c(h.calendarContainer,\"rightMost\",!1),c(h.calendarContainer,\"centerMost\",!0),v.insertRule(\".flatpickr-calendar.centerMost:before,.flatpickr-calendar.centerMost:after\"+C,b),h.calendarContainer.style.left=w+\"px\",h.calendarContainer.style.right=\"auto\"}else h.calendarContainer.style.left=\"auto\",h.calendarContainer.style.right=m+\"px\";else h.calendarContainer.style.left=f+\"px\",h.calendarContainer.style.right=\"auto\"}}}function re(){h.config.noCalendar||h.isMobile||(pe(),R())}function le(){h._input.focus(),-1!==window.navigator.userAgent.indexOf(\"MSIE\")||void 0!==navigator.msMaxTouchPoints?setTimeout(h.close,0):h.close()}function ce(e){e.preventDefault(),e.stopPropagation();var t=function e(t,n){return n(t)?t:t.parentNode?e(t.parentNode,n):void 0}(e.target,(function(e){return e.classList&&e.classList.contains(\"flatpickr-day\")&&!e.classList.contains(\"flatpickr-disabled\")&&!e.classList.contains(\"notAllowed\")}));if(void 0!==t){var n=t,a=h.latestSelectedDateObj=new Date(n.dateObj.getTime()),i=(a.getMonth()h.currentMonth+h.config.showMonths-1)&&\"range\"!==h.config.mode;if(h.selectedDateElem=n,\"single\"===h.config.mode)h.selectedDates=[a];else if(\"multiple\"===h.config.mode){var o=ge(a);o?h.selectedDates.splice(parseInt(o),1):h.selectedDates.push(a)}else\"range\"===h.config.mode&&(2===h.selectedDates.length&&h.clear(!1,!1),h.latestSelectedDateObj=a,h.selectedDates.push(a),0!==w(a,h.selectedDates[0],!0)&&h.selectedDates.sort((function(e,t){return e.getTime()-t.getTime()})));if(E(),i){var r=h.currentYear!==a.getFullYear();h.currentYear=a.getFullYear(),h.currentMonth=a.getMonth(),r&&(fe(\"onYearChange\"),B()),fe(\"onMonthChange\")}if(pe(),R(),ve(),h.config.enableTime&&setTimeout((function(){return h.showTimeInput=!0}),50),i||\"range\"===h.config.mode||1!==h.config.showMonths?void 0!==h.selectedDateElem&&void 0===h.hourElement&&h.selectedDateElem&&h.selectedDateElem.focus():j(n),void 0!==h.hourElement&&void 0!==h.hourElement&&h.hourElement.focus(),h.config.closeOnSelect){var l=\"single\"===h.config.mode&&!h.config.enableTime,c=\"range\"===h.config.mode&&2===h.selectedDates.length&&!h.config.enableTime;(l||c)&&le()}F()}}h.parseDate=D({config:h.config,l10n:h.l10n}),h._handlers=[],h.pluginElements=[],h.loadedPlugins=[],h._bind=O,h._setHoursFromDate=T,h._positionCalendar=oe,h.changeMonth=$,h.changeYear=V,h.clear=function(e,t){void 0===e&&(e=!0),void 0===t&&(t=!0),h.input.value=\"\",void 0!==h.altInput&&(h.altInput.value=\"\"),void 0!==h.mobileInput&&(h.mobileInput.value=\"\"),h.selectedDates=[],h.latestSelectedDateObj=void 0,!0===t&&(h.currentYear=h._initialDate.getFullYear(),h.currentMonth=h._initialDate.getMonth()),h.showTimeInput=!1,!0===h.config.enableTime&&k(),h.redraw(),e&&fe(\"onChange\")},h.close=function(){h.isOpen=!1,h.isMobile||(void 0!==h.calendarContainer&&h.calendarContainer.classList.remove(\"open\"),void 0!==h._input&&h._input.classList.remove(\"active\")),fe(\"onClose\")},h._createElement=d,h.destroy=function(){void 0!==h.config&&fe(\"onDestroy\");for(var e=h._handlers.length;e--;){var t=h._handlers[e];t.element.removeEventListener(t.event,t.handler,t.options)}if(h._handlers=[],h.mobileInput)h.mobileInput.parentNode&&h.mobileInput.parentNode.removeChild(h.mobileInput),h.mobileInput=void 0;else if(h.calendarContainer&&h.calendarContainer.parentNode)if(h.config.static&&h.calendarContainer.parentNode){var n=h.calendarContainer.parentNode;if(n.lastChild&&n.removeChild(n.lastChild),n.parentNode){for(;n.firstChild;)n.parentNode.insertBefore(n.firstChild,n);n.parentNode.removeChild(n)}}else h.calendarContainer.parentNode.removeChild(h.calendarContainer);h.altInput&&(h.input.type=\"text\",h.altInput.parentNode&&h.altInput.parentNode.removeChild(h.altInput),delete h.altInput),h.input&&(h.input.type=h.input._type,h.input.classList.remove(\"flatpickr-input\"),h.input.removeAttribute(\"readonly\"),h.input.value=\"\"),[\"_showTimeInput\",\"latestSelectedDateObj\",\"_hideNextMonthArrow\",\"_hidePrevMonthArrow\",\"__hideNextMonthArrow\",\"__hidePrevMonthArrow\",\"isMobile\",\"isOpen\",\"selectedDateElem\",\"minDateHasTime\",\"maxDateHasTime\",\"days\",\"daysContainer\",\"_input\",\"_positionElement\",\"innerContainer\",\"rContainer\",\"monthNav\",\"todayDateElem\",\"calendarContainer\",\"weekdayContainer\",\"prevMonthNav\",\"nextMonthNav\",\"monthsDropdownContainer\",\"currentMonthElement\",\"currentYearElement\",\"navigationCurrentMonth\",\"selectedDateElem\",\"config\"].forEach((function(e){try{delete h[e]}catch(e){}}))},h.isEnabled=Z,h.jumpToDate=N,h.open=function(e,t){if(void 0===t&&(t=h._positionElement),!0===h.isMobile)return e&&(e.preventDefault(),e.target&&e.target.blur()),void 0!==h.mobileInput&&(h.mobileInput.focus(),h.mobileInput.click()),void fe(\"onOpen\");if(!h._input.disabled&&!h.config.inline){var n=h.isOpen;h.isOpen=!0,n||(h.calendarContainer.classList.add(\"open\"),h._input.classList.add(\"active\"),fe(\"onOpen\"),oe(t)),!0===h.config.enableTime&&!0===h.config.noCalendar&&(0===h.selectedDates.length&&ne(),!1!==h.config.allowInput||void 0!==e&&h.timeContainer.contains(e.relatedTarget)||setTimeout((function(){return h.hourElement.select()}),50))}},h.redraw=re,h.set=function(e,n){if(null!==e&&\"object\"==typeof e)for(var a in Object.assign(h.config,e),e)void 0!==de[a]&&de[a].forEach((function(e){return e()}));else h.config[e]=n,void 0!==de[e]?de[e].forEach((function(e){return e()})):t.indexOf(e)>-1&&(h.config[e]=l(n));h.redraw(),ve(!1)},h.setDate=function(e,t,n){if(void 0===t&&(t=!1),void 0===n&&(n=h.config.dateFormat),0!==e&&!e||e instanceof Array&&0===e.length)return h.clear(t);se(e,n),h.showTimeInput=h.selectedDates.length>0,h.latestSelectedDateObj=h.selectedDates[h.selectedDates.length-1],h.redraw(),N(),T(),0===h.selectedDates.length&&h.clear(!1),ve(t),t&&fe(\"onChange\")},h.toggle=function(e){if(!0===h.isOpen)return h.close();h.open(e)};var de={locale:[ie,q],showMonths:[K,M,U],minDate:[N],maxDate:[N]};function se(e,t){var n=[];if(e instanceof Array)n=e.map((function(e){return h.parseDate(e,t)}));else if(e instanceof Date||\"number\"==typeof e)n=[h.parseDate(e,t)];else if(\"string\"==typeof e)switch(h.config.mode){case\"single\":case\"time\":n=[h.parseDate(e,t)];break;case\"multiple\":n=e.split(h.config.conjunction).map((function(e){return h.parseDate(e,t)}));break;case\"range\":n=e.split(h.l10n.rangeSeparator).map((function(e){return h.parseDate(e,t)}))}else h.config.errorHandler(new Error(\"Invalid date supplied: \"+JSON.stringify(e)));h.selectedDates=n.filter((function(e){return e instanceof Date&&Z(e,!1)})),\"range\"===h.config.mode&&h.selectedDates.sort((function(e,t){return e.getTime()-t.getTime()}))}function ue(e){return e.slice().map((function(e){return\"string\"==typeof e||\"number\"==typeof e||e instanceof Date?h.parseDate(e,void 0,!0):e&&\"object\"==typeof e&&e.from&&e.to?{from:h.parseDate(e.from,void 0),to:h.parseDate(e.to,void 0)}:e})).filter((function(e){return e}))}function fe(e,t){if(void 0!==h.config){var n=h.config[e];if(void 0!==n&&n.length>0)for(var a=0;n[a]&&a1||\"static\"===h.config.monthSelectorType?h.monthElements[t].textContent=m(n.getMonth(),h.config.shorthandCurrentMonth,h.l10n)+\" \":h.monthsDropdownContainer.value=n.getMonth().toString(),e.value=n.getFullYear().toString()})),h._hidePrevMonthArrow=void 0!==h.config.minDate&&(h.currentYear===h.config.minDate.getFullYear()?h.currentMonth<=h.config.minDate.getMonth():h.currentYearh.config.maxDate.getMonth():h.currentYear>h.config.maxDate.getFullYear()))}function he(e){return h.selectedDates.map((function(t){return h.formatDate(t,e)})).filter((function(e,t,n){return\"range\"!==h.config.mode||h.config.enableTime||n.indexOf(e)===t})).join(\"range\"!==h.config.mode?h.config.conjunction:h.l10n.rangeSeparator)}function ve(e){void 0===e&&(e=!0),void 0!==h.mobileInput&&h.mobileFormatStr&&(h.mobileInput.value=void 0!==h.latestSelectedDateObj?h.formatDate(h.latestSelectedDateObj,h.mobileFormatStr):\"\"),h.input.value=he(h.config.dateFormat),void 0!==h.altInput&&(h.altInput.value=he(h.config.altFormat)),!1!==e&&fe(\"onValueUpdate\")}function De(e){var t=h.prevMonthNav.contains(e.target),n=h.nextMonthNav.contains(e.target);t||n?$(t?-1:1):h.yearElements.indexOf(e.target)>=0?e.target.select():e.target.classList.contains(\"arrowUp\")?h.changeYear(h.currentYear+1):e.target.classList.contains(\"arrowDown\")&&h.changeYear(h.currentYear-1)}return function(){h.element=h.input=f,h.isOpen=!1,function(){var a=[\"wrap\",\"weekNumbers\",\"allowInput\",\"clickOpens\",\"time_24hr\",\"enableTime\",\"noCalendar\",\"altInput\",\"shorthandCurrentMonth\",\"inline\",\"static\",\"enableSeconds\",\"disableMobile\"],i=e({},g,JSON.parse(JSON.stringify(f.dataset||{}))),o={};h.config.parseDate=i.parseDate,h.config.formatDate=i.formatDate,Object.defineProperty(h.config,\"enable\",{get:function(){return h.config._enable},set:function(e){h.config._enable=ue(e)}}),Object.defineProperty(h.config,\"disable\",{get:function(){return h.config._disable},set:function(e){h.config._disable=ue(e)}});var r=\"time\"===i.mode;if(!i.dateFormat&&(i.enableTime||r)){var c=y.defaultConfig.dateFormat||n.dateFormat;o.dateFormat=i.noCalendar||r?\"H:i\"+(i.enableSeconds?\":S\":\"\"):c+\" H:i\"+(i.enableSeconds?\":S\":\"\")}if(i.altInput&&(i.enableTime||r)&&!i.altFormat){var d=y.defaultConfig.altFormat||n.altFormat;o.altFormat=i.noCalendar||r?\"h:i\"+(i.enableSeconds?\":S K\":\" K\"):d+\" h:i\"+(i.enableSeconds?\":S\":\"\")+\" K\"}i.altInputClass||(h.config.altInputClass=h.input.className+\" \"+h.config.altInputClass),Object.defineProperty(h.config,\"minDate\",{get:function(){return h.config._minDate},set:ae(\"min\")}),Object.defineProperty(h.config,\"maxDate\",{get:function(){return h.config._maxDate},set:ae(\"max\")});var s=function(e){return function(t){h.config[\"min\"===e?\"_minTime\":\"_maxTime\"]=h.parseDate(t,\"H:i:S\")}};Object.defineProperty(h.config,\"minTime\",{get:function(){return h.config._minTime},set:s(\"min\")}),Object.defineProperty(h.config,\"maxTime\",{get:function(){return h.config._maxTime},set:s(\"max\")}),\"time\"===i.mode&&(h.config.noCalendar=!0,h.config.enableTime=!0),Object.assign(h.config,o,i);for(var u=0;u-1?h.config[p]=l(m[p]).map(C).concat(h.config[p]):void 0===i[p]&&(h.config[p]=m[p])}fe(\"onParseConfig\")}(),ie(),h.input=h.config.wrap?f.querySelector(\"[data-input]\"):f,h.input?(h.input._type=h.input.type,h.input.type=\"text\",h.input.classList.add(\"flatpickr-input\"),h._input=h.input,h.config.altInput&&(h.altInput=d(h.input.nodeName,h.config.altInputClass),h._input=h.altInput,h.altInput.placeholder=h.input.placeholder,h.altInput.disabled=h.input.disabled,h.altInput.required=h.input.required,h.altInput.tabIndex=h.input.tabIndex,h.altInput.type=\"text\",h.input.setAttribute(\"type\",\"hidden\"),!h.config.static&&h.input.parentNode&&h.input.parentNode.insertBefore(h.altInput,h.input.nextSibling)),h.config.allowInput||h._input.setAttribute(\"readonly\",\"readonly\"),h._positionElement=h.config.positionElement||h._input):h.config.errorHandler(new Error(\"Invalid input element specified\")),function(){h.selectedDates=[],h.now=h.parseDate(h.config.now)||new Date;var e=h.config.defaultDate||(\"INPUT\"!==h.input.nodeName&&\"TEXTAREA\"!==h.input.nodeName||!h.input.placeholder||h.input.value!==h.input.placeholder?h.input.value:null);e&&se(e,h.config.dateFormat),h._initialDate=h.selectedDates.length>0?h.selectedDates[0]:h.config.minDate&&h.config.minDate.getTime()>h.now.getTime()?h.config.minDate:h.config.maxDate&&h.config.maxDate.getTime()0&&(h.latestSelectedDateObj=h.selectedDates[0]),void 0!==h.config.minTime&&(h.config.minTime=h.parseDate(h.config.minTime,\"H:i\")),void 0!==h.config.maxTime&&(h.config.maxTime=h.parseDate(h.config.maxTime,\"H:i\")),h.minDateHasTime=!!h.config.minDate&&(h.config.minDate.getHours()>0||h.config.minDate.getMinutes()>0||h.config.minDate.getSeconds()>0),h.maxDateHasTime=!!h.config.maxDate&&(h.config.maxDate.getHours()>0||h.config.maxDate.getMinutes()>0||h.config.maxDate.getSeconds()>0),Object.defineProperty(h,\"showTimeInput\",{get:function(){return h._showTimeInput},set:function(e){h._showTimeInput=e,h.calendarContainer&&c(h.calendarContainer,\"showTimeInput\",e),h.isOpen&&oe()}})}(),h.utils={getDaysInMonth:function(e,t){return void 0===e&&(e=h.currentMonth),void 0===t&&(t=h.currentYear),1===e&&(t%4==0&&t%100!=0||t%400==0)?29:h.l10n.daysInMonth[e]}},h.isMobile||function(){var e=window.document.createDocumentFragment();if(h.calendarContainer=d(\"div\",\"flatpickr-calendar\"),h.calendarContainer.tabIndex=-1,!h.config.noCalendar){if(e.appendChild((h.monthNav=d(\"div\",\"flatpickr-months\"),h.yearElements=[],h.monthElements=[],h.prevMonthNav=d(\"span\",\"flatpickr-prev-month\"),h.prevMonthNav.innerHTML=h.config.prevArrow,h.nextMonthNav=d(\"span\",\"flatpickr-next-month\"),h.nextMonthNav.innerHTML=h.config.nextArrow,K(),Object.defineProperty(h,\"_hidePrevMonthArrow\",{get:function(){return h.__hidePrevMonthArrow},set:function(e){h.__hidePrevMonthArrow!==e&&(c(h.prevMonthNav,\"flatpickr-disabled\",e),h.__hidePrevMonthArrow=e)}}),Object.defineProperty(h,\"_hideNextMonthArrow\",{get:function(){return h.__hideNextMonthArrow},set:function(e){h.__hideNextMonthArrow!==e&&(c(h.nextMonthNav,\"flatpickr-disabled\",e),h.__hideNextMonthArrow=e)}}),h.currentYearElement=h.yearElements[0],pe(),h.monthNav)),h.innerContainer=d(\"div\",\"flatpickr-innerContainer\"),h.config.weekNumbers){var t=function(){h.calendarContainer.classList.add(\"hasWeeks\");var e=d(\"div\",\"flatpickr-weekwrapper\");e.appendChild(d(\"span\",\"flatpickr-weekday\",h.l10n.weekAbbreviation));var t=d(\"div\",\"flatpickr-weeks\");return e.appendChild(t),{weekWrapper:e,weekNumbers:t}}(),n=t.weekWrapper,a=t.weekNumbers;h.innerContainer.appendChild(n),h.weekNumbers=a,h.weekWrapper=n}h.rContainer=d(\"div\",\"flatpickr-rContainer\"),h.rContainer.appendChild(U()),h.daysContainer||(h.daysContainer=d(\"div\",\"flatpickr-days\"),h.daysContainer.tabIndex=-1),R(),h.rContainer.appendChild(h.daysContainer),h.innerContainer.appendChild(h.rContainer),e.appendChild(h.innerContainer)}h.config.enableTime&&e.appendChild(function(){h.calendarContainer.classList.add(\"hasTime\"),h.config.noCalendar&&h.calendarContainer.classList.add(\"noCalendar\"),h.timeContainer=d(\"div\",\"flatpickr-time\"),h.timeContainer.tabIndex=-1;var e=d(\"span\",\"flatpickr-time-separator\",\":\"),t=u(\"flatpickr-hour\",{\"aria-label\":h.l10n.hourAriaLabel});h.hourElement=t.getElementsByTagName(\"input\")[0];var n=u(\"flatpickr-minute\",{\"aria-label\":h.l10n.minuteAriaLabel});if(h.minuteElement=n.getElementsByTagName(\"input\")[0],h.hourElement.tabIndex=h.minuteElement.tabIndex=-1,h.hourElement.value=i(h.latestSelectedDateObj?h.latestSelectedDateObj.getHours():h.config.time_24hr?h.config.defaultHour:function(e){switch(e%24){case 0:case 12:return 12;default:return e%12}}(h.config.defaultHour)),h.minuteElement.value=i(h.latestSelectedDateObj?h.latestSelectedDateObj.getMinutes():h.config.defaultMinute),h.hourElement.setAttribute(\"step\",h.config.hourIncrement.toString()),h.minuteElement.setAttribute(\"step\",h.config.minuteIncrement.toString()),h.hourElement.setAttribute(\"min\",h.config.time_24hr?\"0\":\"1\"),h.hourElement.setAttribute(\"max\",h.config.time_24hr?\"23\":\"12\"),h.minuteElement.setAttribute(\"min\",\"0\"),h.minuteElement.setAttribute(\"max\",\"59\"),h.timeContainer.appendChild(t),h.timeContainer.appendChild(e),h.timeContainer.appendChild(n),h.config.time_24hr&&h.timeContainer.classList.add(\"time24hr\"),h.config.enableSeconds){h.timeContainer.classList.add(\"hasSeconds\");var a=u(\"flatpickr-second\");h.secondElement=a.getElementsByTagName(\"input\")[0],h.secondElement.value=i(h.latestSelectedDateObj?h.latestSelectedDateObj.getSeconds():h.config.defaultSeconds),h.secondElement.setAttribute(\"step\",h.minuteElement.getAttribute(\"step\")),h.secondElement.setAttribute(\"min\",\"0\"),h.secondElement.setAttribute(\"max\",\"59\"),h.timeContainer.appendChild(d(\"span\",\"flatpickr-time-separator\",\":\")),h.timeContainer.appendChild(a)}return h.config.time_24hr||(h.amPM=d(\"span\",\"flatpickr-am-pm\",h.l10n.amPM[o((h.latestSelectedDateObj?h.hourElement.value:h.config.defaultHour)>11)]),h.amPM.title=h.l10n.toggleTitle,h.amPM.tabIndex=-1,h.timeContainer.appendChild(h.amPM)),h.timeContainer}()),c(h.calendarContainer,\"rangeMode\",\"range\"===h.config.mode),c(h.calendarContainer,\"animate\",!0===h.config.animate),c(h.calendarContainer,\"multiMonth\",h.config.showMonths>1),h.calendarContainer.appendChild(e);var r=void 0!==h.config.appendTo&&void 0!==h.config.appendTo.nodeType;if((h.config.inline||h.config.static)&&(h.calendarContainer.classList.add(h.config.inline?\"inline\":\"static\"),h.config.inline&&(!r&&h.element.parentNode?h.element.parentNode.insertBefore(h.calendarContainer,h._input.nextSibling):void 0!==h.config.appendTo&&h.config.appendTo.appendChild(h.calendarContainer)),h.config.static)){var l=d(\"div\",\"flatpickr-wrapper\");h.element.parentNode&&h.element.parentNode.insertBefore(l,h.element),l.appendChild(h.element),h.altInput&&l.appendChild(h.altInput),l.appendChild(h.calendarContainer)}h.config.static||h.config.inline||(void 0!==h.config.appendTo?h.config.appendTo:window.document.body).appendChild(h.calendarContainer)}(),function(){if(h.config.wrap&&[\"open\",\"close\",\"toggle\",\"clear\"].forEach((function(e){Array.prototype.forEach.call(h.element.querySelectorAll(\"[data-\"+e+\"]\"),(function(t){return O(t,\"click\",h[e])}))})),h.isMobile)!function(){var e=h.config.enableTime?h.config.noCalendar?\"time\":\"datetime-local\":\"date\";h.mobileInput=d(\"input\",h.input.className+\" flatpickr-mobile\"),h.mobileInput.step=h.input.getAttribute(\"step\")||\"any\",h.mobileInput.tabIndex=1,h.mobileInput.type=e,h.mobileInput.disabled=h.input.disabled,h.mobileInput.required=h.input.required,h.mobileInput.placeholder=h.input.placeholder,h.mobileFormatStr=\"datetime-local\"===e?\"Y-m-d\\\\TH:i:S\":\"date\"===e?\"Y-m-d\":\"H:i:S\",h.selectedDates.length>0&&(h.mobileInput.defaultValue=h.mobileInput.value=h.formatDate(h.selectedDates[0],h.mobileFormatStr)),h.config.minDate&&(h.mobileInput.min=h.formatDate(h.config.minDate,\"Y-m-d\")),h.config.maxDate&&(h.mobileInput.max=h.formatDate(h.config.maxDate,\"Y-m-d\")),h.input.type=\"hidden\",void 0!==h.altInput&&(h.altInput.type=\"hidden\");try{h.input.parentNode&&h.input.parentNode.insertBefore(h.mobileInput,h.input.nextSibling)}catch(e){}O(h.mobileInput,\"change\",(function(e){h.setDate(e.target.value,!1,h.mobileFormatStr),fe(\"onChange\"),fe(\"onClose\")}))}();else{var e=r(te,50);h._debouncedChange=r(F,300),h.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&O(h.daysContainer,\"mouseover\",(function(e){\"range\"===h.config.mode&&ee(e.target)})),O(window.document.body,\"keydown\",X),h.config.inline||h.config.static||O(window,\"resize\",e),void 0!==window.ontouchstart?O(window.document,\"touchstart\",G):O(window.document,\"mousedown\",_(G)),O(window.document,\"focus\",G,{capture:!0}),!0===h.config.clickOpens&&(O(h._input,\"focus\",h.open),O(h._input,\"mousedown\",_(h.open))),void 0!==h.daysContainer&&(O(h.monthNav,\"mousedown\",_(De)),O(h.monthNav,[\"keyup\",\"increment\"],S),O(h.daysContainer,\"mousedown\",_(ce))),void 0!==h.timeContainer&&void 0!==h.minuteElement&&void 0!==h.hourElement&&(O(h.timeContainer,[\"increment\"],x),O(h.timeContainer,\"blur\",x,{capture:!0}),O(h.timeContainer,\"mousedown\",_(P)),O([h.hourElement,h.minuteElement],[\"focus\",\"click\"],(function(e){return e.target.select()})),void 0!==h.secondElement&&O(h.secondElement,\"focus\",(function(){return h.secondElement&&h.secondElement.select()})),void 0!==h.amPM&&O(h.amPM,\"mousedown\",_((function(e){x(e),F()}))))}}(),(h.selectedDates.length||h.config.noCalendar)&&(h.config.enableTime&&T(h.config.noCalendar?h.latestSelectedDateObj||h.config.minDate:void 0),ve(!1)),M(),h.showTimeInput=h.selectedDates.length>0||h.config.noCalendar;var a=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);!h.isMobile&&a&&oe(),fe(\"onReady\")}(),h}function M(e,t){for(var n=Array.prototype.slice.call(e).filter((function(e){return e instanceof HTMLElement})),a=[],i=0;ithis.render());const{start:s,end:l,value:r,step:o,title:n}=this.model.properties;this.on_change([s,l,r,o],()=>{const{start:t,end:e,value:i,step:s}=this._calc_to();this.noUiSlider.updateOptions({range:{min:t,max:e},start:i,step:s})});const{bar_color:a}=this.model.properties;this.on_change(a,()=>{this._set_bar_color()});const{show_value:d}=this.model.properties;this.on_change([r,n,d],()=>this._update_title())}styles(){return[...super.styles(),h.default,c.default]}_update_title(){r.empty(this.title_el);const t=null==this.model.title||0==this.model.title.length&&!this.model.show_value;if(this.title_el.style.display=t?\"none\":\"\",!t&&(0!=this.model.title.length&&(this.title_el.textContent=this.model.title+\": \"),this.model.show_value)){const{value:t}=this._calc_to(),e=t.map(t=>this.model.pretty(t)).join(\" .. \");this.title_el.appendChild(r.span({class:d.bk_slider_value},e))}}_set_bar_color(){if(!this.model.disabled){this.slider_el.querySelector(\".noUi-connect\").style.backgroundColor=this.model.bar_color}}render(){super.render();const{start:t,end:e,value:i,step:s}=this._calc_to();let n;if(this.model.tooltips){const t={to:t=>this.model.pretty(t)};n=o.repeat(t,i.length)}else n=!1;if(null==this.slider_el){this.slider_el=r.div(),l.create(this.slider_el,{range:{min:t,max:e},start:i,step:s,behaviour:this.model.behaviour,connect:this.model.connected,tooltips:n,orientation:this.model.orientation,direction:this.model.direction}),this.noUiSlider.on(\"slide\",(t,e,i)=>this._slide(i)),this.noUiSlider.on(\"change\",(t,e,i)=>this._change(i));const o=(t,e)=>{if(!n)return;this.slider_el.querySelectorAll(\".noUi-handle\")[t].querySelector(\".noUi-tooltip\").style.display=e?\"block\":\"\"};this.noUiSlider.on(\"start\",(t,e)=>o(e,!0)),this.noUiSlider.on(\"end\",(t,e)=>o(e,!1))}else this.noUiSlider.updateOptions({range:{min:t,max:e},start:i,step:s});this._set_bar_color(),this.model.disabled?this.slider_el.setAttribute(\"disabled\",\"true\"):this.slider_el.removeAttribute(\"disabled\"),this.title_el=r.div({class:d.bk_slider_title}),this._update_title(),this.group_el=r.div({class:_.bk_input_group},this.title_el,this.slider_el),this.el.appendChild(this.group_el)}_slide(t){this.model.value=this._calc_from(t)}_change(t){this.model.value=this._calc_from(t),this.model.value_throttled=this.model.value}}u.__name__=\"AbstractBaseSliderView\";class m extends u{_calc_to(){return{start:this.model.start,end:this.model.end,value:[this.model.value],step:this.model.step}}_calc_from([t]){return Number.isInteger(this.model.start)&&Number.isInteger(this.model.end)&&Number.isInteger(this.model.step)?Math.round(t):t}}i.AbstractSliderView=m,m.__name__=\"AbstractSliderView\";class p extends u{_calc_to(){return{start:this.model.start,end:this.model.end,value:this.model.value,step:this.model.step}}_calc_from(t){return t}}i.AbstractRangeSliderView=p,p.__name__=\"AbstractRangeSliderView\";class b extends n.Control{constructor(t){super(t),this.connected=!1}static init_AbstractSlider(){this.define(({Any:t,Boolean:e,Number:i,String:s,Color:l,Or:r,Enum:o,Ref:n})=>({title:[s,\"\"],show_value:[e,!0],start:[t],end:[t],value:[t],value_throttled:[t],step:[i,1],format:[r(s,n(a.TickFormatter))],direction:[o(\"ltr\",\"rtl\"),\"ltr\"],tooltips:[e,!0],bar_color:[l,\"#e6e6e6\"]}))}_formatter(t,e){return\"\"+t}pretty(t){return this._formatter(t,this.format)}}i.AbstractSlider=b,b.__name__=\"AbstractSlider\",b.init_AbstractSlider()},\n 424: function _(t,e,r){\n /*! nouislider - 14.6.0 - 6/27/2020 */\n var n;n=function(){\"use strict\";var t=\"14.6.0\";function e(t){t.parentElement.removeChild(t)}function r(t){return null!=t}function n(t){t.preventDefault()}function i(t){return\"number\"==typeof t&&!isNaN(t)&&isFinite(t)}function o(t,e,r){r>0&&(u(t,e),setTimeout((function(){c(t,e)}),r))}function s(t){return Math.max(Math.min(t,100),0)}function a(t){return Array.isArray(t)?t:[t]}function l(t){var e=(t=String(t)).split(\".\");return e.length>1?e[1].length:0}function u(t,e){t.classList&&!/\\s/.test(e)?t.classList.add(e):t.className+=\" \"+e}function c(t,e){t.classList&&!/\\s/.test(e)?t.classList.remove(e):t.className=t.className.replace(new RegExp(\"(^|\\\\b)\"+e.split(\" \").join(\"|\")+\"(\\\\b|$)\",\"gi\"),\" \")}function p(t){var e=void 0!==window.pageXOffset,r=\"CSS1Compat\"===(t.compatMode||\"\");return{x:e?window.pageXOffset:r?t.documentElement.scrollLeft:t.body.scrollLeft,y:e?window.pageYOffset:r?t.documentElement.scrollTop:t.body.scrollTop}}function f(t,e){return 100/(e-t)}function d(t,e,r){return 100*e/(t[r+1]-t[r])}function h(t,e){for(var r=1;t>=e[r];)r+=1;return r}function m(t,e,r){if(r>=t.slice(-1)[0])return 100;var n=h(r,t),i=t[n-1],o=t[n],s=e[n-1],a=e[n];return s+function(t,e){return d(t,t[0]<0?e+Math.abs(t[0]):e-t[0],0)}([i,o],r)/f(s,a)}function g(t,e,r,n){if(100===n)return n;var i=h(n,t),o=t[i-1],s=t[i];return r?n-o>(s-o)/2?s:o:e[i-1]?t[i-1]+function(t,e){return Math.round(t/e)*e}(n-t[i-1],e[i-1]):n}function v(t,e,r){var n;if(\"number\"==typeof e&&(e=[e]),!Array.isArray(e))throw new Error(\"noUiSlider (14.6.0): 'range' contains invalid value.\");if(!i(n=\"min\"===t?0:\"max\"===t?100:parseFloat(t))||!i(e[0]))throw new Error(\"noUiSlider (14.6.0): 'range' value isn't numeric.\");r.xPct.push(n),r.xVal.push(e[0]),n?r.xSteps.push(!isNaN(e[1])&&e[1]):isNaN(e[1])||(r.xSteps[0]=e[1]),r.xHighestCompleteStep.push(0)}function b(t,e,r){if(e)if(r.xVal[t]!==r.xVal[t+1]){r.xSteps[t]=d([r.xVal[t],r.xVal[t+1]],e,0)/f(r.xPct[t],r.xPct[t+1]);var n=(r.xVal[t+1]-r.xVal[t])/r.xNumSteps[t],i=Math.ceil(Number(n.toFixed(3))-1),o=r.xVal[t]+r.xNumSteps[t]*i;r.xHighestCompleteStep[t]=o}else r.xSteps[t]=r.xHighestCompleteStep[t]=r.xVal[t]}function x(t,e,r){var n;this.xPct=[],this.xVal=[],this.xSteps=[r||!1],this.xNumSteps=[!1],this.xHighestCompleteStep=[],this.snap=e;var i=[];for(n in t)t.hasOwnProperty(n)&&i.push([t[n],n]);for(i.length&&\"object\"==typeof i[0][0]?i.sort((function(t,e){return t[0][0]-e[0][0]})):i.sort((function(t,e){return t[0]-e[0]})),n=0;nthis.xPct[i+1];)i++;else t===this.xPct[this.xPct.length-1]&&(i=this.xPct.length-2);r||t!==this.xPct[i+1]||i++;var o=1,s=e[i],a=0,l=0,u=0,c=0;for(n=r?(t-this.xPct[i])/(this.xPct[i+1]-this.xPct[i]):(this.xPct[i+1]-t)/(this.xPct[i+1]-this.xPct[i]);s>0;)a=this.xPct[i+1+c]-this.xPct[i+c],e[i+c]*o+100-100*n>100?(l=a*n,o=(s-100*n)/e[i+c],n=1):(l=e[i+c]*a/100*o,o=0),r?(u-=l,this.xPct.length+c>=1&&c--):(u+=l,this.xPct.length-c>=1&&c++),s=e[i+c]*o;return t+u},x.prototype.toStepping=function(t){return t=m(this.xVal,this.xPct,t)},x.prototype.fromStepping=function(t){return function(t,e,r){if(r>=100)return t.slice(-1)[0];var n=h(r,e),i=t[n-1],o=t[n],s=e[n-1];return function(t,e){return e*(t[1]-t[0])/100+t[0]}([i,o],(r-s)*f(s,e[n]))}(this.xVal,this.xPct,t)},x.prototype.getStep=function(t){return t=g(this.xPct,this.xSteps,this.snap,t)},x.prototype.getDefaultStep=function(t,e,r){var n=h(t,this.xPct);return(100===t||e&&t===this.xPct[n-1])&&(n=Math.max(n-1,1)),(this.xVal[n]-this.xVal[n-1])/r},x.prototype.getNearbySteps=function(t){var e=h(t,this.xPct);return{stepBefore:{startValue:this.xVal[e-2],step:this.xNumSteps[e-2],highestStep:this.xHighestCompleteStep[e-2]},thisStep:{startValue:this.xVal[e-1],step:this.xNumSteps[e-1],highestStep:this.xHighestCompleteStep[e-1]},stepAfter:{startValue:this.xVal[e],step:this.xNumSteps[e],highestStep:this.xHighestCompleteStep[e]}}},x.prototype.countStepDecimals=function(){var t=this.xNumSteps.map(l);return Math.max.apply(null,t)},x.prototype.convert=function(t){return this.getStep(this.toStepping(t))};var S={to:function(t){return void 0!==t&&t.toFixed(2)},from:Number},w={target:\"target\",base:\"base\",origin:\"origin\",handle:\"handle\",handleLower:\"handle-lower\",handleUpper:\"handle-upper\",touchArea:\"touch-area\",horizontal:\"horizontal\",vertical:\"vertical\",background:\"background\",connect:\"connect\",connects:\"connects\",ltr:\"ltr\",rtl:\"rtl\",textDirectionLtr:\"txt-dir-ltr\",textDirectionRtl:\"txt-dir-rtl\",draggable:\"draggable\",drag:\"state-drag\",tap:\"state-tap\",active:\"active\",tooltip:\"tooltip\",pips:\"pips\",pipsHorizontal:\"pips-horizontal\",pipsVertical:\"pips-vertical\",marker:\"marker\",markerHorizontal:\"marker-horizontal\",markerVertical:\"marker-vertical\",markerNormal:\"marker-normal\",markerLarge:\"marker-large\",markerSub:\"marker-sub\",value:\"value\",valueHorizontal:\"value-horizontal\",valueVertical:\"value-vertical\",valueNormal:\"value-normal\",valueLarge:\"value-large\",valueSub:\"value-sub\"};function y(t){if(function(t){return\"object\"==typeof t&&\"function\"==typeof t.to&&\"function\"==typeof t.from}(t))return!0;throw new Error(\"noUiSlider (14.6.0): 'format' requires 'to' and 'from' methods.\")}function E(t,e){if(!i(e))throw new Error(\"noUiSlider (14.6.0): 'step' is not numeric.\");t.singleStep=e}function C(t,e){if(!i(e))throw new Error(\"noUiSlider (14.6.0): 'keyboardPageMultiplier' is not numeric.\");t.keyboardPageMultiplier=e}function P(t,e){if(!i(e))throw new Error(\"noUiSlider (14.6.0): 'keyboardDefaultStep' is not numeric.\");t.keyboardDefaultStep=e}function N(t,e){if(\"object\"!=typeof e||Array.isArray(e))throw new Error(\"noUiSlider (14.6.0): 'range' is not an object.\");if(void 0===e.min||void 0===e.max)throw new Error(\"noUiSlider (14.6.0): Missing 'min' or 'max' in 'range'.\");if(e.min===e.max)throw new Error(\"noUiSlider (14.6.0): 'range' 'min' and 'max' cannot be equal.\");t.spectrum=new x(e,t.snap,t.singleStep)}function k(t,e){if(e=a(e),!Array.isArray(e)||!e.length)throw new Error(\"noUiSlider (14.6.0): 'start' option is incorrect.\");t.handles=e.length,t.start=e}function U(t,e){if(t.snap=e,\"boolean\"!=typeof e)throw new Error(\"noUiSlider (14.6.0): 'snap' option must be a boolean.\")}function A(t,e){if(t.animate=e,\"boolean\"!=typeof e)throw new Error(\"noUiSlider (14.6.0): 'animate' option must be a boolean.\")}function V(t,e){if(t.animationDuration=e,\"number\"!=typeof e)throw new Error(\"noUiSlider (14.6.0): 'animationDuration' option must be a number.\")}function D(t,e){var r,n=[!1];if(\"lower\"===e?e=[!0,!1]:\"upper\"===e&&(e=[!1,!0]),!0===e||!1===e){for(r=1;r1)throw new Error(\"noUiSlider (14.6.0): 'padding' option must not exceed 100% of the range.\")}}function H(t,e){switch(e){case\"ltr\":t.dir=0;break;case\"rtl\":t.dir=1;break;default:throw new Error(\"noUiSlider (14.6.0): 'direction' option was not recognized.\")}}function j(t,e){if(\"string\"!=typeof e)throw new Error(\"noUiSlider (14.6.0): 'behaviour' must be a string containing options.\");var r=e.indexOf(\"tap\")>=0,n=e.indexOf(\"drag\")>=0,i=e.indexOf(\"fixed\")>=0,o=e.indexOf(\"snap\")>=0,s=e.indexOf(\"hover\")>=0,a=e.indexOf(\"unconstrained\")>=0;if(i){if(2!==t.handles)throw new Error(\"noUiSlider (14.6.0): 'fixed' behaviour must be used with 2 handles\");O(t,t.start[1]-t.start[0])}if(a&&(t.margin||t.limit))throw new Error(\"noUiSlider (14.6.0): 'unconstrained' behaviour cannot be used with margin or limit\");t.events={tap:r||o,drag:n,fixed:i,snap:o,hover:s,unconstrained:a}}function F(t,e){if(!1!==e)if(!0===e){t.tooltips=[];for(var r=0;r0&&((a=M(i,!1)).className=c(s,r.cssClasses.value),a.setAttribute(\"data-value\",o),a.style[r.style]=t+\"%\",a.innerHTML=n.to(o))}}(o,t[o][0],t[o][1])})),i}function B(){h&&(e(h),h=null)}function q(t){B();var e=t.mode,r=t.density||1,n=t.filter||!1,i=function(t,e,r){if(\"range\"===t||\"steps\"===t)return y.xVal;if(\"count\"===t){if(e<2)throw new Error(\"noUiSlider (14.6.0): 'values' (>= 2) required for mode 'count'.\");var n=e-1,i=100/n;for(e=[];n--;)e[n]=n*i;e.push(100),t=\"positions\"}return\"positions\"===t?e.map((function(t){return y.fromStepping(r?y.getStep(t):t)})):\"values\"===t?r?e.map((function(t){return y.fromStepping(y.getStep(y.toStepping(t)))})):e:void 0}(e,t.values||!1,t.stepped||!1),o=function(t,e,r){var n,i={},o=y.xVal[0],s=y.xVal[y.xVal.length-1],a=!1,l=!1,u=0;return n=r.slice().sort((function(t,e){return t-e})),(r=n.filter((function(t){return!this[t]&&(this[t]=!0)}),{}))[0]!==o&&(r.unshift(o),a=!0),r[r.length-1]!==s&&(r.push(s),l=!0),r.forEach((function(n,o){var s,c,p,f,d,h,m,g,v,b,x=n,S=r[o+1],w=\"steps\"===e;if(w&&(s=y.xNumSteps[o]),s||(s=S-x),!1!==x&&void 0!==S)for(s=Math.max(s,1e-7),c=x;c<=S;c=(c+s).toFixed(7)/1){for(g=(d=(f=y.toStepping(c))-u)/t,b=d/(v=Math.round(g)),p=1;p<=v;p+=1)i[(h=u+p*b).toFixed(5)]=[y.fromStepping(h),0];m=r.indexOf(c)>-1?1:w?2:0,!o&&a&&c!==S&&(m=0),c===S&&l||(i[f.toFixed(5)]=[c,m]),u=f}})),i}(r,e,i),s=t.format||{to:Math.round};return h=w.appendChild(T(o,n,s))}function X(){var t=l.getBoundingClientRect(),e=\"offset\"+[\"Width\",\"Height\"][r.ort];return 0===r.ort?t.width||l[e]:t.height||l[e]}function _(t,e,n,i){var o=function(o){return!!(o=function(t,e,r){var n,i,o=0===t.type.indexOf(\"touch\"),s=0===t.type.indexOf(\"mouse\"),a=0===t.type.indexOf(\"pointer\");if(0===t.type.indexOf(\"MSPointer\")&&(a=!0),o){var l=function(t){return t.target===r||r.contains(t.target)||t.target.shadowRoot&&t.target.shadowRoot.contains(r)};if(\"touchstart\"===t.type){var u=Array.prototype.filter.call(t.touches,l);if(u.length>1)return!1;n=u[0].pageX,i=u[0].pageY}else{var c=Array.prototype.find.call(t.changedTouches,l);if(!c)return!1;n=c.pageX,i=c.pageY}}return e=e||p(U),(s||a)&&(n=t.clientX+e.x,i=t.clientY+e.y),t.pageOffset=e,t.points=[n,i],t.cursor=s||a,t}(o,i.pageOffset,i.target||e))&&!(H()&&!i.doNotReject)&&(s=w,a=r.cssClasses.tap,!((s.classList?s.classList.contains(a):new RegExp(\"\\\\b\"+a+\"\\\\b\").test(s.className))&&!i.doNotReject)&&!(t===x.start&&void 0!==o.buttons&&o.buttons>1)&&(!i.hover||!o.buttons)&&(S||o.preventDefault(),o.calcPoint=o.points[r.ort],void n(o,i)));var s,a},s=[];return t.split(\" \").forEach((function(t){e.addEventListener(t,o,!!S&&{passive:!0}),s.push([t,o])})),s}function I(t){var e,n,i,o,a,u,c=100*(t-(e=l,n=r.ort,i=e.getBoundingClientRect(),o=e.ownerDocument,a=o.documentElement,u=p(o),/webkit.*Chrome.*Mobile/i.test(navigator.userAgent)&&(u.x=0),n?i.top+u.y-a.clientTop:i.left+u.x-a.clientLeft))/X();return c=s(c),r.dir?100-c:c}function W(t,e){\"mouseout\"===t.type&&\"HTML\"===t.target.nodeName&&null===t.relatedTarget&&G(t,e)}function $(t,e){if(-1===navigator.appVersion.indexOf(\"MSIE 9\")&&0===t.buttons&&0!==e.buttonsProperty)return G(t,e);var n=(r.dir?-1:1)*(t.calcPoint-e.startCalcPoint);it(n>0,100*n/e.baseSize,e.locations,e.handleNumbers)}function G(t,e){e.handle&&(c(e.handle,r.cssClasses.active),N-=1),e.listeners.forEach((function(t){A.removeEventListener(t[0],t[1])})),0===N&&(c(w,r.cssClasses.drag),st(),t.cursor&&(V.style.cursor=\"\",V.removeEventListener(\"selectstart\",n))),e.handleNumbers.forEach((function(t){et(\"change\",t),et(\"set\",t),et(\"end\",t)}))}function J(t,e){if(e.handleNumbers.some(j))return!1;var i;1===e.handleNumbers.length&&(i=f[e.handleNumbers[0]].children[0],N+=1,u(i,r.cssClasses.active)),t.stopPropagation();var o=[],s=_(x.move,A,$,{target:t.target,handle:i,listeners:o,startCalcPoint:t.calcPoint,baseSize:X(),pageOffset:t.pageOffset,handleNumbers:e.handleNumbers,buttonsProperty:t.buttons,locations:C.slice()}),a=_(x.end,A,G,{target:t.target,handle:i,listeners:o,doNotReject:!0,handleNumbers:e.handleNumbers}),l=_(\"mouseout\",A,W,{target:t.target,handle:i,listeners:o,doNotReject:!0,handleNumbers:e.handleNumbers});o.push.apply(o,s.concat(a,l)),t.cursor&&(V.style.cursor=getComputedStyle(t.target).cursor,f.length>1&&u(w,r.cssClasses.drag),V.addEventListener(\"selectstart\",n,!1)),e.handleNumbers.forEach((function(t){et(\"start\",t)}))}function K(t){if(!t.buttons&&!t.touches)return!1;t.stopPropagation();var e=I(t.calcPoint),n=function(t){var e=100,r=!1;return f.forEach((function(n,i){if(!j(i)){var o=C[i],s=Math.abs(o-t);(so||100===s&&100===e)&&(r=i,e=s)}})),r}(e);if(!1===n)return!1;r.events.snap||o(w,r.cssClasses.tap,r.animationDuration),at(n,e,!0,!0),st(),et(\"slide\",n,!0),et(\"update\",n,!0),et(\"change\",n,!0),et(\"set\",n,!0),r.events.snap&&J(t,{handleNumbers:[n]})}function Q(t){var e=I(t.calcPoint),r=y.getStep(e),n=y.fromStepping(r);Object.keys(k).forEach((function(t){\"hover\"===t.split(\".\")[0]&&k[t].forEach((function(t){t.call(g,n)}))}))}function Z(t,e){k[t]=k[t]||[],k[t].push(e),\"update\"===t.split(\".\")[0]&&f.forEach((function(t,e){et(\"update\",e)}))}function tt(t){var e=t&&t.split(\".\")[0],r=e&&t.substring(e.length);Object.keys(k).forEach((function(t){var n=t.split(\".\")[0],i=t.substring(n.length);e&&e!==n||r&&r!==i||delete k[t]}))}function et(t,e,n){Object.keys(k).forEach((function(i){var o=i.split(\".\")[0];t===o&&k[i].forEach((function(t){t.call(g,E.map(r.format.to),e,E.slice(),n||!1,C.slice(),g)}))}))}function rt(t,e,n,i,o,a){var l;return f.length>1&&!r.events.unconstrained&&(i&&e>0&&(l=y.getAbsoluteDistance(t[e-1],r.margin,0),n=Math.max(n,l)),o&&e1&&r.limit&&(i&&e>0&&(l=y.getAbsoluteDistance(t[e-1],r.limit,0),n=Math.min(n,l)),o&&e1?n.forEach((function(t,r){var n=rt(i,t,i[t]+e,o[r],s[r],!1);!1===n?e=0:(e=n-i[t],i[t]=n)})):o=s=[!0];var a=!1;n.forEach((function(t,n){a=at(t,r[t]+e,o[n],s[n])||a})),a&&n.forEach((function(t){et(\"update\",t),et(\"slide\",t)}))}function ot(t,e){return r.dir?100-t-e:t}function st(){P.forEach((function(t){var e=C[t]>50?-1:1,r=3+(f.length+e*t);f[t].style.zIndex=r}))}function at(t,e,n,i){return!1!==(e=rt(C,t,e,n,i,!1))&&(function(t,e){C[t]=e,E[t]=y.fromStepping(e);var n=\"translate(\"+nt(10*(ot(e,0)-D)+\"%\",\"0\")+\")\";f[t].style[r.transformRule]=n,lt(t),lt(t+1)}(t,e),!0)}function lt(t){if(d[t]){var e=0,n=100;0!==t&&(e=C[t-1]),t!==d.length-1&&(n=C[t]);var i=n-e,o=\"translate(\"+nt(ot(e,i)+\"%\",\"0\")+\")\",s=\"scale(\"+nt(i/100,\"1\")+\")\";d[t].style[r.transformRule]=o+\" \"+s}}function ut(t,e){return null===t||!1===t||void 0===t?C[e]:(\"number\"==typeof t&&(t=String(t)),t=r.format.from(t),!1===(t=y.toStepping(t))||isNaN(t)?C[e]:t)}function ct(t,e){var n=a(t),i=void 0===C[0];e=void 0===e||!!e,r.animate&&!i&&o(w,r.cssClasses.tap,r.animationDuration),P.forEach((function(t){at(t,ut(n[t],t),!0,!1)}));for(var s=1===P.length?0:1;sn.stepAfter.startValue&&(o=n.stepAfter.startValue-i),s=i>n.thisStep.startValue?n.thisStep.step:!1!==n.stepBefore.step&&i-n.stepBefore.highestStep,100===e?o=null:0===e&&(s=null);var a=y.countStepDecimals();return null!==o&&!1!==o&&(o=Number(o.toFixed(a))),null!==s&&!1!==s&&(s=Number(s.toFixed(a))),[s,o]}return u(v=w,r.cssClasses.target),0===r.dir?u(v,r.cssClasses.ltr):u(v,r.cssClasses.rtl),0===r.ort?u(v,r.cssClasses.horizontal):u(v,r.cssClasses.vertical),u(v,\"rtl\"===getComputedStyle(v).direction?r.cssClasses.textDirectionRtl:r.cssClasses.textDirectionLtr),l=M(v,r.cssClasses.base),function(t,e){var n=M(e,r.cssClasses.connects);f=[],(d=[]).push(L(n,t[0]));for(var i=0;i=0&&t .noUi-tooltip {\\n -webkit-transform: translate(50%, 0);\\n transform: translate(50%, 0);\\n left: auto;\\n bottom: 10px;\\n}\\n.bk-root .noUi-vertical .noUi-origin > .noUi-tooltip {\\n -webkit-transform: translate(0, -18px);\\n transform: translate(0, -18px);\\n top: auto;\\n right: 28px;\\n}\\n.bk-root .noUi-handle {\\n cursor: grab;\\n cursor: -webkit-grab;\\n}\\n.bk-root .noUi-handle.noUi-active {\\n cursor: grabbing;\\n cursor: -webkit-grabbing;\\n}\\n.bk-root .noUi-handle:after,\\n.bk-root .noUi-handle:before {\\n display: none;\\n}\\n.bk-root .noUi-tooltip {\\n display: none;\\n white-space: nowrap;\\n}\\n.bk-root .noUi-handle:hover .noUi-tooltip {\\n display: block;\\n}\\n.bk-root .noUi-horizontal {\\n width: 100%;\\n height: 10px;\\n}\\n.bk-root .noUi-vertical {\\n width: 10px;\\n height: 100%;\\n}\\n.bk-root .noUi-horizontal .noUi-handle {\\n width: 14px;\\n height: 18px;\\n right: -7px;\\n top: -5px;\\n}\\n.bk-root .noUi-vertical .noUi-handle {\\n width: 18px;\\n height: 14px;\\n right: -5px;\\n top: -7px;\\n}\\n.bk-root .noUi-target.noUi-horizontal {\\n margin: 5px 0px;\\n}\\n.bk-root .noUi-target.noUi-vertical {\\n margin: 0px 5px;\\n}\\n\"},\n 427: function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});t.default=\"\\n.bk-root .bk-slider-title {\\n white-space: nowrap;\\n}\\n.bk-root .bk-slider-value {\\n font-weight: 600;\\n}\\n\"},\n 428: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const r=e(1).__importDefault(e(186)),a=e(423);class d extends a.AbstractSliderView{}i.DateSliderView=d,d.__name__=\"DateSliderView\";class s extends a.AbstractSlider{constructor(e){super(e),this.behaviour=\"tap\",this.connected=[!0,!1]}static init_DateSlider(){this.prototype.default_view=d,this.override({format:\"%d %b %Y\"})}_formatter(e,t){return r.default(e,t)}}i.DateSlider=s,s.__name__=\"DateSlider\",s.init_DateSlider()},\n 429: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const r=e(1),_=e(430),n=r.__importStar(e(18));class s extends _.MarkupView{render(){super.render(),this.model.render_as_text?this.markup_el.textContent=this.model.text:this.markup_el.innerHTML=this.model.text}}i.DivView=s,s.__name__=\"DivView\";class a extends _.Markup{constructor(e){super(e)}static init_Div(){this.prototype.default_view=s,this.define({render_as_text:[n.Boolean,!1]})}}i.Div=a,a.__name__=\"Div\",a.init_Div()},\n 430: function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),a=e(217),n=e(72),l=i.__importStar(e(18)),r=e(472),_=e(431),c=i.__importDefault(e(432));class u extends r.WidgetView{connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>{this.layout.invalidate_cache(),this.render(),this.root.compute_layout()})}styles(){return[...super.styles(),c.default]}_update_layout(){this.layout=new a.CachedVariadicBox(this.el),this.layout.set_sizing(this.box_sizing())}render(){super.render();const e=Object.assign(Object.assign({},this.model.style),{display:\"inline-block\"});this.markup_el=n.div({class:_.bk_clearfix,style:e}),this.el.appendChild(this.markup_el)}}s.MarkupView=u,u.__name__=\"MarkupView\";class o extends r.Widget{constructor(e){super(e)}static init_Markup(){this.define({text:[l.String,\"\"],style:[l.Any,{}]})}}s.Markup=o,o.__name__=\"Markup\",o.init_Markup()},\n 431: function _(e,c,f){Object.defineProperty(f,\"__esModule\",{value:!0}),f.bk_clearfix=\"bk-clearfix\"},\n 432: function _(e,n,t){Object.defineProperty(t,\"__esModule\",{value:!0});t.default='\\n.bk-root .bk-clearfix:before,\\n.bk-root .bk-clearfix:after {\\n content: \"\";\\n display: table;\\n}\\n.bk-root .bk-clearfix:after {\\n clear: both;\\n}\\n'},\n 433: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=e(404),o=e(313),_=e(72),d=n.__importStar(e(18)),l=e(8),r=e(173),u=e(281),c=e(282),h=n.__importDefault(e(284));class p extends s.AbstractButtonView{constructor(){super(...arguments),this._open=!1}styles(){return[...super.styles(),h.default]}render(){super.render();const e=_.div({class:[c.bk_caret,r.bk_down]});if(this.model.is_split){const t=this._render_button(e);t.classList.add(u.bk_dropdown_toggle),t.addEventListener(\"click\",()=>this._toggle_menu()),this.group_el.appendChild(t)}else this.button_el.appendChild(e);const t=this.model.menu.map((e,t)=>{if(null==e)return _.div({class:c.bk_divider});{const i=l.isString(e)?e:e[0],n=_.div({},i);return n.addEventListener(\"click\",()=>this._item_click(t)),n}});this.menu=_.div({class:[c.bk_menu,r.bk_below]},t),this.el.appendChild(this.menu),_.undisplay(this.menu)}_show_menu(){if(!this._open){this._open=!0,_.display(this.menu);const e=t=>{const{target:i}=t;i instanceof HTMLElement&&!this.el.contains(i)&&(document.removeEventListener(\"click\",e),this._hide_menu())};document.addEventListener(\"click\",e)}}_hide_menu(){this._open&&(this._open=!1,_.undisplay(this.menu))}_toggle_menu(){this._open?this._hide_menu():this._show_menu()}click(){this.model.is_split?(this._hide_menu(),this.model.trigger_event(new o.ButtonClick),super.click()):this._toggle_menu()}_item_click(e){this._hide_menu();const t=this.model.menu[e];if(null!=t){const i=l.isString(t)?t:t[1];l.isString(i)?this.model.trigger_event(new o.MenuItemClick(i)):i.execute(this.model,{index:e})}}}i.DropdownView=p,p.__name__=\"DropdownView\";class m extends s.AbstractButton{constructor(e){super(e)}static init_Dropdown(){this.prototype.default_view=p,this.define({split:[d.Boolean,!1],menu:[d.Array,[]]}),this.override({label:\"Dropdown\"})}get is_split(){return this.split}}i.Dropdown=m,m.__name__=\"Dropdown\",m.init_Dropdown()},\n 434: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const l=e(1).__importStar(e(18)),s=e(472);class n extends s.WidgetView{connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.render()),this.connect(this.model.properties.width.change,()=>this.render())}render(){null==this.dialogEl&&(this.dialogEl=document.createElement(\"input\"),this.dialogEl.type=\"file\",this.dialogEl.multiple=this.model.multiple,this.dialogEl.onchange=()=>{const{files:e}=this.dialogEl;null!=e&&this.load_files(e)},this.el.appendChild(this.dialogEl)),null!=this.model.accept&&\"\"!=this.model.accept&&(this.dialogEl.accept=this.model.accept),this.dialogEl.style.width=\"{this.model.width}px\",this.dialogEl.disabled=this.model.disabled}async load_files(e){const t=[],i=[],l=[];let s;for(s=0;s{const l=new FileReader;l.onload=()=>{var s;const{result:n}=l;null!=n?t(n):i(null!==(s=l.error)&&void 0!==s?s:new Error(`unable to read '${e.name}'`))},l.readAsDataURL(e)})}}i.FileInputView=n,n.__name__=\"FileInputView\";class o extends s.Widget{constructor(e){super(e)}static init_FileInput(){this.prototype.default_view=n,this.define({value:[l.Any,\"\"],mime_type:[l.Any,\"\"],filename:[l.Any,\"\"],accept:[l.String,\"\"],multiple:[l.Boolean,!1]})}}i.FileInput=o,o.__name__=\"FileInput\",o.init_FileInput()},\n 435: function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const i=e(1),n=e(72),l=e(8),o=i.__importStar(e(18)),c=e(410),r=e(412);class h extends c.InputWidgetView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.value.change,()=>this.render_selection()),this.connect(this.model.properties.options.change,()=>this.render()),this.connect(this.model.properties.name.change,()=>this.render()),this.connect(this.model.properties.title.change,()=>this.render()),this.connect(this.model.properties.size.change,()=>this.render()),this.connect(this.model.properties.disabled.change,()=>this.render())}render(){super.render();const e=this.model.options.map(e=>{let t,s;return l.isString(e)?t=s=e:[t,s]=e,n.option({value:t},s)});this.select_el=n.select({multiple:!0,class:r.bk_input,name:this.model.name,disabled:this.model.disabled},e),this.select_el.addEventListener(\"change\",()=>this.change_input()),this.group_el.appendChild(this.select_el),this.render_selection()}render_selection(){const e=new Set(this.model.value);for(const t of this.el.querySelectorAll(\"option\"))t.selected=e.has(t.value);this.select_el.size=this.model.size}change_input(){const e=null!=this.el.querySelector(\"select:focus\"),t=[];for(const e of this.el.querySelectorAll(\"option\"))e.selected&&t.push(e.value);this.model.value=t,super.change_input(),e&&this.select_el.focus()}}s.MultiSelectView=h,h.__name__=\"MultiSelectView\";class d extends c.InputWidget{constructor(e){super(e)}static init_MultiSelect(){this.prototype.default_view=h,this.define({value:[o.Array,[]],options:[o.Array,[]],size:[o.Number,4]})}}s.MultiSelect=d,d.__name__=\"MultiSelect\",d.init_MultiSelect()},\n 436: function _(a,e,r){Object.defineProperty(r,\"__esModule\",{value:!0});const t=a(430),p=a(72);class s extends t.MarkupView{render(){super.render();const a=p.p({style:{margin:0}},this.model.text);this.markup_el.appendChild(a)}}r.ParagraphView=s,s.__name__=\"ParagraphView\";class i extends t.Markup{constructor(a){super(a)}static init_Paragraph(){this.prototype.default_view=s}}r.Paragraph=i,i.__name__=\"Paragraph\",i.init_Paragraph()},\n 437: function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});const n=e(409);class r extends n.TextInputView{render(){super.render(),this.input_el.type=\"password\"}}s.PasswordInputView=r,r.__name__=\"PasswordInputView\";class p extends n.TextInput{constructor(e){super(e)}static init_PasswordInput(){this.prototype.default_view=r}}s.PasswordInput=p,p.__name__=\"PasswordInput\",p.init_PasswordInput()},\n 438: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const l=e(1),s=l.__importDefault(e(439)),o=e(72),n=e(8),h=e(217),a=l.__importStar(e(18)),c=e(412),u=l.__importDefault(e(440)),d=e(410);class _ extends d.InputWidgetView{constructor(){super(...arguments),this._last_height=null}connect_signals(){super.connect_signals(),this.connect(this.model.properties.disabled.change,()=>this.set_disabled());const{value:e,max_items:t,option_limit:i,delete_button:l,placeholder:s,options:o,name:n,title:h}=this.model.properties;this.on_change([e,t,i,l,s,o,n,h],()=>this.render())}styles(){return[...super.styles(),u.default]}_update_layout(){this.layout=new h.CachedVariadicBox(this.el),this.layout.set_sizing(this.box_sizing())}render(){super.render(),this.select_el=o.select({multiple:!0,class:c.bk_input,name:this.model.name,disabled:this.model.disabled}),this.group_el.appendChild(this.select_el);const e=new Set(this.model.value),t=this.model.options.map(t=>{let i,l;return n.isString(t)?i=l=t:[i,l]=t,{value:i,label:l,selected:e.has(i)}}),i=this.model.solid?\"solid\":\"light\",l=\"choices__item \"+i,h=\"choices__button \"+i,a={choices:t,duplicateItemsAllowed:!1,removeItemButton:this.model.delete_button,classNames:{item:l,button:h}};null!=this.model.placeholder&&(a.placeholderValue=this.model.placeholder),null!=this.model.max_items&&(a.maxItemCount=this.model.max_items),null!=this.model.option_limit&&(a.renderChoiceLimit=this.model.option_limit),this.choice_el=new s.default(this.select_el,a);const u=()=>this.choice_el.containerOuter.element.getBoundingClientRect().height;null!=this._last_height&&this._last_height!=u()&&this.root.invalidate_layout(),this._last_height=u(),this.select_el.addEventListener(\"change\",()=>this.change_input())}set_disabled(){this.model.disabled?this.choice_el.disable():this.choice_el.enable()}change_input(){const e=null!=this.el.querySelector(\"select:focus\"),t=[];for(const e of this.el.querySelectorAll(\"option\"))e.selected&&t.push(e.value);this.model.value=t,super.change_input(),e&&this.select_el.focus()}}i.MultiChoiceView=_,_.__name__=\"MultiChoiceView\";class r extends d.InputWidget{constructor(e){super(e)}static init_MultiChoice(){this.prototype.default_view=_,this.define({value:[a.Array,[]],options:[a.Array,[]],max_items:[a.Number,null],delete_button:[a.Boolean,!0],placeholder:[a.String,null],option_limit:[a.Number,null],solid:[a.Boolean,!0]})}}i.MultiChoice=r,r.__name__=\"MultiChoice\",r.init_MultiChoice()},\n 439: function _(e,t,i){\n /*! choices.js v9.0.1 | Β© 2019 Josh Johnson | https://github.com/jshjohnson/Choices#readme */\n var n,s;n=window,s=function(){return function(e){var t={};function i(n){if(t[n])return t[n].exports;var s=t[n]={i:n,l:!1,exports:{}};return e[n].call(s.exports,s,s.exports,i),s.l=!0,s.exports}return i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&\"object\"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,\"default\",{enumerable:!0,value:e}),2&t&&\"string\"!=typeof e)for(var s in e)i.d(n,s,function(t){return e[t]}.bind(null,s));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,\"a\",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p=\"/public/assets/scripts/\",i(i.s=4)}([function(e,t,i){\"use strict\";var n=function(e){return function(e){return!!e&&\"object\"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return\"[object RegExp]\"===t||\"[object Date]\"===t||function(e){return e.$$typeof===s}(e)}(e)},s=\"function\"==typeof Symbol&&Symbol.for?Symbol.for(\"react.element\"):60103;function r(e,t){return!1!==t.clone&&t.isMergeableObject(e)?l((i=e,Array.isArray(i)?[]:{}),e,t):e;var i}function o(e,t,i){return e.concat(t).map((function(e){return r(e,i)}))}function a(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return e.propertyIsEnumerable(t)})):[]}(e))}function c(e,t,i){var n={};return i.isMergeableObject(e)&&a(e).forEach((function(t){n[t]=r(e[t],i)})),a(t).forEach((function(s){(function(e,t){try{return t in e&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))}catch(e){return!1}})(e,s)||(i.isMergeableObject(t[s])&&e[s]?n[s]=function(e,t){if(!t.customMerge)return l;var i=t.customMerge(e);return\"function\"==typeof i?i:l}(s,i)(e[s],t[s],i):n[s]=r(t[s],i))})),n}function l(e,t,i){(i=i||{}).arrayMerge=i.arrayMerge||o,i.isMergeableObject=i.isMergeableObject||n,i.cloneUnlessOtherwiseSpecified=r;var s=Array.isArray(t);return s===Array.isArray(e)?s?i.arrayMerge(e,t,i):c(e,t,i):r(t,i)}l.all=function(e,t){if(!Array.isArray(e))throw new Error(\"first argument should be an array\");return e.reduce((function(e,i){return l(e,i,t)}),{})};var h=l;e.exports=h},function(e,t,i){\"use strict\";(function(e,n){var s,r=i(3);s=\"undefined\"!=typeof self?self:\"undefined\"!=typeof window?window:void 0!==e?e:n;var o=Object(r.a)(s);t.a=o}).call(this,i(5),i(6)(e))},function(e,t,i){\n /*!\n * Fuse.js v3.4.5 - Lightweight fuzzy-search (http://fusejs.io)\n *\n * Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me)\n * All Rights Reserved. Apache Software License 2.0\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n e.exports=function(e){var t={};function i(n){if(t[n])return t[n].exports;var s=t[n]={i:n,l:!1,exports:{}};return e[n].call(s.exports,s,s.exports,i),s.l=!0,s.exports}return i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&\"object\"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,\"default\",{enumerable:!0,value:e}),2&t&&\"string\"!=typeof e)for(var s in e)i.d(n,s,function(t){return e[t]}.bind(null,s));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,\"a\",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p=\"\",i(i.s=1)}([function(e,t){e.exports=function(e){return Array.isArray?Array.isArray(e):\"[object Array]\"===Object.prototype.toString.call(e)}},function(e,t,i){function n(e){return(n=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e})(e)}function s(e,t){for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:{limit:!1};this._log('---------\\nSearch pattern: \"'.concat(e,'\"'));var i=this._prepareSearchers(e),n=i.tokenSearchers,s=i.fullSearcher,r=this._search(n,s),o=r.weights,a=r.results;return this._computeScore(o,a),this.options.shouldSort&&this._sort(a),t.limit&&\"number\"==typeof t.limit&&(a=a.slice(0,t.limit)),this._format(a)}},{key:\"_prepareSearchers\",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:\"\",t=[];if(this.options.tokenize)for(var i=e.split(this.options.tokenSeparator),n=0,s=i.length;n0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1?arguments[1]:void 0,i=this.list,n={},s=[];if(\"string\"==typeof i[0]){for(var r=0,o=i.length;r1)throw new Error(\"Key weight has to be > 0 and <= 1\");p=p.name}else a[p]={weight:1};this._analyze({key:p,value:this.options.getFn(h,p),record:h,index:c},{resultMap:n,results:s,tokenSearchers:e,fullSearcher:t})}return{weights:a,results:s}}},{key:\"_analyze\",value:function(e,t){var i=e.key,n=e.arrayIndex,s=void 0===n?-1:n,r=e.value,o=e.record,c=e.index,l=t.tokenSearchers,h=void 0===l?[]:l,u=t.fullSearcher,d=void 0===u?[]:u,p=t.resultMap,m=void 0===p?{}:p,f=t.results,v=void 0===f?[]:f;if(null!=r){var g=!1,_=-1,b=0;if(\"string\"==typeof r){this._log(\"\\nKey: \".concat(\"\"===i?\"-\":i));var y=d.search(r);if(this._log('Full text: \"'.concat(r,'\", score: ').concat(y.score)),this.options.tokenize){for(var E=r.split(this.options.tokenSeparator),I=[],S=0;S-1&&(P=(P+_)/2),this._log(\"Score average:\",P);var D=!this.options.tokenize||!this.options.matchAllTokens||b>=h.length;if(this._log(\"\\nCheck Matches: \".concat(D)),(g||y.isMatch)&&D){var M=m[c];M?M.output.push({key:i,arrayIndex:s,value:r,score:P,matchedIndices:y.matchedIndices}):(m[c]={item:o,output:[{key:i,arrayIndex:s,value:r,score:P,matchedIndices:y.matchedIndices}]},v.push(m[c]))}}else if(a(r))for(var N=0,F=r.length;N-1&&(o.arrayIndex=r.arrayIndex),t.matches.push(o)}}})),this.options.includeScore&&s.push((function(e,t){t.score=e.score}));for(var r=0,o=e.length;ri)return s(e,this.pattern,n);var o=this.options,a=o.location,c=o.distance,l=o.threshold,h=o.findAllMatches,u=o.minMatchCharLength;return r(e,this.pattern,this.patternAlphabet,{location:a,distance:c,threshold:l,findAllMatches:h,minMatchCharLength:u})}}])&&n(t.prototype,i),e}();e.exports=a},function(e,t){var i=/[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|]/g;e.exports=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:/ +/g,s=new RegExp(t.replace(i,\"\\\\$&\").replace(n,\"|\")),r=e.match(s),o=!!r,a=[];if(o)for(var c=0,l=r.length;c=P;N-=1){var F=N-1,j=i[e.charAt(F)];if(j&&(E[F]=1),M[N]=(M[N+1]<<1|1)&j,0!==T&&(M[N]|=(O[N+1]|O[N])<<1|1|O[N+1]),M[N]&L&&(C=n(t,{errors:T,currentLocation:F,expectedLocation:v,distance:l}))<=_){if(_=C,(b=F)<=v)break;P=Math.max(1,2*v-b)}}if(n(t,{errors:T+1,currentLocation:v,expectedLocation:v,distance:l})>_)break;O=M}return{isMatch:b>=0,score:0===C?.001:C,matchedIndices:s(E,f)}}},function(e,t){e.exports=function(e,t){var i=t.errors,n=void 0===i?0:i,s=t.currentLocation,r=void 0===s?0:s,o=t.expectedLocation,a=void 0===o?0:o,c=t.distance,l=void 0===c?100:c,h=n/e.length,u=Math.abs(a-r);return l?h+u/l:u?1:h}},function(e,t){e.exports=function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,i=[],n=-1,s=-1,r=0,o=e.length;r=t&&i.push([n,s]),n=-1)}return e[r-1]&&r-n>=t&&i.push([n,r-1]),i}},function(e,t){e.exports=function(e){for(var t={},i=e.length,n=0;n/g,\"&rt;\").replace(/-1?e.map((function(e){var i=e;return i.id===parseInt(t.choiceId,10)&&(i.selected=!0),i})):e;case\"REMOVE_ITEM\":return t.choiceId>-1?e.map((function(e){var i=e;return i.id===parseInt(t.choiceId,10)&&(i.selected=!1),i})):e;case\"FILTER_CHOICES\":return e.map((function(e){var i=e;return i.active=t.results.some((function(e){var t=e.item,n=e.score;return t.id===i.id&&(i.score=n,!0)})),i}));case\"ACTIVATE_CHOICES\":return e.map((function(e){var i=e;return i.active=t.active,i}));case\"CLEAR_CHOICES\":return v;default:return e}},general:_}),A=function(e,t){var i=e;if(\"CLEAR_ALL\"===t.type)i=void 0;else if(\"RESET_TO\"===t.type)return O(t.state);return C(i,t)};function L(e,t){for(var i=0;i\"'+I(e)+'\"'},maxItemText:function(e){return\"Only \"+e+\" values can be added\"},valueComparer:function(e,t){return e===t},fuseOptions:{includeScore:!0},callbackOnInit:null,callbackOnCreateTemplates:null,classNames:{containerOuter:\"choices\",containerInner:\"choices__inner\",input:\"choices__input\",inputCloned:\"choices__input--cloned\",list:\"choices__list\",listItems:\"choices__list--multiple\",listSingle:\"choices__list--single\",listDropdown:\"choices__list--dropdown\",item:\"choices__item\",itemSelectable:\"choices__item--selectable\",itemDisabled:\"choices__item--disabled\",itemChoice:\"choices__item--choice\",placeholder:\"choices__placeholder\",group:\"choices__group\",groupHeading:\"choices__heading\",button:\"choices__button\",activeState:\"is-active\",focusState:\"is-focused\",openState:\"is-open\",disabledState:\"is-disabled\",highlightedState:\"is-highlighted\",selectedState:\"is-selected\",flippedState:\"is-flipped\",loadingState:\"is-loading\",noResults:\"has-no-results\",noChoices:\"has-no-choices\"}},D=\"showDropdown\",M=\"hideDropdown\",N=\"change\",F=\"choice\",j=\"search\",K=\"addItem\",R=\"removeItem\",H=\"highlightItem\",B=\"highlightChoice\",V=\"ADD_CHOICE\",G=\"FILTER_CHOICES\",q=\"ACTIVATE_CHOICES\",U=\"CLEAR_CHOICES\",z=\"ADD_GROUP\",W=\"ADD_ITEM\",X=\"REMOVE_ITEM\",$=\"HIGHLIGHT_ITEM\",J=46,Y=8,Z=13,Q=65,ee=27,te=38,ie=40,ne=33,se=34,re=function(){function e(e){var t=e.element,i=e.type,n=e.classNames,s=e.position;this.element=t,this.classNames=n,this.type=i,this.position=s,this.isOpen=!1,this.isFlipped=!1,this.isFocussed=!1,this.isDisabled=!1,this.isLoading=!1,this._onFocus=this._onFocus.bind(this),this._onBlur=this._onBlur.bind(this)}var t=e.prototype;return t.addEventListeners=function(){this.element.addEventListener(\"focus\",this._onFocus),this.element.addEventListener(\"blur\",this._onBlur)},t.removeEventListeners=function(){this.element.removeEventListener(\"focus\",this._onFocus),this.element.removeEventListener(\"blur\",this._onBlur)},t.shouldFlip=function(e){if(\"number\"!=typeof e)return!1;var t=!1;return\"auto\"===this.position?t=!window.matchMedia(\"(min-height: \"+(e+1)+\"px)\").matches:\"top\"===this.position&&(t=!0),t},t.setActiveDescendant=function(e){this.element.setAttribute(\"aria-activedescendant\",e)},t.removeActiveDescendant=function(){this.element.removeAttribute(\"aria-activedescendant\")},t.open=function(e){this.element.classList.add(this.classNames.openState),this.element.setAttribute(\"aria-expanded\",\"true\"),this.isOpen=!0,this.shouldFlip(e)&&(this.element.classList.add(this.classNames.flippedState),this.isFlipped=!0)},t.close=function(){this.element.classList.remove(this.classNames.openState),this.element.setAttribute(\"aria-expanded\",\"false\"),this.removeActiveDescendant(),this.isOpen=!1,this.isFlipped&&(this.element.classList.remove(this.classNames.flippedState),this.isFlipped=!1)},t.focus=function(){this.isFocussed||this.element.focus()},t.addFocusState=function(){this.element.classList.add(this.classNames.focusState)},t.removeFocusState=function(){this.element.classList.remove(this.classNames.focusState)},t.enable=function(){this.element.classList.remove(this.classNames.disabledState),this.element.removeAttribute(\"aria-disabled\"),\"select-one\"===this.type&&this.element.setAttribute(\"tabindex\",\"0\"),this.isDisabled=!1},t.disable=function(){this.element.classList.add(this.classNames.disabledState),this.element.setAttribute(\"aria-disabled\",\"true\"),\"select-one\"===this.type&&this.element.setAttribute(\"tabindex\",\"-1\"),this.isDisabled=!0},t.wrap=function(e){!function(e,t){void 0===t&&(t=document.createElement(\"div\")),e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t.appendChild(e)}(e,this.element)},t.unwrap=function(e){this.element.parentNode.insertBefore(e,this.element),this.element.parentNode.removeChild(this.element)},t.addLoadingState=function(){this.element.classList.add(this.classNames.loadingState),this.element.setAttribute(\"aria-busy\",\"true\"),this.isLoading=!0},t.removeLoadingState=function(){this.element.classList.remove(this.classNames.loadingState),this.element.removeAttribute(\"aria-busy\"),this.isLoading=!1},t._onFocus=function(){this.isFocussed=!0},t._onBlur=function(){this.isFocussed=!1},e}();function oe(e,t){for(var i=0;i0?this.element.scrollTop+o-s:e.offsetTop;requestAnimationFrame((function(){i._animateScroll(a,t)}))}},t._scrollDown=function(e,t,i){var n=(i-e)/t,s=n>1?n:1;this.element.scrollTop=e+s},t._scrollUp=function(e,t,i){var n=(e-i)/t,s=n>1?n:1;this.element.scrollTop=e-s},t._animateScroll=function(e,t){var i=this,n=this.element.scrollTop,s=!1;t>0?(this._scrollDown(n,4,e),ne&&(s=!0)),s&&requestAnimationFrame((function(){i._animateScroll(e,t)}))},e}();function le(e,t){for(var i=0;i0?\"treeitem\":\"option\"),Object.assign(g.dataset,{choice:\"\",id:l,value:h,selectText:i}),m?(g.classList.add(a),g.dataset.choiceDisabled=\"\",g.setAttribute(\"aria-disabled\",\"true\")):(g.classList.add(r),g.dataset.choiceSelectable=\"\"),g},input:function(e,t){var i=e.input,n=e.inputCloned,s=Object.assign(document.createElement(\"input\"),{type:\"text\",className:i+\" \"+n,autocomplete:\"off\",autocapitalize:\"off\",spellcheck:!1});return s.setAttribute(\"role\",\"textbox\"),s.setAttribute(\"aria-autocomplete\",\"list\"),s.setAttribute(\"aria-label\",t),s},dropdown:function(e){var t=e.list,i=e.listDropdown,n=document.createElement(\"div\");return n.classList.add(t,i),n.setAttribute(\"aria-expanded\",\"false\"),n},notice:function(e,t,i){var n=e.item,s=e.itemChoice,r=e.noResults,o=e.noChoices;void 0===i&&(i=\"\");var a=[n,s];return\"no-choices\"===i?a.push(o):\"no-results\"===i&&a.push(r),Object.assign(document.createElement(\"div\"),{innerHTML:t,className:a.join(\" \")})},option:function(e){var t=e.label,i=e.value,n=e.customProperties,s=e.active,r=e.disabled,o=new Option(t,i,!1,s);return n&&(o.dataset.customProperties=n),o.disabled=r,o}},ve=function(e){return void 0===e&&(e=!0),{type:q,active:e}},ge=function(e,t){return{type:$,id:e,highlighted:t}},_e=function(e){var t=e.value,i=e.id,n=e.active,s=e.disabled;return{type:z,value:t,id:i,active:n,disabled:s}},be=function(e){return{type:\"SET_IS_LOADING\",isLoading:e}};function ye(e,t){for(var i=0;i=0?this._store.getGroupById(s):null;return this._store.dispatch(ge(i,!0)),t&&this.passedElement.triggerEvent(H,{id:i,value:o,label:c,groupValue:l&&l.value?l.value:null}),this},r.unhighlightItem=function(e){if(!e)return this;var t=e.id,i=e.groupId,n=void 0===i?-1:i,s=e.value,r=void 0===s?\"\":s,o=e.label,a=void 0===o?\"\":o,c=n>=0?this._store.getGroupById(n):null;return this._store.dispatch(ge(t,!1)),this.passedElement.triggerEvent(H,{id:t,value:r,label:a,groupValue:c&&c.value?c.value:null}),this},r.highlightAll=function(){var e=this;return this._store.items.forEach((function(t){return e.highlightItem(t)})),this},r.unhighlightAll=function(){var e=this;return this._store.items.forEach((function(t){return e.unhighlightItem(t)})),this},r.removeActiveItemsByValue=function(e){var t=this;return this._store.activeItems.filter((function(t){return t.value===e})).forEach((function(e){return t._removeItem(e)})),this},r.removeActiveItems=function(e){var t=this;return this._store.activeItems.filter((function(t){return t.id!==e})).forEach((function(e){return t._removeItem(e)})),this},r.removeHighlightedItems=function(e){var t=this;return void 0===e&&(e=!1),this._store.highlightedActiveItems.forEach((function(i){t._removeItem(i),e&&t._triggerChange(i.value)})),this},r.showDropdown=function(e){var t=this;return this.dropdown.isActive||requestAnimationFrame((function(){t.dropdown.show(),t.containerOuter.open(t.dropdown.distanceFromTopWindow),!e&&t._canSearch&&t.input.focus(),t.passedElement.triggerEvent(D,{})})),this},r.hideDropdown=function(e){var t=this;return this.dropdown.isActive?(requestAnimationFrame((function(){t.dropdown.hide(),t.containerOuter.close(),!e&&t._canSearch&&(t.input.removeActiveDescendant(),t.input.blur()),t.passedElement.triggerEvent(M,{})})),this):this},r.getValue=function(e){void 0===e&&(e=!1);var t=this._store.activeItems.reduce((function(t,i){var n=e?i.value:i;return t.push(n),t}),[]);return this._isSelectOneElement?t[0]:t},r.setValue=function(e){var t=this;return this.initialised?(e.forEach((function(e){return t._setChoiceOrItem(e)})),this):this},r.setChoiceByValue=function(e){var t=this;return!this.initialised||this._isTextElement||(Array.isArray(e)?e:[e]).forEach((function(e){return t._findAndSelectChoiceByValue(e)})),this},r.setChoices=function(e,t,i,n){var s=this;if(void 0===e&&(e=[]),void 0===t&&(t=\"value\"),void 0===i&&(i=\"label\"),void 0===n&&(n=!1),!this.initialised)throw new ReferenceError(\"setChoices was called on a non-initialized instance of Choices\");if(!this._isSelectElement)throw new TypeError(\"setChoices can't be used with INPUT based Choices\");if(\"string\"!=typeof t||!t)throw new TypeError(\"value parameter must be a name of 'value' field in passed objects\");if(n&&this.clearChoices(),\"function\"==typeof e){var r=e(this);if(\"function\"==typeof Promise&&r instanceof Promise)return new Promise((function(e){return requestAnimationFrame(e)})).then((function(){return s._handleLoadingState(!0)})).then((function(){return r})).then((function(e){return s.setChoices(e,t,i,n)})).catch((function(e){s.config.silent||console.error(e)})).then((function(){return s._handleLoadingState(!1)})).then((function(){return s}));if(!Array.isArray(r))throw new TypeError(\".setChoices first argument function must return either array of choices or Promise, got: \"+typeof r);return this.setChoices(r,t,i,!1)}if(!Array.isArray(e))throw new TypeError(\".setChoices must be called either with array of choices with a function resulting into Promise of array of choices\");return this.containerOuter.removeLoadingState(),this._startLoading(),e.forEach((function(e){e.choices?s._addGroup({id:parseInt(e.id,10)||null,group:e,valueKey:t,labelKey:i}):s._addChoice({value:e[t],label:e[i],isSelected:e.selected,isDisabled:e.disabled,customProperties:e.customProperties,placeholder:e.placeholder})})),this._stopLoading(),this},r.clearChoices=function(){return this._store.dispatch({type:U}),this},r.clearStore=function(){return this._store.dispatch({type:\"CLEAR_ALL\"}),this},r.clearInput=function(){var e=!this._isSelectOneElement;return this.input.clear(e),!this._isTextElement&&this._canSearch&&(this._isSearching=!1,this._store.dispatch(ve(!0))),this},r._render=function(){if(!this._store.isLoading()){this._currentState=this._store.state;var e=this._currentState.choices!==this._prevState.choices||this._currentState.groups!==this._prevState.groups||this._currentState.items!==this._prevState.items,t=this._isSelectElement,i=this._currentState.items!==this._prevState.items;e&&(t&&this._renderChoices(),i&&this._renderItems(),this._prevState=this._currentState)}},r._renderChoices=function(){var e=this,t=this._store,i=t.activeGroups,n=t.activeChoices,s=document.createDocumentFragment();if(this.choiceList.clear(),this.config.resetScrollPosition&&requestAnimationFrame((function(){return e.choiceList.scrollToTop()})),i.length>=1&&!this._isSearching){var r=n.filter((function(e){return!0===e.placeholder&&-1===e.groupId}));r.length>=1&&(s=this._createChoicesFragment(r,s)),s=this._createGroupsFragment(i,n,s)}else n.length>=1&&(s=this._createChoicesFragment(n,s));if(s.childNodes&&s.childNodes.length>0){var o=this._store.activeItems,a=this._canAddItem(o,this.input.value);a.response?(this.choiceList.append(s),this._highlightChoice()):this.choiceList.append(this._getTemplate(\"notice\",a.notice))}else{var c,l;this._isSearching?(l=\"function\"==typeof this.config.noResultsText?this.config.noResultsText():this.config.noResultsText,c=this._getTemplate(\"notice\",l,\"no-results\")):(l=\"function\"==typeof this.config.noChoicesText?this.config.noChoicesText():this.config.noChoicesText,c=this._getTemplate(\"notice\",l,\"no-choices\")),this.choiceList.append(c)}},r._renderItems=function(){var e=this._store.activeItems||[];this.itemList.clear();var t=this._createItemsFragment(e);t.childNodes&&this.itemList.append(t)},r._createGroupsFragment=function(e,t,i){var n=this;return void 0===i&&(i=document.createDocumentFragment()),this.config.shouldSort&&e.sort(this.config.sorter),e.forEach((function(e){var s=function(e){return t.filter((function(t){return n._isSelectOneElement?t.groupId===e.id:t.groupId===e.id&&(\"always\"===n.config.renderSelectedChoices||!t.selected)}))}(e);if(s.length>=1){var r=n._getTemplate(\"choiceGroup\",e);i.appendChild(r),n._createChoicesFragment(s,i,!0)}})),i},r._createChoicesFragment=function(e,t,i){var n=this;void 0===t&&(t=document.createDocumentFragment()),void 0===i&&(i=!1);var s=this.config,r=s.renderSelectedChoices,o=s.searchResultLimit,a=s.renderChoiceLimit,c=this._isSearching?w:this.config.sorter,l=function(e){if(\"auto\"!==r||n._isSelectOneElement||!e.selected){var i=n._getTemplate(\"choice\",e,n.config.itemSelectText);t.appendChild(i)}},h=e;\"auto\"!==r||this._isSelectOneElement||(h=e.filter((function(e){return!e.selected})));var u=h.reduce((function(e,t){return t.placeholder?e.placeholderChoices.push(t):e.normalChoices.push(t),e}),{placeholderChoices:[],normalChoices:[]}),d=u.placeholderChoices,p=u.normalChoices;(this.config.shouldSort||this._isSearching)&&p.sort(c);var m=h.length,f=this._isSelectOneElement?[].concat(d,p):p;this._isSearching?m=o:a&&a>0&&!i&&(m=a);for(var v=0;v=n){var o=s?this._searchChoices(e):0;this.passedElement.triggerEvent(j,{value:e,resultCount:o})}else r&&(this._isSearching=!1,this._store.dispatch(ve(!0)))}},r._canAddItem=function(e,t){var i=!0,n=\"function\"==typeof this.config.addItemText?this.config.addItemText(t):this.config.addItemText;if(!this._isSelectOneElement){var s=function(e,t,i){return void 0===i&&(i=\"value\"),e.some((function(e){return\"string\"==typeof t?e[i]===t.trim():e[i]===t}))}(e,t);this.config.maxItemCount>0&&this.config.maxItemCount<=e.length&&(i=!1,n=\"function\"==typeof this.config.maxItemText?this.config.maxItemText(this.config.maxItemCount):this.config.maxItemText),!this.config.duplicateItemsAllowed&&s&&i&&(i=!1,n=\"function\"==typeof this.config.uniqueItemText?this.config.uniqueItemText(t):this.config.uniqueItemText),this._isTextElement&&this.config.addItems&&i&&\"function\"==typeof this.config.addItemFilter&&!this.config.addItemFilter(t)&&(i=!1,n=\"function\"==typeof this.config.customAddItemText?this.config.customAddItemText(t):this.config.customAddItemText)}return{response:i,notice:n}},r._searchChoices=function(e){var t=\"string\"==typeof e?e.trim():e,i=\"string\"==typeof this._currentValue?this._currentValue.trim():this._currentValue;if(t.length<1&&t===i+\" \")return 0;var n=this._store.searchableChoices,r=t,o=[].concat(this.config.searchFields),a=Object.assign(this.config.fuseOptions,{keys:o}),c=new s.a(n,a).search(r);return this._currentValue=t,this._highlightPosition=0,this._isSearching=!0,this._store.dispatch(function(e){return{type:G,results:e}}(c)),c.length},r._addEventListeners=function(){var e=document.documentElement;e.addEventListener(\"touchend\",this._onTouchEnd,!0),this.containerOuter.element.addEventListener(\"keydown\",this._onKeyDown,!0),this.containerOuter.element.addEventListener(\"mousedown\",this._onMouseDown,!0),e.addEventListener(\"click\",this._onClick,{passive:!0}),e.addEventListener(\"touchmove\",this._onTouchMove,{passive:!0}),this.dropdown.element.addEventListener(\"mouseover\",this._onMouseOver,{passive:!0}),this._isSelectOneElement&&(this.containerOuter.element.addEventListener(\"focus\",this._onFocus,{passive:!0}),this.containerOuter.element.addEventListener(\"blur\",this._onBlur,{passive:!0})),this.input.element.addEventListener(\"keyup\",this._onKeyUp,{passive:!0}),this.input.element.addEventListener(\"focus\",this._onFocus,{passive:!0}),this.input.element.addEventListener(\"blur\",this._onBlur,{passive:!0}),this.input.element.form&&this.input.element.form.addEventListener(\"reset\",this._onFormReset,{passive:!0}),this.input.addEventListeners()},r._removeEventListeners=function(){var e=document.documentElement;e.removeEventListener(\"touchend\",this._onTouchEnd,!0),this.containerOuter.element.removeEventListener(\"keydown\",this._onKeyDown,!0),this.containerOuter.element.removeEventListener(\"mousedown\",this._onMouseDown,!0),e.removeEventListener(\"click\",this._onClick),e.removeEventListener(\"touchmove\",this._onTouchMove),this.dropdown.element.removeEventListener(\"mouseover\",this._onMouseOver),this._isSelectOneElement&&(this.containerOuter.element.removeEventListener(\"focus\",this._onFocus),this.containerOuter.element.removeEventListener(\"blur\",this._onBlur)),this.input.element.removeEventListener(\"keyup\",this._onKeyUp),this.input.element.removeEventListener(\"focus\",this._onFocus),this.input.element.removeEventListener(\"blur\",this._onBlur),this.input.element.form&&this.input.element.form.removeEventListener(\"reset\",this._onFormReset),this.input.removeEventListeners()},r._onKeyDown=function(e){var t,i=e.target,n=e.keyCode,s=e.ctrlKey,r=e.metaKey,o=this._store.activeItems,a=this.input.isFocussed,c=this.dropdown.isActive,l=this.itemList.hasChildren(),h=String.fromCharCode(n),u=J,d=Y,p=Z,m=Q,f=ee,v=te,g=ie,_=ne,b=se,y=s||r;!this._isTextElement&&/[a-zA-Z0-9-_ ]/.test(h)&&this.showDropdown();var E=((t={})[m]=this._onAKey,t[p]=this._onEnterKey,t[f]=this._onEscapeKey,t[v]=this._onDirectionKey,t[_]=this._onDirectionKey,t[g]=this._onDirectionKey,t[b]=this._onDirectionKey,t[d]=this._onDeleteKey,t[u]=this._onDeleteKey,t);E[n]&&E[n]({event:e,target:i,keyCode:n,metaKey:r,activeItems:o,hasFocusedInput:a,hasActiveDropdown:c,hasItems:l,hasCtrlDownKeyPressed:y})},r._onKeyUp=function(e){var t=e.target,i=e.keyCode,n=this.input.value,s=this._store.activeItems,r=this._canAddItem(s,n),o=J,a=Y;if(this._isTextElement)if(r.notice&&n){var c=this._getTemplate(\"notice\",r.notice);this.dropdown.element.innerHTML=c.outerHTML,this.showDropdown(!0)}else this.hideDropdown(!0);else{var l=(i===o||i===a)&&!t.value,h=!this._isTextElement&&this._isSearching,u=this._canSearch&&r.response;l&&h?(this._isSearching=!1,this._store.dispatch(ve(!0))):u&&this._handleSearch(this.input.value)}this._canSearch=this.config.searchEnabled},r._onAKey=function(e){var t=e.hasItems;e.hasCtrlDownKeyPressed&&t&&(this._canSearch=!1,this.config.removeItems&&!this.input.value&&this.input.element===document.activeElement&&this.highlightAll())},r._onEnterKey=function(e){var t=e.event,i=e.target,n=e.activeItems,s=e.hasActiveDropdown,r=Z,o=i.hasAttribute(\"data-button\");if(this._isTextElement&&i.value){var a=this.input.value;this._canAddItem(n,a).response&&(this.hideDropdown(!0),this._addItem({value:a}),this._triggerChange(a),this.clearInput())}if(o&&(this._handleButtonAction(n,i),t.preventDefault()),s){var c=this.dropdown.getChild(\".\"+this.config.classNames.highlightedState);c&&(n[0]&&(n[0].keyCode=r),this._handleChoiceAction(n,c)),t.preventDefault()}else this._isSelectOneElement&&(this.showDropdown(),t.preventDefault())},r._onEscapeKey=function(e){e.hasActiveDropdown&&(this.hideDropdown(!0),this.containerOuter.focus())},r._onDirectionKey=function(e){var t,i,n,s=e.event,r=e.hasActiveDropdown,o=e.keyCode,a=e.metaKey,c=ie,l=ne,h=se;if(r||this._isSelectOneElement){this.showDropdown(),this._canSearch=!1;var u,d=o===c||o===h?1:-1;if(a||o===h||o===l)u=d>0?this.dropdown.element.querySelector(\"[data-choice-selectable]:last-of-type\"):this.dropdown.element.querySelector(\"[data-choice-selectable]\");else{var p=this.dropdown.element.querySelector(\".\"+this.config.classNames.highlightedState);u=p?function(e,t,i){if(void 0===i&&(i=1),e instanceof Element&&\"string\"==typeof t){for(var n=(i>0?\"next\":\"previous\")+\"ElementSibling\",s=e[n];s;){if(s.matches(t))return s;s=s[n]}return s}}(p,\"[data-choice-selectable]\",d):this.dropdown.element.querySelector(\"[data-choice-selectable]\")}u&&(t=u,i=this.choiceList.element,void 0===(n=d)&&(n=1),t&&(n>0?i.scrollTop+i.offsetHeight>=t.offsetTop+t.offsetHeight:t.offsetTop>=i.scrollTop)||this.choiceList.scrollToChildElement(u,d),this._highlightChoice(u)),s.preventDefault()}},r._onDeleteKey=function(e){var t=e.event,i=e.target,n=e.hasFocusedInput,s=e.activeItems;!n||i.value||this._isSelectOneElement||(this._handleBackspace(s),t.preventDefault())},r._onTouchMove=function(){this._wasTap&&(this._wasTap=!1)},r._onTouchEnd=function(e){var t=(e||e.touches[0]).target;this._wasTap&&this.containerOuter.element.contains(t)&&((t===this.containerOuter.element||t===this.containerInner.element)&&(this._isTextElement?this.input.focus():this._isSelectMultipleElement&&this.showDropdown()),e.stopPropagation()),this._wasTap=!0},r._onMouseDown=function(e){var t=e.target;if(t instanceof HTMLElement){if(Ee&&this.choiceList.element.contains(t)){var i=this.choiceList.element.firstElementChild,n=\"ltr\"===this._direction?e.offsetX>=i.offsetWidth:e.offsetX0&&this.unhighlightAll(),this.containerOuter.removeFocusState(),this.hideDropdown(!0))},r._onFocus=function(e){var t,i=this,n=e.target;this.containerOuter.element.contains(n)&&((t={}).text=function(){n===i.input.element&&i.containerOuter.addFocusState()},t[\"select-one\"]=function(){i.containerOuter.addFocusState(),n===i.input.element&&i.showDropdown(!0)},t[\"select-multiple\"]=function(){n===i.input.element&&(i.showDropdown(!0),i.containerOuter.addFocusState())},t)[this.passedElement.element.type]()},r._onBlur=function(e){var t=this,i=e.target;if(this.containerOuter.element.contains(i)&&!this._isScrollingOnIe){var n,s=this._store.activeItems.some((function(e){return e.highlighted}));((n={}).text=function(){i===t.input.element&&(t.containerOuter.removeFocusState(),s&&t.unhighlightAll(),t.hideDropdown(!0))},n[\"select-one\"]=function(){t.containerOuter.removeFocusState(),(i===t.input.element||i===t.containerOuter.element&&!t._canSearch)&&t.hideDropdown(!0)},n[\"select-multiple\"]=function(){i===t.input.element&&(t.containerOuter.removeFocusState(),t.hideDropdown(!0),s&&t.unhighlightAll())},n)[this.passedElement.element.type]()}else this._isScrollingOnIe=!1,this.input.element.focus()},r._onFormReset=function(){this._store.dispatch({type:\"RESET_TO\",state:this._initialState})},r._highlightChoice=function(e){var t=this;void 0===e&&(e=null);var i=Array.from(this.dropdown.element.querySelectorAll(\"[data-choice-selectable]\"));if(i.length){var n=e;Array.from(this.dropdown.element.querySelectorAll(\".\"+this.config.classNames.highlightedState)).forEach((function(e){e.classList.remove(t.config.classNames.highlightedState),e.setAttribute(\"aria-selected\",\"false\")})),n?this._highlightPosition=i.indexOf(n):(n=i.length>this._highlightPosition?i[this._highlightPosition]:i[i.length-1])||(n=i[0]),n.classList.add(this.config.classNames.highlightedState),n.setAttribute(\"aria-selected\",\"true\"),this.passedElement.triggerEvent(B,{el:n}),this.dropdown.isActive&&(this.input.setActiveDescendant(n.id),this.containerOuter.setActiveDescendant(n.id))}},r._addItem=function(e){var t=e.value,i=e.label,n=void 0===i?null:i,s=e.choiceId,r=void 0===s?-1:s,o=e.groupId,a=void 0===o?-1:o,c=e.customProperties,l=void 0===c?null:c,h=e.placeholder,u=void 0!==h&&h,d=e.keyCode,p=void 0===d?null:d,m=\"string\"==typeof t?t.trim():t,f=p,v=l,g=this._store.items,_=n||m,b=r||-1,y=a>=0?this._store.getGroupById(a):null,E=g?g.length+1:1;return this.config.prependValue&&(m=this.config.prependValue+m.toString()),this.config.appendValue&&(m+=this.config.appendValue.toString()),this._store.dispatch(function(e){var t=e.value,i=e.label,n=e.id,s=e.choiceId,r=e.groupId,o=e.customProperties,a=e.placeholder,c=e.keyCode;return{type:W,value:t,label:i,id:n,choiceId:s,groupId:r,customProperties:o,placeholder:a,keyCode:c}}({value:m,label:_,id:E,choiceId:b,groupId:a,customProperties:l,placeholder:u,keyCode:f})),this._isSelectOneElement&&this.removeActiveItems(E),this.passedElement.triggerEvent(K,{id:E,value:m,label:_,customProperties:v,groupValue:y&&y.value?y.value:void 0,keyCode:f}),this},r._removeItem=function(e){if(!e||!E(\"Object\",e))return this;var t=e.id,i=e.value,n=e.label,s=e.choiceId,r=e.groupId,o=r>=0?this._store.getGroupById(r):null;return this._store.dispatch(function(e,t){return{type:X,id:e,choiceId:t}}(t,s)),o&&o.value?this.passedElement.triggerEvent(R,{id:t,value:i,label:n,groupValue:o.value}):this.passedElement.triggerEvent(R,{id:t,value:i,label:n}),this},r._addChoice=function(e){var t=e.value,i=e.label,n=void 0===i?null:i,s=e.isSelected,r=void 0!==s&&s,o=e.isDisabled,a=void 0!==o&&o,c=e.groupId,l=void 0===c?-1:c,h=e.customProperties,u=void 0===h?null:h,d=e.placeholder,p=void 0!==d&&d,m=e.keyCode,f=void 0===m?null:m;if(null!=t){var v=this._store.choices,g=n||t,_=v?v.length+1:1,b=this._baseId+\"-\"+this._idNames.itemChoice+\"-\"+_;this._store.dispatch(function(e){var t=e.value,i=e.label,n=e.id,s=e.groupId,r=e.disabled,o=e.elementId,a=e.customProperties,c=e.placeholder,l=e.keyCode;return{type:V,value:t,label:i,id:n,groupId:s,disabled:r,elementId:o,customProperties:a,placeholder:c,keyCode:l}}({id:_,groupId:l,elementId:b,value:t,label:g,disabled:a,customProperties:u,placeholder:p,keyCode:f})),r&&this._addItem({value:t,label:g,choiceId:_,customProperties:u,placeholder:p,keyCode:f})}},r._addGroup=function(e){var t=this,i=e.group,n=e.id,s=e.valueKey,r=void 0===s?\"value\":s,o=e.labelKey,a=void 0===o?\"label\":o,c=E(\"Object\",i)?i.choices:Array.from(i.getElementsByTagName(\"OPTION\")),l=n||Math.floor((new Date).valueOf()*Math.random()),h=!!i.disabled&&i.disabled;c?(this._store.dispatch(_e({value:i.label,id:l,active:!0,disabled:h})),c.forEach((function(e){var i=e.disabled||e.parentNode&&e.parentNode.disabled;t._addChoice({value:e[r],label:E(\"Object\",e)?e[a]:e.innerHTML,isSelected:e.selected,isDisabled:i,groupId:l,customProperties:e.customProperties,placeholder:e.placeholder})}))):this._store.dispatch(_e({value:i.label,id:i.id,active:!1,disabled:i.disabled}))},r._getTemplate=function(e){var t;if(!e)return null;for(var i=this.config.classNames,n=arguments.length,s=new Array(n>1?n-1:0),r=1;rthis.input_el.name=this.model.name||\"\"),this.connect(this.model.properties.value.change,()=>{this.input_el.value=this.format_value,this.old_value=this.input_el.value}),this.connect(this.model.properties.low.change,()=>{const{value:e,low:t,high:l}=this.model;null!=t&&null!=l&&h.assert(t<=l,\"Invalid bounds, low must be inferior to high\"),null!=e&&null!=t&&(this.model.value=Math.max(e,t))}),this.connect(this.model.properties.high.change,()=>{const{value:e,low:t,high:l}=this.model;null!=t&&null!=l&&h.assert(l>=t,\"Invalid bounds, high must be superior to low\"),null!=e&&null!=l&&(this.model.value=Math.min(e,l))}),this.connect(this.model.properties.high.change,()=>this.input_el.placeholder=this.model.placeholder),this.connect(this.model.properties.disabled.change,()=>this.input_el.disabled=this.model.disabled),this.connect(this.model.properties.placeholder.change,()=>this.input_el.placeholder=this.model.placeholder)}get format_value(){return null!=this.model.value?this.model.pretty(this.model.value):\"\"}_set_input_filter(e){this.input_el.addEventListener(\"input\",()=>{const{selectionStart:t,selectionEnd:l}=this.input_el;if(e(this.input_el.value))this.old_value=this.input_el.value;else{const e=this.old_value.length-this.input_el.value.length;this.input_el.value=this.old_value,t&&l&&this.input_el.setSelectionRange(t-1,l+e)}})}render(){super.render(),this.input_el=u.input({type:\"text\",class:r.bk_input,name:this.model.name,value:this.format_value,disabled:this.model.disabled,placeholder:this.model.placeholder}),this.old_value=this.format_value,this.set_input_filter(),this.input_el.addEventListener(\"change\",()=>this.change_input()),this.input_el.addEventListener(\"focusout\",()=>this.input_el.value=this.format_value),this.group_el.appendChild(this.input_el)}set_input_filter(){\"int\"==this.model.mode?this._set_input_filter(e=>d.test(e)):\"float\"==this.model.mode&&this._set_input_filter(e=>p.test(e))}bound_value(e){let t=e;const{low:l,high:i}=this.model;return t=null!=l?Math.max(l,t):t,t=null!=i?Math.min(i,t):t,t}get value(){let e=\"\"!==this.input_el.value?Number(this.input_el.value):null;return null!=e&&(e=this.bound_value(e)),e}change_input(){null==this.value?this.model.value=null:Number.isNaN(this.value)||(this.model.value=this.value)}}l.NumericInputView=_,_.__name__=\"NumericInputView\";class m extends s.InputWidget{constructor(e){super(e)}static init_NumericInput(){this.prototype.default_view=_,this.define({value:[o.Number,null],placeholder:[o.String,\"\"],mode:[o.Any,\"int\"],format:[o.Any],low:[o.Number,null],high:[o.Number,null]})}_formatter(e,t){return a.isString(t)?n.format(e,t):t.doFormat([e],{loc:0})[0]}pretty(e){return null!=this.format?this._formatter(e,this.format):\"\"+e}}l.NumericInput=m,m.__name__=\"NumericInput\",m.init_NumericInput()},\n 442: function _(t,_,r){Object.defineProperty(r,\"__esModule\",{value:!0});const e=t(1);e.__exportStar(t(13),r),e.__exportStar(t(9),r),e.__exportStar(t(29),r),e.__exportStar(t(443),r),e.__exportStar(t(8),r),e.__exportStar(t(25),r)},\n 443: function _(e,t,s){Object.defineProperty(s,\"__esModule\",{value:!0});class n{constructor(e){this.seed=e%2147483647,this.seed<=0&&(this.seed+=2147483646)}integer(){return this.seed=48271*this.seed%2147483647,this.seed}float(){return(this.integer()-1)/2147483646}floats(e){const t=new Array(e);for(let s=0;s{n.classes(o).toggle(s.bk_active,t===e)})}}e.RadioButtonGroupView=_,_.__name__=\"RadioButtonGroupView\";class c extends a.ButtonGroup{constructor(t){super(t)}static init_RadioButtonGroup(){this.prototype.default_view=_,this.define({active:[u.Any,null]})}}e.RadioButtonGroup=c,c.__name__=\"RadioButtonGroup\",c.init_RadioButtonGroup()},\n 446: function _(e,i,t){Object.defineProperty(t,\"__esModule\",{value:!0});const n=e(1),a=e(72),s=e(29),o=n.__importStar(e(18)),d=e(417),l=e(173),p=e(412);class r extends d.InputGroupView{render(){super.render();const e=a.div({class:[p.bk_input_group,this.model.inline?l.bk_inline:null]});this.el.appendChild(e);const i=s.uniqueId(),{active:t,labels:n}=this.model;this._inputs=[];for(let s=0;sthis.change_active(s)),this._inputs.push(o),this.model.disabled&&(o.disabled=!0),s==t&&(o.checked=!0);const d=a.label({},o,a.span({},n[s]));e.appendChild(d)}}change_active(e){this.model.active=e}}t.RadioGroupView=r,r.__name__=\"RadioGroupView\";class u extends d.InputGroup{constructor(e){super(e)}static init_RadioGroup(){this.prototype.default_view=r,this.define({active:[o.Number],labels:[o.Array,[]],inline:[o.Boolean,!1]})}}t.RadioGroup=u,u.__name__=\"RadioGroup\",u.init_RadioGroup()},\n 447: function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=e(1).__importStar(e(188)),a=e(423),n=e(8);class o extends a.AbstractRangeSliderView{}r.RangeSliderView=o,o.__name__=\"RangeSliderView\";class s extends a.AbstractSlider{constructor(e){super(e),this.behaviour=\"drag\",this.connected=[!1,!0,!1]}static init_RangeSlider(){this.prototype.default_view=o,this.override({format:\"0[.]00\"})}_formatter(e,t){return n.isString(t)?i.format(e,t):t.doFormat([e],{loc:0})[0]}}r.RangeSlider=s,s.__name__=\"RangeSlider\",s.init_RangeSlider()},\n 448: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=e(72),l=e(8),o=e(13),p=n.__importStar(e(18)),u=e(410),a=e(412);class _ extends u.InputWidgetView{connect_signals(){super.connect_signals();const{value:e,options:t}=this.model.properties;this.on_change(e,()=>{this._update_value()}),this.on_change(t,()=>{s.empty(this.input_el),s.append(this.input_el,...this.options_el())})}options_el(){function e(e){return e.map(e=>{let t,i;return l.isString(e)?t=i=e:[t,i]=e,s.option({value:t},i)})}const{options:t}=this.model;return l.isArray(t)?e(t):o.entries(t).map(([t,i])=>s.optgroup({label:t},e(i)))}render(){super.render(),this.input_el=s.select({class:a.bk_input,name:this.model.name,disabled:this.model.disabled},this.options_el()),this._update_value(),this.input_el.addEventListener(\"change\",()=>this.change_input()),this.group_el.appendChild(this.input_el)}change_input(){const e=this.input_el.value;this.model.value=e,super.change_input()}_update_value(){const{value:e}=this.model;null!=e&&0!=e.length&&(this.input_el.value=this.model.value)}}i.SelectView=_,_.__name__=\"SelectView\";class h extends u.InputWidget{constructor(e){super(e)}static init_Select(){this.prototype.default_view=_,this.define({value:[p.String,\"\"],options:[p.Any,[]]})}}i.Select=h,h.__name__=\"Select\",h.init_Select()},\n 449: function _(e,t,r){Object.defineProperty(r,\"__esModule\",{value:!0});const i=e(1).__importStar(e(188)),o=e(423),s=e(8);class _ extends o.AbstractSliderView{}r.SliderView=_,_.__name__=\"SliderView\";class a extends o.AbstractSlider{constructor(e){super(e),this.behaviour=\"tap\",this.connected=[!0,!1]}static init_Slider(){this.prototype.default_view=_,this.override({format:\"0[.]00\"})}_formatter(e,t){return s.isString(t)?i.format(e,t):t.doFormat([e],{loc:0})[0]}}r.Slider=a,a.__name__=\"Slider\",a.init_Slider()},\n 450: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const n=e(1),s=e(441),l=n.__importStar(e(18)),r=e(72),{min:o,max:_,floor:a,abs:h}=Math;function u(e){return a(e)!==e?e.toFixed(16).replace(/0+$/,\"\").split(\".\")[1].length:0}class p extends s.NumericInputView{*buttons(){yield this.btn_up_el,yield this.btn_down_el}initialize(){super.initialize(),this._interval=200}connect_signals(){super.connect_signals();const e=this.model.properties;this.on_change(e.disabled,()=>{for(const e of this.buttons())r.toggle_attribute(e,\"disabled\",this.model.disabled)})}render(){super.render(),this.wrapper_el=r.div({class:\"bk-spin-wrapper\"}),this.group_el.replaceChild(this.wrapper_el,this.input_el),this.btn_up_el=r.button({class:\"bk-spin-btn bk-spin-btn-up\"}),this.btn_down_el=r.button({class:\"bk-spin-btn bk-spin-btn-down\"}),this.wrapper_el.appendChild(this.input_el),this.wrapper_el.appendChild(this.btn_up_el),this.wrapper_el.appendChild(this.btn_down_el);for(const e of this.buttons())r.toggle_attribute(e,\"disabled\",this.model.disabled),e.addEventListener(\"mousedown\",e=>this._btn_mouse_down(e)),e.addEventListener(\"mouseup\",()=>this._btn_mouse_up()),e.addEventListener(\"mouseleave\",()=>this._btn_mouse_leave());this.input_el.addEventListener(\"keydown\",e=>this._input_key_down(e)),this.input_el.addEventListener(\"keyup\",()=>this.model.value_throttled=this.model.value),this.input_el.addEventListener(\"wheel\",e=>this._input_mouse_wheel(e)),this.input_el.addEventListener(\"wheel\",function(e,t,i=!1){let n;return function(...s){const l=this,r=i&&void 0===n;void 0!==n&&clearTimeout(n),n=setTimeout((function(){n=void 0,i||e.apply(l,s)}),t),r&&e.apply(l,s)}}(()=>{this.model.value_throttled=this.model.value},this.model.wheel_wait,!1))}get precision(){const{low:e,high:t,step:i}=this.model;return _(...[e,t,i].map(h).reduce((e,t)=>(null!=t&&e.push(t),e),[]).map(u))}_start_incrementation(e){clearInterval(this._interval_handle),this._counter=0;const{step:t}=this.model,i=e=>{if(this._counter+=1,this._counter%5==0){const t=Math.floor(this._counter/5);t<10?(clearInterval(this._interval_handle),this._interval_handle=setInterval(()=>i(e),this._interval/(t+1))):t>=10&&t<=13&&(clearInterval(this._interval_handle),this._interval_handle=setInterval(()=>i(2*e),this._interval/10))}this.increment(e)};this._interval_handle=setInterval(()=>i(e*t),this._interval)}_stop_incrementation(){clearInterval(this._interval_handle),this.model.value_throttled=this.model.value}_btn_mouse_down(e){e.preventDefault();const t=e.currentTarget===this.btn_up_el?1:-1;this.increment(t*this.model.step),this.input_el.focus(),this._start_incrementation(t)}_btn_mouse_up(){this._stop_incrementation()}_btn_mouse_leave(){this._stop_incrementation()}_input_mouse_wheel(e){if(document.activeElement===this.input_el){e.preventDefault();const t=e.deltaY>0?-1:1;this.increment(t*this.model.step)}}_input_key_down(e){switch(e.keyCode){case r.Keys.Up:return e.preventDefault(),this.increment(this.model.step);case r.Keys.Down:return e.preventDefault(),this.increment(-this.model.step);case r.Keys.PageUp:return e.preventDefault(),this.increment(this.model.page_step_multiplier*this.model.step);case r.Keys.PageDown:return e.preventDefault(),this.increment(-this.model.page_step_multiplier*this.model.step)}}adjust_to_precision(e){return this.bound_value(Number(e.toFixed(this.precision)))}increment(e){const{low:t,high:i}=this.model;null==this.model.value?e>0?this.model.value=null!=t?t:null!=i?o(0,i):0:e<0&&(this.model.value=null!=i?i:null!=t?_(t,0):0):this.model.value=this.adjust_to_precision(this.model.value+e)}change_input(){super.change_input(),this.model.value_throttled=this.model.value}}i.SpinnerView=p,p.__name__=\"SpinnerView\";class d extends s.NumericInput{constructor(e){super(e)}static init_Spinner(){this.prototype.default_view=p,this.define({value_throttled:[l.Number,null],step:[l.Number,1],page_step_multiplier:[l.Number,10],wheel_wait:[l.Number,100]}),this.override({mode:\"float\"})}}i.Spinner=d,d.__name__=\"Spinner\",d.init_Spinner()},\n 451: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),n=e(410),l=e(72),h=s.__importStar(e(18)),o=e(412);class a extends n.InputWidgetView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.name.change,()=>this.input_el.name=this.model.name||\"\"),this.connect(this.model.properties.value.change,()=>this.input_el.value=this.model.value),this.connect(this.model.properties.disabled.change,()=>this.input_el.disabled=this.model.disabled),this.connect(this.model.properties.placeholder.change,()=>this.input_el.placeholder=this.model.placeholder),this.connect(this.model.properties.rows.change,()=>this.input_el.rows=this.model.rows),this.connect(this.model.properties.cols.change,()=>this.input_el.cols=this.model.cols),this.connect(this.model.properties.max_length.change,()=>this.input_el.maxLength=this.model.max_length)}render(){super.render(),this.input_el=l.textarea({class:o.bk_input,name:this.model.name,disabled:this.model.disabled,placeholder:this.model.placeholder,cols:this.model.cols,rows:this.model.rows,maxLength:this.model.max_length}),this.input_el.textContent=this.model.value,this.input_el.addEventListener(\"change\",()=>this.change_input()),this.group_el.appendChild(this.input_el)}change_input(){this.model.value=this.input_el.value,super.change_input()}}i.TextAreaInputView=a,a.__name__=\"TextAreaInputView\";class p extends n.InputWidget{constructor(e){super(e)}static init_TextAreaInput(){this.prototype.default_view=a,this.define({value:[h.String,\"\"],value_input:[h.String,\"\"],placeholder:[h.String,\"\"],cols:[h.Number,20],rows:[h.Number,2],max_length:[h.Number,500]})}}i.TextAreaInput=p,p.__name__=\"TextAreaInput\",p.init_TextAreaInput()},\n 452: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),c=e(404),o=e(72),a=s.__importStar(e(18)),n=e(173);class l extends c.AbstractButtonView{connect_signals(){super.connect_signals(),this.connect(this.model.properties.active.change,()=>this._update_active())}render(){super.render(),this._update_active()}click(){this.model.active=!this.model.active,super.click()}_update_active(){o.classes(this.button_el).toggle(n.bk_active,this.model.active)}}i.ToggleView=l,l.__name__=\"ToggleView\";class _ extends c.AbstractButton{constructor(e){super(e)}static init_Toggle(){this.prototype.default_view=l,this.define({active:[a.Boolean,!1]}),this.override({label:\"Toggle\"})}}i.Toggle=_,_.__name__=\"Toggle\",_.init_Toggle()},\n }, 402, {\"models/widgets/main\":402,\"models/widgets/index\":403,\"models/widgets/abstract_button\":404,\"models/widgets/control\":405,\"models/widgets/widget\":472,\"models/widgets/abstract_icon\":407,\"models/widgets/autocomplete_input\":408,\"models/widgets/text_input\":409,\"models/widgets/input_widget\":410,\"styles/widgets/inputs.css\":411,\"styles/widgets/inputs\":412,\"models/widgets/button\":413,\"models/widgets/checkbox_button_group\":414,\"models/widgets/button_group\":415,\"models/widgets/checkbox_group\":416,\"models/widgets/input_group\":417,\"models/widgets/color_picker\":418,\"models/widgets/date_picker\":419,\"styles/widgets/flatpickr.css\":421,\"models/widgets/date_range_slider\":422,\"models/widgets/abstract_slider\":423,\"styles/widgets/sliders\":425,\"styles/widgets/nouislider.css\":426,\"styles/widgets/sliders.css\":427,\"models/widgets/date_slider\":428,\"models/widgets/div\":429,\"models/widgets/markup\":430,\"styles/clearfix\":431,\"styles/clearfix.css\":432,\"models/widgets/dropdown\":433,\"models/widgets/file_input\":434,\"models/widgets/multiselect\":435,\"models/widgets/paragraph\":436,\"models/widgets/password_input\":437,\"models/widgets/multichoice\":438,\"styles/widgets/choices.css\":440,\"models/widgets/numeric_input\":441,\"api/linalg\":442,\"core/util/random\":443,\"models/widgets/pretext\":444,\"models/widgets/radio_button_group\":445,\"models/widgets/radio_group\":446,\"models/widgets/range_slider\":447,\"models/widgets/selectbox\":448,\"models/widgets/slider\":449,\"models/widgets/spinner\":450,\"models/widgets/textarea_input\":451,\"models/widgets/toggle\":452}, {});\n })\n\n\n /* END bokeh-widgets.min.js */\n },\n \n function(Bokeh) {\n /* BEGIN bokeh-tables.min.js */\n /*!\n * Copyright (c) 2012 - 2020, Anaconda, Inc., and Bokeh Contributors\n * All rights reserved.\n * \n * Redistribution and use in source and binary forms, with or without modification,\n * are permitted provided that the following conditions are met:\n * \n * Redistributions of source code must retain the above copyright notice,\n * this list of conditions and the following disclaimer.\n * \n * Redistributions in binary form must reproduce the above copyright notice,\n * this list of conditions and the following disclaimer in the documentation\n * and/or other materials provided with the distribution.\n * \n * Neither the name of Anaconda nor the names of any contributors\n * may be used to endorse or promote products derived from this software\n * without specific prior written permission.\n * \n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\n * THE POSSIBILITY OF SUCH DAMAGE.\n */\n (function(root, factory) {\n factory(root[\"Bokeh\"], \"2.2.3\");\n })(this, function(Bokeh, version) {\n var define;\n return (function(modules, entry, aliases, externals) {\n const bokeh = typeof Bokeh !== \"undefined\" && (version != null ? Bokeh[version] : Bokeh);\n if (bokeh != null) {\n return bokeh.register_plugin(modules, entry, aliases);\n } else {\n throw new Error(\"Cannot find Bokeh \" + version + \". You have to load it prior to loading plugins.\");\n }\n })\n ({\n 453: function _(e,t,o){Object.defineProperty(o,\"__esModule\",{value:!0});const r=e(1).__importStar(e(454));o.Tables=r;e(7).register_models(r)},\n 454: function _(a,g,r){Object.defineProperty(r,\"__esModule\",{value:!0});const e=a(1);e.__exportStar(a(455),r),e.__exportStar(a(475),r);var t=a(456);r.DataTable=t.DataTable;var o=a(474);r.TableColumn=o.TableColumn;var n=a(473);r.TableWidget=n.TableWidget;var u=a(481);r.AvgAggregator=u.AvgAggregator,r.MinAggregator=u.MinAggregator,r.MaxAggregator=u.MaxAggregator,r.SumAggregator=u.SumAggregator;var l=a(482);r.GroupingInfo=l.GroupingInfo,r.DataCube=l.DataCube},\n 455: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1).__importStar(e(18)),r=e(72),a=e(78),n=e(81),l=e(456),u=e(478);class d extends a.DOMView{constructor(e){const{model:t,parent:i}=e.column;super(Object.assign({model:t,parent:i},e)),this.args=e,this.initialize(),this.render()}get emptyValue(){return null}initialize(){super.initialize(),this.inputEl=this._createInput(),this.defaultValue=null}async lazy_initialize(){throw new Error(\"unsupported\")}css_classes(){return super.css_classes().concat(u.bk_cell_editor)}render(){super.render(),this.args.container.append(this.el),this.el.appendChild(this.inputEl),this.renderEditor(),this.disableNavigation()}renderEditor(){}disableNavigation(){this.inputEl.addEventListener(\"keydown\",e=>{switch(e.keyCode){case r.Keys.Left:case r.Keys.Right:case r.Keys.Up:case r.Keys.Down:case r.Keys.PageUp:case r.Keys.PageDown:e.stopImmediatePropagation()}})}destroy(){this.remove()}focus(){this.inputEl.focus()}show(){}hide(){}position(){}getValue(){return this.inputEl.value}setValue(e){this.inputEl.value=e}serializeValue(){return this.getValue()}isValueChanged(){return!(\"\"==this.getValue()&&null==this.defaultValue)&&this.getValue()!==this.defaultValue}applyValue(e,t){const i=this.args.grid.getData(),s=i.index.indexOf(e[l.DTINDEX_NAME]);i.setField(s,this.args.column.field,t)}loadValue(e){const t=e[this.args.column.field];this.defaultValue=null!=t?t:this.emptyValue,this.setValue(this.defaultValue)}validateValue(e){if(this.args.column.validator){const t=this.args.column.validator(e);if(!t.valid)return t}return{valid:!0,msg:null}}validate(){return this.validateValue(this.getValue())}}i.CellEditorView=d,d.__name__=\"CellEditorView\";class o extends n.Model{}i.CellEditor=o,o.__name__=\"CellEditor\";class _ extends d{get emptyValue(){return\"\"}_createInput(){return r.input({type:\"text\"})}renderEditor(){this.inputEl.focus(),this.inputEl.select()}loadValue(e){super.loadValue(e),this.inputEl.defaultValue=this.defaultValue,this.inputEl.select()}}i.StringEditorView=_,_.__name__=\"StringEditorView\";class c extends o{static init_StringEditor(){this.prototype.default_view=_,this.define({completions:[s.Array,[]]})}}i.StringEditor=c,c.__name__=\"StringEditor\",c.init_StringEditor();class p extends d{_createInput(){return r.textarea()}renderEditor(){this.inputEl.focus(),this.inputEl.select()}}i.TextEditorView=p,p.__name__=\"TextEditorView\";class h extends o{static init_TextEditor(){this.prototype.default_view=p}}i.TextEditor=h,h.__name__=\"TextEditor\",h.init_TextEditor();class E extends d{_createInput(){return r.select()}renderEditor(){for(const e of this.model.options)this.inputEl.appendChild(r.option({value:e},e));this.focus()}}i.SelectEditorView=E,E.__name__=\"SelectEditorView\";class V extends o{static init_SelectEditor(){this.prototype.default_view=E,this.define({options:[s.Array,[]]})}}i.SelectEditor=V,V.__name__=\"SelectEditor\",V.init_SelectEditor();class m extends d{_createInput(){return r.input({type:\"text\"})}}i.PercentEditorView=m,m.__name__=\"PercentEditorView\";class f extends o{static init_PercentEditor(){this.prototype.default_view=m}}i.PercentEditor=f,f.__name__=\"PercentEditor\",f.init_PercentEditor();class x extends d{_createInput(){return r.input({type:\"checkbox\"})}renderEditor(){this.focus()}loadValue(e){this.defaultValue=!!e[this.args.column.field],this.inputEl.checked=this.defaultValue}serializeValue(){return this.inputEl.checked}}i.CheckboxEditorView=x,x.__name__=\"CheckboxEditorView\";class w extends o{static init_CheckboxEditor(){this.prototype.default_view=x}}i.CheckboxEditor=w,w.__name__=\"CheckboxEditor\",w.init_CheckboxEditor();class g extends d{_createInput(){return r.input({type:\"text\"})}renderEditor(){this.inputEl.focus(),this.inputEl.select()}remove(){super.remove()}serializeValue(){return parseInt(this.getValue(),10)||0}loadValue(e){super.loadValue(e),this.inputEl.defaultValue=this.defaultValue,this.inputEl.select()}validateValue(e){return isNaN(e)?{valid:!1,msg:\"Please enter a valid integer\"}:super.validateValue(e)}}i.IntEditorView=g,g.__name__=\"IntEditorView\";class y extends o{static init_IntEditor(){this.prototype.default_view=g,this.define({step:[s.Number,1]})}}i.IntEditor=y,y.__name__=\"IntEditor\",y.init_IntEditor();class v extends d{_createInput(){return r.input({type:\"text\"})}renderEditor(){this.inputEl.focus(),this.inputEl.select()}remove(){super.remove()}serializeValue(){return parseFloat(this.getValue())||0}loadValue(e){super.loadValue(e),this.inputEl.defaultValue=this.defaultValue,this.inputEl.select()}validateValue(e){return isNaN(e)?{valid:!1,msg:\"Please enter a valid number\"}:super.validateValue(e)}}i.NumberEditorView=v,v.__name__=\"NumberEditorView\";class b extends o{static init_NumberEditor(){this.prototype.default_view=v,this.define({step:[s.Number,.01]})}}i.NumberEditor=b,b.__name__=\"NumberEditor\",b.init_NumberEditor();class I extends d{_createInput(){return r.input({type:\"text\"})}}i.TimeEditorView=I,I.__name__=\"TimeEditorView\";class N extends o{static init_TimeEditor(){this.prototype.default_view=I}}i.TimeEditor=N,N.__name__=\"TimeEditor\",N.init_TimeEditor();class C extends d{_createInput(){return r.input({type:\"text\"})}get emptyValue(){return new Date}renderEditor(){this.inputEl.focus(),this.inputEl.select()}destroy(){super.destroy()}show(){super.show()}hide(){super.hide()}position(){return super.position()}getValue(){}setValue(e){}}i.DateEditorView=C,C.__name__=\"DateEditorView\";class D extends o{static init_DateEditor(){this.prototype.default_view=C}}i.DateEditor=D,D.__name__=\"DateEditor\",D.init_DateEditor()},\n 456: function _(e,t,i){Object.defineProperty(i,\"__esModule\",{value:!0});const s=e(1),o=e(457),n=e(461),l=e(462),r=e(463),d=e(29),a=e(8),h=e(9),u=e(13),c=e(19),_=e(472),m=e(473),g=e(474),p=e(478),f=s.__importDefault(e(479)),b=s.__importDefault(e(480));i.DTINDEX_NAME=\"__bkdt_internal_index__\",i.AutosizeModes={fit_columns:\"FCV\",fit_viewport:\"FVC\",force_fit:\"LFF\",none:\"NOA\"};class w{constructor(e,t){this.init(e,t)}init(e,t){if(i.DTINDEX_NAME in e.data)throw new Error(`special name ${i.DTINDEX_NAME} cannot be used as a data table column`);this.source=e,this.view=t,this.index=[...this.view.indices]}getLength(){return this.index.length}getItem(e){const t={};for(const i of u.keys(this.source.data))t[i]=this.source.data[i][this.index[e]];return t[i.DTINDEX_NAME]=this.index[e],t}getField(e,t){return t==i.DTINDEX_NAME?this.index[e]:this.source.data[t][this.index[e]]}setField(e,t,i){const s=this.index[e];this.source.patch({[t]:[[s,i]]})}getRecords(){return h.range(0,this.getLength()).map(e=>this.getItem(e))}getItems(){return this.getRecords()}slice(e,t,i){return e=null!=e?e:0,t=null!=t?t:this.getLength(),i=null!=i?i:1,h.range(e,t,i).map(e=>this.getItem(e))}sort(e){let t=e.map(e=>[e.sortCol.field,e.sortAsc?1:-1]);0==t.length&&(t=[[i.DTINDEX_NAME,1]]);const s=this.getRecords(),o=this.index.slice();this.index.sort((e,i)=>{for(const[n,l]of t){const t=s[o.indexOf(e)][n],r=s[o.indexOf(i)][n];if(t!==r)return a.isNumber(t)&&a.isNumber(r)?l*(t-r||+isNaN(t)-+isNaN(r)):\"\"+t>\"\"+r?l:-l}return 0})}}i.TableDataProvider=w,w.__name__=\"TableDataProvider\";class x extends _.WidgetView{constructor(){super(...arguments),this._in_selection_update=!1,this._warned_not_reorderable=!1,this._width=null}connect_signals(){super.connect_signals(),this.connect(this.model.change,()=>this.render()),this.connect(this.model.source.streaming,()=>this.updateGrid()),this.connect(this.model.source.patching,()=>this.updateGrid()),this.connect(this.model.source.change,()=>this.updateGrid()),this.connect(this.model.source.properties.data.change,()=>this.updateGrid()),this.connect(this.model.source.selected.change,()=>this.updateSelection()),this.connect(this.model.source.selected.properties.indices.change,()=>this.updateSelection())}remove(){var e;null===(e=this.grid)||void 0===e||e.destroy(),super.remove()}styles(){return[...super.styles(),f.default,b.default]}update_position(){super.update_position(),this.grid.resizeCanvas()}after_layout(){super.after_layout(),this.updateLayout(!0,!1)}box_sizing(){const e=super.box_sizing();return\"fit_viewport\"===this.model.autosize_mode&&null!=this._width&&(e.width=this._width),e}updateLayout(e,t){const s=this.autosize;s===i.AutosizeModes.fit_columns||s===i.AutosizeModes.force_fit?(e||this.grid.resizeCanvas(),this.grid.autosizeColumns()):e&&t&&s===i.AutosizeModes.fit_viewport&&this.invalidate_layout()}updateGrid(){if(this.model.view.compute_indices(),this.data.init(this.model.source,this.model.view),this.model.sortable){const e=this.grid.getColumns(),t=this.grid.getSortColumns().map(t=>({sortCol:{field:e[this.grid.getColumnIndex(t.columnId)].field},sortAsc:t.sortAsc}));this.data.sort(t)}this.grid.invalidate(),this.updateLayout(!0,!0)}updateSelection(){if(this._in_selection_update)return;const{selected:e}=this.model.source,t=e.indices.map(e=>this.data.index.indexOf(e)).sort();this._in_selection_update=!0,this.grid.setSelectedRows(t),this._in_selection_update=!1;const i=this.grid.getViewport(),s=this.model.get_scroll_index(i,t);null!=s&&this.grid.scrollRowToTop(s)}newIndexColumn(){return{id:d.uniqueId(),name:this.model.index_header,field:i.DTINDEX_NAME,width:this.model.index_width,behavior:\"select\",cannotTriggerInsert:!0,resizable:!1,selectable:!1,sortable:!0,cssClass:p.bk_cell_index,headerCssClass:p.bk_header_index}}css_classes(){return super.css_classes().concat(p.bk_data_table)}get autosize(){let e;return e=!0===this.model.fit_columns?i.AutosizeModes.force_fit:!1===this.model.fit_columns?i.AutosizeModes.none:i.AutosizeModes[this.model.autosize_mode],e}render(){var e;const t=this.model.columns.map(e=>Object.assign(Object.assign({},e.toColumn()),{parent:this}));let s=null;if(\"checkbox\"==this.model.selectable&&(s=new n.CheckboxSelectColumn({cssClass:p.bk_cell_select}),t.unshift(s.getColumnDefinition())),null!=this.model.index_position){const e=this.model.index_position,i=this.newIndexColumn();-1==e?t.push(i):e<-1?t.splice(e+1,0,i):t.splice(e,0,i)}let{reorderable:d}=this.model;!d||\"undefined\"!=typeof $&&null!=$.fn&&null!=$.fn.sortable||(this._warned_not_reorderable||(c.logger.warn(\"jquery-ui is required to enable DataTable.reorderable\"),this._warned_not_reorderable=!0),d=!1);let h=-1,u=!1;const{frozen_rows:_,frozen_columns:m}=this.model,g=null==m?-1:m-1;null!=_&&(u=_<0,h=Math.abs(_));const f={enableCellNavigation:!1!==this.model.selectable,enableColumnReorder:d,autosizeColsMode:this.autosize,multiColumnSort:this.model.sortable,editable:this.model.editable,autoEdit:this.model.auto_edit,autoHeight:!1,rowHeight:this.model.row_height,frozenColumn:g,frozenRow:h,frozenBottom:u},b=null!=this.grid;if(this.data=new w(this.model.source,this.model.view),this.grid=new r.Grid(this.el,this.data,t,f),this.autosize==i.AutosizeModes.fit_viewport){this.grid.autosizeColumns();let i=0;for(const s of t)i+=null!==(e=s.width)&&void 0!==e?e:0;this._width=Math.ceil(i)}if(this.grid.onSort.subscribe((e,t)=>{if(!this.model.sortable)return;const i=t.sortCols;null!=i&&(this.data.sort(i),this.grid.invalidate(),this.updateSelection(),this.grid.render(),this.model.header_row||this._hide_header(),this.model.update_sort_columns(i))}),!1!==this.model.selectable){this.grid.setSelectionModel(new o.RowSelectionModel({selectActiveRow:null==s})),null!=s&&this.grid.registerPlugin(s);const e={dataItemColumnValueExtractor(e,t){let i=e[t.field];return a.isString(i)&&(i=i.replace(/\\n/g,\"\\\\n\")),i},includeHeaderWhenCopying:!1};this.grid.registerPlugin(new l.CellExternalCopyManager(e)),this.grid.onSelectedRowsChanged.subscribe((e,t)=>{this._in_selection_update||(this.model.source.selected.indices=t.rows.map(e=>this.data.index[e]))}),this.updateSelection(),this.model.header_row||this._hide_header()}b&&this.updateLayout(b,!1)}_hide_header(){for(const e of this.el.querySelectorAll(\".slick-header-columns\"))e.style.height=\"0px\";this.grid.resizeCanvas()}}i.DataTableView=x,x.__name__=\"DataTableView\";class C extends m.TableWidget{constructor(e){super(e),this._sort_columns=[]}get sort_columns(){return this._sort_columns}static init_DataTable(){this.prototype.default_view=x,this.define(({Array:e,Boolean:t,Int:i,Ref:s,String:o,Enum:n,Or:l,Null:r})=>({autosize_mode:[n(\"fit_columns\",\"fit_viewport\",\"none\",\"force_fit\"),\"force_fit\"],auto_edit:[t,!1],columns:[e(s(g.TableColumn)),[]],fit_columns:[l(t,r),null],frozen_columns:[l(i,r),null],frozen_rows:[l(i,r),null],sortable:[t,!0],reorderable:[t,!0],editable:[t,!1],selectable:[l(t,n(\"checkbox\")),!0],index_position:[l(i,r),0],index_header:[o,\"#\"],index_width:[i,40],scroll_to_selection:[t,!0],header_row:[t,!0],row_height:[i,25]})),this.override({width:600,height:400})}update_sort_columns(e){this._sort_columns=e.map(({sortCol:e,sortAsc:t})=>({field:e.field,sortAsc:t}))}get_scroll_index(e,t){return this.scroll_to_selection&&0!=t.length?h.some(t,t=>e.top<=t&&t<=e.bottom)?null:Math.max(0,Math.min(...t)-1):null}}i.DataTable=C,C.__name__=\"DataTable\",C.init_DataTable()},\n 457: function _(e,t,n){var o=e(458),r=e(460);t.exports={RowSelectionModel:function(e){var t,n,l,i=[],c=this,u=new r.EventHandler,s={selectActiveRow:!0};function a(e){return function(){n||(n=!0,e.apply(this,arguments),n=!1)}}function f(e){for(var t=[],n=0;n=0&&l0&&t-1 in e)}b.fn=b.prototype={jquery:\"3.5.1\",constructor:b,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=b.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return b.each(this,e)},map:function(e){return this.pushStack(b.map(this,(function(t,n){return e.call(t,n,t)})))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(b.grep(this,(function(e,t){return(t+1)%2})))},odd:function(){return this.pushStack(b.grep(this,(function(e,t){return t%2})))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|\"+M+\")\"+M+\"*\"),U=new RegExp(M+\"|>\"),X=new RegExp(F),V=new RegExp(\"^\"+I+\"$\"),G={ID:new RegExp(\"^#(\"+I+\")\"),CLASS:new RegExp(\"^\\\\.(\"+I+\")\"),TAG:new RegExp(\"^(\"+I+\"|[*])\"),ATTR:new RegExp(\"^\"+W),PSEUDO:new RegExp(\"^\"+F),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+M+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+M+\"*(?:([+-]|)\"+M+\"*(\\\\d+)|))\"+M+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+R+\")$\",\"i\"),needsContext:new RegExp(\"^\"+M+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+M+\"*((?:-\\\\d)?\\\\d*)\"+M+\"*\\\\)|)(?=[^-]|$)\",\"i\")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\\d$/i,K=/^[^{]+\\{\\s*\\[native \\w/,Z=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,ee=/[+~]/,te=new RegExp(\"\\\\\\\\[\\\\da-fA-F]{1,6}\"+M+\"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\",\"g\"),ne=function(e,t){var n=\"0x\"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,ie=function(e,t){return t?\"\\0\"===e?\"οΏ½\":e.slice(0,-1)+\"\\\\\"+e.charCodeAt(e.length-1).toString(16)+\" \":\"\\\\\"+e},oe=function(){p()},ae=be((function(e){return!0===e.disabled&&\"fieldset\"===e.nodeName.toLowerCase()}),{dir:\"parentNode\",next:\"legend\"});try{H.apply(j=O.call(w.childNodes),w.childNodes),j[w.childNodes.length].nodeType}catch(e){H={apply:j.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function se(e,t,r,i){var o,s,l,c,f,h,y,m=t&&t.ownerDocument,w=t?t.nodeType:9;if(r=r||[],\"string\"!=typeof e||!e||1!==w&&9!==w&&11!==w)return r;if(!i&&(p(t),t=t||d,g)){if(11!==w&&(f=Z.exec(e)))if(o=f[1]){if(9===w){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return H.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return H.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!A[e+\" \"]&&(!v||!v.test(e))&&(1!==w||\"object\"!==t.nodeName.toLowerCase())){if(y=e,m=t,1===w&&(U.test(e)||z.test(e))){for((m=ee.test(e)&&ye(t.parentNode)||t)===t&&n.scope||((c=t.getAttribute(\"id\"))?c=c.replace(re,ie):t.setAttribute(\"id\",c=b)),s=(h=a(e)).length;s--;)h[s]=(c?\"#\"+c:\":scope\")+\" \"+xe(h[s]);y=h.join(\",\")}try{return H.apply(r,m.querySelectorAll(y)),r}catch(t){A(e,!0)}finally{c===b&&t.removeAttribute(\"id\")}}}return u(e.replace($,\"$1\"),t,r,i)}function ue(){var e=[];return function t(n,i){return e.push(n+\" \")>r.cacheLength&&delete t[e.shift()],t[n+\" \"]=i}}function le(e){return e[b]=!0,e}function ce(e){var t=d.createElement(\"fieldset\");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){for(var n=e.split(\"|\"),i=n.length;i--;)r.attrHandle[n[i]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function de(e){return function(t){return\"input\"===t.nodeName.toLowerCase()&&t.type===e}}function he(e){return function(t){var n=t.nodeName.toLowerCase();return(\"input\"===n||\"button\"===n)&&t.type===e}}function ge(e){return function(t){return\"form\"in t?t.parentNode&&!1===t.disabled?\"label\"in t?\"label\"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ae(t)===e:t.disabled===e:\"label\"in t&&t.disabled===e}}function ve(e){return le((function(t){return t=+t,le((function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))}))}))}function ye(e){return e&&void 0!==e.getElementsByTagName&&e}for(t in n=se.support={},o=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||\"HTML\")},p=se.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!=d&&9===a.nodeType&&a.documentElement?(h=(d=a).documentElement,g=!o(d),w!=d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener(\"unload\",oe,!1):i.attachEvent&&i.attachEvent(\"onunload\",oe)),n.scope=ce((function(e){return h.appendChild(e).appendChild(d.createElement(\"div\")),void 0!==e.querySelectorAll&&!e.querySelectorAll(\":scope fieldset div\").length})),n.attributes=ce((function(e){return e.className=\"i\",!e.getAttribute(\"className\")})),n.getElementsByTagName=ce((function(e){return e.appendChild(d.createComment(\"\")),!e.getElementsByTagName(\"*\").length})),n.getElementsByClassName=K.test(d.getElementsByClassName),n.getById=ce((function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length})),n.getById?(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute(\"id\")===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode(\"id\");return n&&n.value===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode(\"id\"))&&n.value===e)return[o];for(i=t.getElementsByName(e),r=0;o=i[r++];)if((n=o.getAttributeNode(\"id\"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if(\"*\"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},y=[],v=[],(n.qsa=K.test(d.querySelectorAll))&&(ce((function(e){var t;h.appendChild(e).innerHTML=\"\",e.querySelectorAll(\"[msallowcapture^='']\").length&&v.push(\"[*^$]=\"+M+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\"[selected]\").length||v.push(\"\\\\[\"+M+\"*(?:value|\"+R+\")\"),e.querySelectorAll(\"[id~=\"+b+\"-]\").length||v.push(\"~=\"),(t=d.createElement(\"input\")).setAttribute(\"name\",\"\"),e.appendChild(t),e.querySelectorAll(\"[name='']\").length||v.push(\"\\\\[\"+M+\"*name\"+M+\"*=\"+M+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\":checked\").length||v.push(\":checked\"),e.querySelectorAll(\"a#\"+b+\"+*\").length||v.push(\".#.+[+~]\"),e.querySelectorAll(\"\\\\\\f\"),v.push(\"[\\\\r\\\\n\\\\f]\")})),ce((function(e){e.innerHTML=\"\";var t=d.createElement(\"input\");t.setAttribute(\"type\",\"hidden\"),e.appendChild(t).setAttribute(\"name\",\"D\"),e.querySelectorAll(\"[name=d]\").length&&v.push(\"name\"+M+\"*[*^$|!~]?=\"),2!==e.querySelectorAll(\":enabled\").length&&v.push(\":enabled\",\":disabled\"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(\":disabled\").length&&v.push(\":enabled\",\":disabled\"),e.querySelectorAll(\"*,:x\"),v.push(\",.*:\")}))),(n.matchesSelector=K.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ce((function(e){n.disconnectedMatch=m.call(e,\"*\"),m.call(e,\"[s!='']:x\"),y.push(\"!=\",F)})),v=v.length&&new RegExp(v.join(\"|\")),y=y.length&&new RegExp(y.join(\"|\")),t=K.test(h.compareDocumentPosition),x=t||K.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},N=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e==d||e.ownerDocument==w&&x(w,e)?-1:t==d||t.ownerDocument==w&&x(w,t)?1:c?P(c,e)-P(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==d?-1:t==d?1:i?-1:o?1:c?P(c,e)-P(c,t):0;if(i===o)return pe(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?pe(a[r],s[r]):a[r]==w?-1:s[r]==w?1:0},d):d},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(p(e),n.matchesSelector&&g&&!A[t+\" \"]&&(!y||!y.test(t))&&(!v||!v.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){A(t,!0)}return se(t,d,null,[e]).length>0},se.contains=function(e,t){return(e.ownerDocument||e)!=d&&p(e),x(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&D.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},se.escape=function(e){return(e+\"\").replace(re,ie)},se.error=function(e){throw new Error(\"Syntax error, unrecognized expression: \"+e)},se.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(N),f){for(;t=e[o++];)t===e[o]&&(i=r.push(o));for(;i--;)e.splice(r[i],1)}return c=null,e},i=se.getText=function(e){var t,n=\"\",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if(\"string\"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r++];)n+=i(t);return n},(r=se.selectors={cacheLength:50,createPseudo:le,match:G,attrHandle:{},find:{},relative:{\">\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||\"\").replace(te,ne),\"~=\"===e[2]&&(e[3]=\" \"+e[3]+\" \"),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),\"nth\"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*(\"even\"===e[3]||\"odd\"===e[3])),e[5]=+(e[7]+e[8]||\"odd\"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||\"\":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(\")\",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return\"*\"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+\" \"];return t||(t=new RegExp(\"(^|\"+M+\")\"+e+\"(\"+M+\"|$)\"))&&E(e,(function(e){return t.test(\"string\"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(\"class\")||\"\")}))},ATTR:function(e,t,n){return function(r){var i=se.attr(r,e);return null==i?\"!=\"===t:!t||(i+=\"\",\"=\"===t?i===n:\"!=\"===t?i!==n:\"^=\"===t?n&&0===i.indexOf(n):\"*=\"===t?n&&i.indexOf(n)>-1:\"$=\"===t?n&&i.slice(-n.length)===n:\"~=\"===t?(\" \"+i.replace(B,\" \")+\" \").indexOf(n)>-1:\"|=\"===t&&(i===n||i.slice(0,n.length+1)===n+\"-\"))}},CHILD:function(e,t,n,r,i){var o=\"nth\"!==e.slice(0,3),a=\"last\"!==e.slice(-4),s=\"of-type\"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?\"nextSibling\":\"previousSibling\",v=t.parentNode,y=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(v){if(o){for(;g;){for(p=t;p=p[g];)if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g=\"only\"===e&&!h&&\"nextSibling\"}return!0}if(h=[a?v.firstChild:v.lastChild],a&&m){for(x=(d=(l=(c=(f=(p=v)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&v.childNodes[d];p=++d&&p&&p[g]||(x=d=0)||h.pop();)if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)for(;(p=++d&&p&&p[g]||(x=d=0)||h.pop())&&((s?p.nodeName.toLowerCase()!==y:1!==p.nodeType)||!++x||(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p!==t)););return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||se.error(\"unsupported pseudo: \"+e);return i[b]?i(t):i.length>1?(n=[e,e,\"\",t],r.setFilters.hasOwnProperty(e.toLowerCase())?le((function(e,n){for(var r,o=i(e,t),a=o.length;a--;)e[r=P(e,o[a])]=!(n[r]=o[a])})):function(e){return i(e,0,n)}):i}},pseudos:{not:le((function(e){var t=[],n=[],r=s(e.replace($,\"$1\"));return r[b]?le((function(e,t,n,i){for(var o,a=r(e,null,i,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))})):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}})),has:le((function(e){return function(t){return se(e,t).length>0}})),contains:le((function(e){return e=e.replace(te,ne),function(t){return(t.textContent||i(t)).indexOf(e)>-1}})),lang:le((function(e){return V.test(e||\"\")||se.error(\"unsupported lang: \"+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute(\"xml:lang\")||t.getAttribute(\"lang\"))return(n=n.toLowerCase())===e||0===n.indexOf(e+\"-\")}while((t=t.parentNode)&&1===t.nodeType);return!1}})),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return\"input\"===t&&!!e.checked||\"option\"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return\"input\"===t&&\"button\"===e.type||\"button\"===t},text:function(e){var t;return\"input\"===e.nodeName.toLowerCase()&&\"text\"===e.type&&(null==(t=e.getAttribute(\"type\"))||\"text\"===t.toLowerCase())},first:ve((function(){return[0]})),last:ve((function(e,t){return[t-1]})),eq:ve((function(e,t,n){return[n<0?n+t:n]})),even:ve((function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e})),gt:ve((function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s-1&&(o[l]=!(a[l]=f))}}else y=Te(y===a?y.splice(h,y.length):y),i?i(null,a,y,u):H.apply(a,y)}))}function Ee(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[\" \"],u=a?1:0,c=be((function(e){return e===t}),s,!0),f=be((function(e){return P(t,e)>-1}),s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&we(p),u>1&&xe(e.slice(0,u-1).concat({value:\" \"===e[u-2].type?\"*\":\"\"})).replace($,\"$1\"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,v,y=0,m=\"0\",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG(\"*\",c),E=T+=null==w?1:Math.random()||.1,S=C.length;for(c&&(l=a==d||a||c);m!==S&&null!=(f=C[m]);m++){if(i&&f){for(h=0,a||f.ownerDocument==d||(p(f),s=!g);v=e[h++];)if(v(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!v&&f)&&y--,o&&x.push(f))}if(y+=m,n&&m!==y){for(h=0;v=t[h++];)v(x,b,a,s);if(o){if(y>0)for(;m--;)x[m]||b[m]||(b[m]=q.call(u));b=Te(b)}H.apply(u,b),c&&!o&&b.length>0&&y+t.length>1&&se.uniqueSort(u)}return c&&(T=E,l=w),x};return n?le(o):o}(o,i))).selector=e}return s},u=se.select=function(e,t,n,i){var o,u,l,c,f,p=\"function\"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&\"ID\"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(te,ne),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}for(o=G.needsContext.test(e)?0:u.length;o--&&(l=u[o],!r.relative[c=l.type]);)if((f=r.find[c])&&(i=f(l.matches[0].replace(te,ne),ee.test(u[0].type)&&ye(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&xe(u)))return H.apply(n,i),n;break}}return(p||s(e,d))(i,t,!g,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},n.sortStable=b.split(\"\").sort(N).join(\"\")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ce((function(e){return 1&e.compareDocumentPosition(d.createElement(\"fieldset\"))})),ce((function(e){return e.innerHTML=\"\",\"#\"===e.firstChild.getAttribute(\"href\")}))||fe(\"type|href|height|width\",(function(e,t,n){if(!n)return e.getAttribute(t,\"type\"===t.toLowerCase()?1:2)})),n.attributes&&ce((function(e){return e.innerHTML=\"\",e.firstChild.setAttribute(\"value\",\"\"),\"\"===e.firstChild.getAttribute(\"value\")}))||fe(\"value\",(function(e,t,n){if(!n&&\"input\"===e.nodeName.toLowerCase())return e.defaultValue})),ce((function(e){return null==e.getAttribute(\"disabled\")}))||fe(R,(function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null})),se}(e);b.find=T,b.expr=T.selectors,b.expr[\":\"]=b.expr.pseudos,b.uniqueSort=b.unique=T.uniqueSort,b.text=T.getText,b.isXMLDoc=T.isXML,b.contains=T.contains,b.escapeSelector=T.escape;var C=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&b(e).is(n))break;r.push(e)}return r},E=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},S=b.expr.match.needsContext;function k(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;function N(e,t,n){return h(t)?b.grep(e,(function(e,r){return!!t.call(e,r,e)!==n})):t.nodeType?b.grep(e,(function(e){return e===t!==n})):\"string\"!=typeof t?b.grep(e,(function(e){return s.call(t,e)>-1!==n})):b.filter(t,e,n)}b.filter=function(e,t,n){var r=t[0];return n&&(e=\":not(\"+e+\")\"),1===t.length&&1===r.nodeType?b.find.matchesSelector(r,e)?[r]:[]:b.find.matches(e,b.grep(t,(function(e){return 1===e.nodeType})))},b.fn.extend({find:function(e){var t,n,r=this.length,i=this;if(\"string\"!=typeof e)return this.pushStack(b(e).filter((function(){for(t=0;t1?b.uniqueSort(n):n},filter:function(e){return this.pushStack(N(this,e||[],!1))},not:function(e){return this.pushStack(N(this,e||[],!0))},is:function(e){return!!N(this,\"string\"==typeof e&&S.test(e)?b(e):e||[],!1).length}});var D,j=/^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/;(b.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,\"string\"==typeof e){if(!(r=\"<\"===e[0]&&\">\"===e[e.length-1]&&e.length>=3?[null,e,null]:j.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof b?t[0]:t,b.merge(this,b.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:v,!0)),A.test(r[1])&&b.isPlainObject(t))for(r in t)h(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=v.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):h(e)?void 0!==n.ready?n.ready(e):e(b):b.makeArray(e,this)}).prototype=b.fn,D=b(v);var q=/^(?:parents|prev(?:Until|All))/,L={children:!0,contents:!0,next:!0,prev:!0};function H(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}b.fn.extend({has:function(e){var t=b(e,this),n=t.length;return this.filter((function(){for(var e=0;e-1:1===n.nodeType&&b.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?b.uniqueSort(o):o)},index:function(e){return e?\"string\"==typeof e?s.call(b(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(b.uniqueSort(b.merge(this.get(),b(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),b.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return C(e,\"parentNode\")},parentsUntil:function(e,t,n){return C(e,\"parentNode\",n)},next:function(e){return H(e,\"nextSibling\")},prev:function(e){return H(e,\"previousSibling\")},nextAll:function(e){return C(e,\"nextSibling\")},prevAll:function(e){return C(e,\"previousSibling\")},nextUntil:function(e,t,n){return C(e,\"nextSibling\",n)},prevUntil:function(e,t,n){return C(e,\"previousSibling\",n)},siblings:function(e){return E((e.parentNode||{}).firstChild,e)},children:function(e){return E(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(k(e,\"template\")&&(e=e.content||e),b.merge([],e.childNodes))}},(function(e,t){b.fn[e]=function(n,r){var i=b.map(this,t,n);return\"Until\"!==e.slice(-5)&&(r=n),r&&\"string\"==typeof r&&(i=b.filter(r,i)),this.length>1&&(L[e]||b.uniqueSort(i),q.test(e)&&i.reverse()),this.pushStack(i)}}));var O=/[^\\x20\\t\\r\\n\\f]+/g;function P(e){return e}function R(e){throw e}function M(e,t,n,r){var i;try{e&&h(i=e.promise)?i.call(e).done(t).fail(n):e&&h(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}b.Callbacks=function(e){e=\"string\"==typeof e?function(e){var t={};return b.each(e.match(O)||[],(function(e,n){t[n]=!0})),t}(e):b.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--})),this},has:function(e){return e?b.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n=\"\",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=\"\"),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},b.extend({Deferred:function(t){var n=[[\"notify\",\"progress\",b.Callbacks(\"memory\"),b.Callbacks(\"memory\"),2],[\"resolve\",\"done\",b.Callbacks(\"once memory\"),b.Callbacks(\"once memory\"),0,\"resolved\"],[\"reject\",\"fail\",b.Callbacks(\"once memory\"),b.Callbacks(\"once memory\"),1,\"rejected\"]],r=\"pending\",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},catch:function(e){return i.then(null,e)},pipe:function(){var e=arguments;return b.Deferred((function(t){b.each(n,(function(n,r){var i=h(e[r[4]])&&e[r[4]];o[r[1]]((function(){var e=i&&i.apply(this,arguments);e&&h(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+\"With\"](this,i?[e]:arguments)}))})),e=null})).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==R&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(b.Deferred.getStackHook&&(c.stackTrace=b.Deferred.getStackHook()),e.setTimeout(c))}}return b.Deferred((function(e){n[0][3].add(a(0,e,h(i)?i:P,e.notifyWith)),n[1][3].add(a(0,e,h(t)?t:P)),n[2][3].add(a(0,e,h(r)?r:R))})).promise()},promise:function(e){return null!=e?b.extend(e,i):i}},o={};return b.each(n,(function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add((function(){r=s}),n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+\"With\"](this===o?void 0:this,arguments),this},o[t[0]+\"With\"]=a.fireWith})),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),o=i.call(arguments),a=b.Deferred(),s=function(e){return function(n){r[e]=this,o[e]=arguments.length>1?i.call(arguments):n,--t||a.resolveWith(r,o)}};if(t<=1&&(M(e,a.done(s(n)).resolve,a.reject,!t),\"pending\"===a.state()||h(o[n]&&o[n].then)))return a.then();for(;n--;)M(o[n],s(n),a.reject);return a.promise()}});var I=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;b.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&I.test(t.name)&&e.console.warn(\"jQuery.Deferred exception: \"+t.message,t.stack,n)},b.readyException=function(t){e.setTimeout((function(){throw t}))};var W=b.Deferred();function F(){v.removeEventListener(\"DOMContentLoaded\",F),e.removeEventListener(\"load\",F),b.ready()}b.fn.ready=function(e){return W.then(e).catch((function(e){b.readyException(e)})),this},b.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--b.readyWait:b.isReady)||(b.isReady=!0,!0!==e&&--b.readyWait>0||W.resolveWith(v,[b]))}}),b.ready.then=W.then,\"complete\"===v.readyState||\"loading\"!==v.readyState&&!v.documentElement.doScroll?e.setTimeout(b.ready):(v.addEventListener(\"DOMContentLoaded\",F),e.addEventListener(\"load\",F));var B=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if(\"object\"===x(n))for(s in i=!0,n)B(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,h(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(b(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each((function(){Y.remove(this,e)}))}}),b.extend({queue:function(e,t,n){var r;if(e)return t=(t||\"fx\")+\"queue\",r=G.get(e,t),n&&(!r||Array.isArray(n)?r=G.access(e,t,b.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||\"fx\";var n=b.queue(e,t),r=n.length,i=n.shift(),o=b._queueHooks(e,t);\"inprogress\"===i&&(i=n.shift(),r--),i&&(\"fx\"===t&&n.unshift(\"inprogress\"),delete o.stop,i.call(e,(function(){b.dequeue(e,t)}),o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+\"queueHooks\";return G.get(e,n)||G.access(e,n,{empty:b.Callbacks(\"once memory\").add((function(){G.remove(e,[t+\"queue\",n])}))})}}),b.fn.extend({queue:function(e,t){var n=2;return\"string\"!=typeof e&&(t=e,e=\"fx\",n--),arguments.length\\x20\\t\\r\\n\\f]*)/i,he=/^$|^module$|\\/(?:java|ecma)script/i;ce=v.createDocumentFragment().appendChild(v.createElement(\"div\")),(fe=v.createElement(\"input\")).setAttribute(\"type\",\"radio\"),fe.setAttribute(\"checked\",\"checked\"),fe.setAttribute(\"name\",\"t\"),ce.appendChild(fe),d.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML=\"\",d.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=\"\",d.option=!!ce.lastChild;var ge={thead:[1,\"\",\"
\"],col:[2,\"\",\"
\"],tr:[2,\"\",\"
\"],td:[3,\"\",\"
\"],_default:[0,\"\",\"\"]};function ve(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[],void 0===t||t&&k(e,t)?b.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n\",\"\"]);var me=/<|&#?\\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d-1)i&&i.push(o);else if(l=re(o),a=ve(f.appendChild(o),\"script\"),l&&ye(a),n)for(c=0;o=a[c++];)he.test(o.type||\"\")&&n.push(o);return f}var be=/^key/,we=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Te=/^([^.]*)(?:\\.(.+)|)/;function Ce(){return!0}function Ee(){return!1}function Se(e,t){return e===function(){try{return v.activeElement}catch(e){}}()==(\"focus\"===t)}function ke(e,t,n,r,i,o){var a,s;if(\"object\"==typeof t){for(s in\"string\"!=typeof n&&(r=r||n,n=void 0),t)ke(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&(\"string\"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Ee;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return b().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=b.guid++)),e.each((function(){b.event.add(this,t,i,r,n)}))}function Ae(e,t,n){n?(G.set(e,t,!1),b.event.add(e,t,{namespace:!1,handler:function(e){var r,o,a=G.get(this,t);if(1&e.isTrigger&&this[t]){if(a.length)(b.event.special[t]||{}).delegateType&&e.stopPropagation();else if(a=i.call(arguments),G.set(this,t,a),r=n(this,t),this[t](),a!==(o=G.get(this,t))||r?G.set(this,t,!1):o={},a!==o)return e.stopImmediatePropagation(),e.preventDefault(),o.value}else a.length&&(G.set(this,t,{value:b.event.trigger(b.extend(a[0],b.Event.prototype),a.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===G.get(e,t)&&b.event.add(e,t,Ce)}b.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=G.get(e);if(X(e))for(n.handler&&(n=(o=n).handler,i=o.selector),i&&b.find.matchesSelector(ne,i),n.guid||(n.guid=b.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(t){return void 0!==b&&b.event.triggered!==t.type?b.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||\"\").match(O)||[\"\"]).length;l--;)d=g=(s=Te.exec(t[l])||[])[1],h=(s[2]||\"\").split(\".\").sort(),d&&(f=b.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=b.event.special[d]||{},c=b.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&b.expr.match.needsContext.test(i),namespace:h.join(\".\")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),b.event.global[d]=!0)},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=G.hasData(e)&&G.get(e);if(v&&(u=v.events)){for(l=(t=(t||\"\").match(O)||[\"\"]).length;l--;)if(d=g=(s=Te.exec(t[l])||[])[1],h=(s[2]||\"\").split(\".\").sort(),d){for(f=b.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp(\"(^|\\\\.)\"+h.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"),a=o=p.length;o--;)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&(\"**\"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||b.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)b.event.remove(e,d+t[l],n,r,!0);b.isEmptyObject(u)&&G.remove(e,\"handle events\")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=b.event.fix(e),l=(G.get(this,\"events\")||Object.create(null))[u.type]||[],c=b.event.special[u.type]||{};for(s[0]=u,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&(\"click\"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:b.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\\s*$/g;function qe(e,t){return k(e,\"table\")&&k(11!==t.nodeType?t:t.firstChild,\"tr\")&&b(e).children(\"tbody\")[0]||e}function Le(e){return e.type=(null!==e.getAttribute(\"type\"))+\"/\"+e.type,e}function He(e){return\"true/\"===(e.type||\"\").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(\"type\"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(G.hasData(e)&&(s=G.get(e).events))for(i in G.remove(t,\"handle events\"),s)for(n=0,r=s[i].length;n1&&\"string\"==typeof v&&!d.checkClone&&De.test(v))return e.each((function(i){var o=e.eq(i);y&&(t[0]=v.call(this,i,o.html())),Re(o,t,n,r)}));if(p&&(a=(i=xe(t,e[0].ownerDocument,!1,e,r)).firstChild,1===i.childNodes.length&&(i=a),a||r)){for(u=(s=b.map(ve(i,\"script\"),Le)).length;f0&&ye(a,!u&&ve(e,\"script\")),s},cleanData:function(e){for(var t,n,r,i=b.event.special,o=0;void 0!==(n=e[o]);o++)if(X(n)){if(t=n[G.expando]){if(t.events)for(r in t.events)i[r]?b.event.remove(n,r):b.removeEvent(n,r,t.handle);n[G.expando]=void 0}n[Y.expando]&&(n[Y.expando]=void 0)}}}),b.fn.extend({detach:function(e){return Me(this,e,!0)},remove:function(e){return Me(this,e)},text:function(e){return B(this,(function(e){return void 0===e?b.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)}))}),null,e,arguments.length)},append:function(){return Re(this,arguments,(function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||qe(this,e).appendChild(e)}))},prepend:function(){return Re(this,arguments,(function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=qe(this,e);t.insertBefore(e,t.firstChild)}}))},before:function(){return Re(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this)}))},after:function(){return Re(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)}))},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(b.cleanData(ve(e,!1)),e.textContent=\"\");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map((function(){return b.clone(this,e,t)}))},html:function(e){return B(this,(function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if(\"string\"==typeof e&&!Ne.test(e)&&!ge[(de.exec(e)||[\"\",\"\"])[1].toLowerCase()]){e=b.htmlPrefilter(e);try{for(;n3,ne.removeChild(t)),s}}))}();var ze=[\"Webkit\",\"Moz\",\"ms\"],Ue=v.createElement(\"div\").style,Xe={};function Ve(e){var t=b.cssProps[e]||Xe[e];return t||(e in Ue?e:Xe[e]=function(e){for(var t=e[0].toUpperCase()+e.slice(1),n=ze.length;n--;)if((e=ze[n]+t)in Ue)return e}(e)||e)}var Ge=/^(none|table(?!-c[ea]).+)/,Ye=/^--/,Qe={position:\"absolute\",visibility:\"hidden\",display:\"block\"},Je={letterSpacing:\"0\",fontWeight:\"400\"};function Ke(e,t,n){var r=ee.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||\"px\"):t}function Ze(e,t,n,r,i,o){var a=\"width\"===t?1:0,s=0,u=0;if(n===(r?\"border\":\"content\"))return 0;for(;a<4;a+=2)\"margin\"===n&&(u+=b.css(e,n+te[a],!0,i)),r?(\"content\"===n&&(u-=b.css(e,\"padding\"+te[a],!0,i)),\"margin\"!==n&&(u-=b.css(e,\"border\"+te[a]+\"Width\",!0,i))):(u+=b.css(e,\"padding\"+te[a],!0,i),\"padding\"!==n?u+=b.css(e,\"border\"+te[a]+\"Width\",!0,i):s+=b.css(e,\"border\"+te[a]+\"Width\",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e[\"offset\"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function et(e,t,n){var r=We(e),i=(!d.boxSizingReliable()||n)&&\"border-box\"===b.css(e,\"boxSizing\",!1,r),o=i,a=$e(e,t,r),s=\"offset\"+t[0].toUpperCase()+t.slice(1);if(Ie.test(a)){if(!n)return a;a=\"auto\"}return(!d.boxSizingReliable()&&i||!d.reliableTrDimensions()&&k(e,\"tr\")||\"auto\"===a||!parseFloat(a)&&\"inline\"===b.css(e,\"display\",!1,r))&&e.getClientRects().length&&(i=\"border-box\"===b.css(e,\"boxSizing\",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Ze(e,t,n||(i?\"border\":\"content\"),o,r,a)+\"px\"}function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}b.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=$e(e,\"opacity\");return\"\"===n?\"1\":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=U(t),u=Ye.test(t),l=e.style;if(u||(t=Ve(s)),a=b.cssHooks[t]||b.cssHooks[s],void 0===n)return a&&\"get\"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];\"string\"===(o=typeof n)&&(i=ee.exec(n))&&i[1]&&(n=ae(e,t,i),o=\"number\"),null!=n&&n==n&&(\"number\"!==o||u||(n+=i&&i[3]||(b.cssNumber[s]?\"\":\"px\")),d.clearCloneStyle||\"\"!==n||0!==t.indexOf(\"background\")||(l[t]=\"inherit\"),a&&\"set\"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=U(t);return Ye.test(t)||(t=Ve(s)),(a=b.cssHooks[t]||b.cssHooks[s])&&\"get\"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=$e(e,t,r)),\"normal\"===i&&t in Je&&(i=Je[t]),\"\"===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),b.each([\"height\",\"width\"],(function(e,t){b.cssHooks[t]={get:function(e,n,r){if(n)return!Ge.test(b.css(e,\"display\"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):Fe(e,Qe,(function(){return et(e,t,r)}))},set:function(e,n,r){var i,o=We(e),a=!d.scrollboxSize()&&\"absolute\"===o.position,s=(a||r)&&\"border-box\"===b.css(e,\"boxSizing\",!1,o),u=r?Ze(e,t,r,s,o):0;return s&&a&&(u-=Math.ceil(e[\"offset\"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,\"border\",!1,o)-.5)),u&&(i=ee.exec(n))&&\"px\"!==(i[3]||\"px\")&&(e.style[t]=n,n=b.css(e,t)),Ke(0,n,u)}}})),b.cssHooks.marginLeft=_e(d.reliableMarginLeft,(function(e,t){if(t)return(parseFloat($e(e,\"marginLeft\"))||e.getBoundingClientRect().left-Fe(e,{marginLeft:0},(function(){return e.getBoundingClientRect().left})))+\"px\"})),b.each({margin:\"\",padding:\"\",border:\"Width\"},(function(e,t){b.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o=\"string\"==typeof n?n.split(\" \"):[n];r<4;r++)i[e+te[r]+t]=o[r]||o[r-2]||o[0];return i}},\"margin\"!==e&&(b.cssHooks[e+t].set=Ke)})),b.fn.extend({css:function(e,t){return B(this,(function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=We(e),i=t.length;a1)}}),b.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||b.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(b.cssNumber[n]?\"\":\"px\")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=b.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=b.css(e.elem,e.prop,\"\"))&&\"auto\"!==t?t:0},set:function(e){b.fx.step[e.prop]?b.fx.step[e.prop](e):1!==e.elem.nodeType||!b.cssHooks[e.prop]&&null==e.elem.style[Ve(e.prop)]?e.elem[e.prop]=e.now:b.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},b.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:\"swing\"},b.fx=tt.prototype.init,b.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===v.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,b.fx.interval),b.fx.tick())}function st(){return e.setTimeout((function(){nt=void 0})),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i[\"margin\"+(n=te[r])]=i[\"padding\"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(ct.tweeners[t]||[]).concat(ct.tweeners[\"*\"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each((function(){b.removeAttr(this,e)}))}}),b.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?b.prop(e,t,n):(1===o&&b.isXMLDoc(e)||(i=b.attrHooks[t.toLowerCase()]||(b.expr.match.bool.test(t)?ft:void 0)),void 0!==n?null===n?void b.removeAttr(e,t):i&&\"set\"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+\"\"),n):i&&\"get\"in i&&null!==(r=i.get(e,t))?r:null==(r=b.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!d.radioValue&&\"radio\"===t&&k(e,\"input\")){var n=e.value;return e.setAttribute(\"type\",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(O);if(i&&1===e.nodeType)for(;n=i[r++];)e.removeAttribute(n)}}),ft={set:function(e,t,n){return!1===t?b.removeAttr(e,n):e.setAttribute(n,n),n}},b.each(b.expr.match.bool.source.match(/\\w+/g),(function(e,t){var n=pt[t]||b.find.attr;pt[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=pt[a],pt[a]=i,i=null!=n(e,t,r)?a:null,pt[a]=o),i}}));var dt=/^(?:input|select|textarea|button)$/i,ht=/^(?:a|area)$/i;function gt(e){return(e.match(O)||[]).join(\" \")}function vt(e){return e.getAttribute&&e.getAttribute(\"class\")||\"\"}function yt(e){return Array.isArray(e)?e:\"string\"==typeof e&&e.match(O)||[]}b.fn.extend({prop:function(e,t){return B(this,b.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each((function(){delete this[b.propFix[e]||e]}))}}),b.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&b.isXMLDoc(e)||(t=b.propFix[t]||t,i=b.propHooks[t]),void 0!==n?i&&\"set\"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&\"get\"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=b.find.attr(e,\"tabindex\");return t?parseInt(t,10):dt.test(e.nodeName)||ht.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:\"htmlFor\",class:\"className\"}}),d.optSelected||(b.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),b.each([\"tabIndex\",\"readOnly\",\"maxLength\",\"cellSpacing\",\"cellPadding\",\"rowSpan\",\"colSpan\",\"useMap\",\"frameBorder\",\"contentEditable\"],(function(){b.propFix[this.toLowerCase()]=this})),b.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(h(e))return this.each((function(t){b(this).addClass(e.call(this,t,vt(this)))}));if((t=yt(e)).length)for(;n=this[u++];)if(i=vt(n),r=1===n.nodeType&&\" \"+gt(i)+\" \"){for(a=0;o=t[a++];)r.indexOf(\" \"+o+\" \")<0&&(r+=o+\" \");i!==(s=gt(r))&&n.setAttribute(\"class\",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(h(e))return this.each((function(t){b(this).removeClass(e.call(this,t,vt(this)))}));if(!arguments.length)return this.attr(\"class\",\"\");if((t=yt(e)).length)for(;n=this[u++];)if(i=vt(n),r=1===n.nodeType&&\" \"+gt(i)+\" \"){for(a=0;o=t[a++];)for(;r.indexOf(\" \"+o+\" \")>-1;)r=r.replace(\" \"+o+\" \",\" \");i!==(s=gt(r))&&n.setAttribute(\"class\",s)}return this},toggleClass:function(e,t){var n=typeof e,r=\"string\"===n||Array.isArray(e);return\"boolean\"==typeof t&&r?t?this.addClass(e):this.removeClass(e):h(e)?this.each((function(n){b(this).toggleClass(e.call(this,n,vt(this),t),t)})):this.each((function(){var t,i,o,a;if(r)for(i=0,o=b(this),a=yt(e);t=a[i++];)o.hasClass(t)?o.removeClass(t):o.addClass(t);else void 0!==e&&\"boolean\"!==n||((t=vt(this))&&G.set(this,\"__className__\",t),this.setAttribute&&this.setAttribute(\"class\",t||!1===e?\"\":G.get(this,\"__className__\")||\"\"))}))},hasClass:function(e){var t,n,r=0;for(t=\" \"+e+\" \";n=this[r++];)if(1===n.nodeType&&(\" \"+gt(vt(n))+\" \").indexOf(t)>-1)return!0;return!1}});var mt=/\\r/g;b.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=h(e),this.each((function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,b(this).val()):e)?i=\"\":\"number\"==typeof i?i+=\"\":Array.isArray(i)&&(i=b.map(i,(function(e){return null==e?\"\":e+\"\"}))),(t=b.valHooks[this.type]||b.valHooks[this.nodeName.toLowerCase()])&&\"set\"in t&&void 0!==t.set(this,i,\"value\")||(this.value=i))}))):i?(t=b.valHooks[i.type]||b.valHooks[i.nodeName.toLowerCase()])&&\"get\"in t&&void 0!==(n=t.get(i,\"value\"))?n:\"string\"==typeof(n=i.value)?n.replace(mt,\"\"):null==n?\"\":n:void 0}}),b.extend({valHooks:{option:{get:function(e){var t=b.find.attr(e,\"value\");return null!=t?t:gt(b.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a=\"select-one\"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),b.each([\"radio\",\"checkbox\"],(function(){b.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=b.inArray(b(e).val(),t)>-1}},d.checkOn||(b.valHooks[this].get=function(e){return null===e.getAttribute(\"value\")?\"on\":e.value})})),d.focusin=\"onfocusin\"in e;var xt=/^(?:focusinfocus|focusoutblur)$/,bt=function(e){e.stopPropagation()};b.extend(b.event,{trigger:function(t,n,r,i){var o,a,s,u,l,f,p,d,y=[r||v],m=c.call(t,\"type\")?t.type:t,x=c.call(t,\"namespace\")?t.namespace.split(\".\"):[];if(a=d=s=r=r||v,3!==r.nodeType&&8!==r.nodeType&&!xt.test(m+b.event.triggered)&&(m.indexOf(\".\")>-1&&(x=m.split(\".\"),m=x.shift(),x.sort()),l=m.indexOf(\":\")<0&&\"on\"+m,(t=t[b.expando]?t:new b.Event(m,\"object\"==typeof t&&t)).isTrigger=i?2:3,t.namespace=x.join(\".\"),t.rnamespace=t.namespace?new RegExp(\"(^|\\\\.)\"+x.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:b.makeArray(n,[t]),p=b.event.special[m]||{},i||!p.trigger||!1!==p.trigger.apply(r,n))){if(!i&&!p.noBubble&&!g(r)){for(u=p.delegateType||m,xt.test(u+m)||(a=a.parentNode);a;a=a.parentNode)y.push(a),s=a;s===(r.ownerDocument||v)&&y.push(s.defaultView||s.parentWindow||e)}for(o=0;(a=y[o++])&&!t.isPropagationStopped();)d=a,t.type=o>1?u:p.bindType||m,(f=(G.get(a,\"events\")||Object.create(null))[t.type]&&G.get(a,\"handle\"))&&f.apply(a,n),(f=l&&a[l])&&f.apply&&X(a)&&(t.result=f.apply(a,n),!1===t.result&&t.preventDefault());return t.type=m,i||t.isDefaultPrevented()||p._default&&!1!==p._default.apply(y.pop(),n)||!X(r)||l&&h(r[m])&&!g(r)&&((s=r[l])&&(r[l]=null),b.event.triggered=m,t.isPropagationStopped()&&d.addEventListener(m,bt),r[m](),t.isPropagationStopped()&&d.removeEventListener(m,bt),b.event.triggered=void 0,s&&(r[l]=s)),t.result}},simulate:function(e,t,n){var r=b.extend(new b.Event,n,{type:e,isSimulated:!0});b.event.trigger(r,null,t)}}),b.fn.extend({trigger:function(e,t){return this.each((function(){b.event.trigger(e,t,this)}))},triggerHandler:function(e,t){var n=this[0];if(n)return b.event.trigger(e,t,n,!0)}}),d.focusin||b.each({focus:\"focusin\",blur:\"focusout\"},(function(e,t){var n=function(e){b.event.simulate(t,e.target,b.event.fix(e))};b.event.special[t]={setup:function(){var r=this.ownerDocument||this.document||this,i=G.access(r,t);i||r.addEventListener(e,n,!0),G.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this.document||this,i=G.access(r,t)-1;i?G.access(r,t,i):(r.removeEventListener(e,n,!0),G.remove(r,t))}}}));var wt=e.location,Tt={guid:Date.now()},Ct=/\\?/;b.parseXML=function(t){var n;if(!t||\"string\"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,\"text/xml\")}catch(e){n=void 0}return n&&!n.getElementsByTagName(\"parsererror\").length||b.error(\"Invalid XML: \"+t),n};var Et=/\\[\\]$/,St=/\\r?\\n/g,kt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function Nt(e,t,n,r){var i;if(Array.isArray(t))b.each(t,(function(t,i){n||Et.test(e)?r(e,i):Nt(e+\"[\"+(\"object\"==typeof i&&null!=i?t:\"\")+\"]\",i,n,r)}));else if(n||\"object\"!==x(t))r(e,t);else for(i in t)Nt(e+\"[\"+i+\"]\",t[i],n,r)}b.param=function(e,t){var n,r=[],i=function(e,t){var n=h(t)?t():t;r[r.length]=encodeURIComponent(e)+\"=\"+encodeURIComponent(null==n?\"\":n)};if(null==e)return\"\";if(Array.isArray(e)||e.jquery&&!b.isPlainObject(e))b.each(e,(function(){i(this.name,this.value)}));else for(n in e)Nt(n,e[n],t,i);return r.join(\"&\")},b.fn.extend({serialize:function(){return b.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var e=b.prop(this,\"elements\");return e?b.makeArray(e):this})).filter((function(){var e=this.type;return this.name&&!b(this).is(\":disabled\")&&At.test(this.nodeName)&&!kt.test(e)&&(this.checked||!pe.test(e))})).map((function(e,t){var n=b(this).val();return null==n?null:Array.isArray(n)?b.map(n,(function(e){return{name:t.name,value:e.replace(St,\"\\r\\n\")}})):{name:t.name,value:n.replace(St,\"\\r\\n\")}})).get()}});var Dt=/%20/g,jt=/#.*$/,qt=/([?&])_=[^&]*/,Lt=/^(.*?):[ \\t]*([^\\r\\n]*)$/gm,Ht=/^(?:GET|HEAD)$/,Ot=/^\\/\\//,Pt={},Rt={},Mt=\"*/\".concat(\"*\"),It=v.createElement(\"a\");function Wt(e){return function(t,n){\"string\"!=typeof t&&(n=t,t=\"*\");var r,i=0,o=t.toLowerCase().match(O)||[];if(h(n))for(;r=o[i++];)\"+\"===r[0]?(r=r.slice(1)||\"*\",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function Ft(e,t,n,r){var i={},o=e===Rt;function a(s){var u;return i[s]=!0,b.each(e[s]||[],(function(e,s){var l=s(t,n,r);return\"string\"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)})),u}return a(t.dataTypes[0])||!i[\"*\"]&&a(\"*\")}function Bt(e,t){var n,r,i=b.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&b.extend(!0,e,r),e}It.href=wt.href,b.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:wt.href,type:\"GET\",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(wt.protocol),global:!0,processData:!0,async:!0,contentType:\"application/x-www-form-urlencoded; charset=UTF-8\",accepts:{\"*\":Mt,text:\"text/plain\",html:\"text/html\",xml:\"application/xml, text/xml\",json:\"application/json, text/javascript\"},contents:{xml:/\\bxml\\b/,html:/\\bhtml/,json:/\\bjson\\b/},responseFields:{xml:\"responseXML\",text:\"responseText\",json:\"responseJSON\"},converters:{\"* text\":String,\"text html\":!0,\"text json\":JSON.parse,\"text xml\":b.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Bt(Bt(e,b.ajaxSettings),t):Bt(b.ajaxSettings,e)},ajaxPrefilter:Wt(Pt),ajaxTransport:Wt(Rt),ajax:function(t,n){\"object\"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,o,a,s,u,l,c,f,p,d=b.ajaxSetup({},n),h=d.context||d,g=d.context&&(h.nodeType||h.jquery)?b(h):b.event,y=b.Deferred(),m=b.Callbacks(\"once memory\"),x=d.statusCode||{},w={},T={},C=\"canceled\",E={readyState:0,getResponseHeader:function(e){var t;if(l){if(!a)for(a={};t=Lt.exec(o);)a[t[1].toLowerCase()+\" \"]=(a[t[1].toLowerCase()+\" \"]||[]).concat(t[2]);t=a[e.toLowerCase()+\" \"]}return null==t?null:t.join(\", \")},getAllResponseHeaders:function(){return l?o:null},setRequestHeader:function(e,t){return null==l&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,w[e]=t),this},overrideMimeType:function(e){return null==l&&(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(l)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return r&&r.abort(t),S(0,t),this}};if(y.promise(E),d.url=((t||d.url||wt.href)+\"\").replace(Ot,wt.protocol+\"//\"),d.type=n.method||n.type||d.method||d.type,d.dataTypes=(d.dataType||\"*\").toLowerCase().match(O)||[\"\"],null==d.crossDomain){u=v.createElement(\"a\");try{u.href=d.url,u.href=u.href,d.crossDomain=It.protocol+\"//\"+It.host!=u.protocol+\"//\"+u.host}catch(e){d.crossDomain=!0}}if(d.data&&d.processData&&\"string\"!=typeof d.data&&(d.data=b.param(d.data,d.traditional)),Ft(Pt,d,n,E),l)return E;for(f in(c=b.event&&d.global)&&0==b.active++&&b.event.trigger(\"ajaxStart\"),d.type=d.type.toUpperCase(),d.hasContent=!Ht.test(d.type),i=d.url.replace(jt,\"\"),d.hasContent?d.data&&d.processData&&0===(d.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&(d.data=d.data.replace(Dt,\"+\")):(p=d.url.slice(i.length),d.data&&(d.processData||\"string\"==typeof d.data)&&(i+=(Ct.test(i)?\"&\":\"?\")+d.data,delete d.data),!1===d.cache&&(i=i.replace(qt,\"$1\"),p=(Ct.test(i)?\"&\":\"?\")+\"_=\"+Tt.guid+++p),d.url=i+p),d.ifModified&&(b.lastModified[i]&&E.setRequestHeader(\"If-Modified-Since\",b.lastModified[i]),b.etag[i]&&E.setRequestHeader(\"If-None-Match\",b.etag[i])),(d.data&&d.hasContent&&!1!==d.contentType||n.contentType)&&E.setRequestHeader(\"Content-Type\",d.contentType),E.setRequestHeader(\"Accept\",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(\"*\"!==d.dataTypes[0]?\", \"+Mt+\"; q=0.01\":\"\"):d.accepts[\"*\"]),d.headers)E.setRequestHeader(f,d.headers[f]);if(d.beforeSend&&(!1===d.beforeSend.call(h,E,d)||l))return E.abort();if(C=\"abort\",m.add(d.complete),E.done(d.success),E.fail(d.error),r=Ft(Rt,d,n,E)){if(E.readyState=1,c&&g.trigger(\"ajaxSend\",[E,d]),l)return E;d.async&&d.timeout>0&&(s=e.setTimeout((function(){E.abort(\"timeout\")}),d.timeout));try{l=!1,r.send(w,S)}catch(e){if(l)throw e;S(-1,e)}}else S(-1,\"No Transport\");function S(t,n,a,u){var f,p,v,w,T,C=n;l||(l=!0,s&&e.clearTimeout(s),r=void 0,o=u||\"\",E.readyState=t>0?4:0,f=t>=200&&t<300||304===t,a&&(w=function(e,t,n){for(var r,i,o,a,s=e.contents,u=e.dataTypes;\"*\"===u[0];)u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader(\"Content-Type\"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+\" \"+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(d,E,a)),!f&&b.inArray(\"script\",d.dataTypes)>-1&&(d.converters[\"text script\"]=function(){}),w=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if(\"*\"===o)o=u;else if(\"*\"!==u&&u!==o){if(!(a=l[u+\" \"+o]||l[\"* \"+o]))for(i in l)if((s=i.split(\" \"))[1]===o&&(a=l[u+\" \"+s[0]]||l[\"* \"+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:\"parsererror\",error:a?e:\"No conversion from \"+u+\" to \"+o}}}return{state:\"success\",data:t}}(d,w,E,f),f?(d.ifModified&&((T=E.getResponseHeader(\"Last-Modified\"))&&(b.lastModified[i]=T),(T=E.getResponseHeader(\"etag\"))&&(b.etag[i]=T)),204===t||\"HEAD\"===d.type?C=\"nocontent\":304===t?C=\"notmodified\":(C=w.state,p=w.data,f=!(v=w.error))):(v=C,!t&&C||(C=\"error\",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+\"\",f?y.resolveWith(h,[p,C,E]):y.rejectWith(h,[E,C,v]),E.statusCode(x),x=void 0,c&&g.trigger(f?\"ajaxSuccess\":\"ajaxError\",[E,d,f?p:v]),m.fireWith(h,[E,C]),c&&(g.trigger(\"ajaxComplete\",[E,d]),--b.active||b.event.trigger(\"ajaxStop\")))}return E},getJSON:function(e,t,n){return b.get(e,t,n,\"json\")},getScript:function(e,t){return b.get(e,void 0,t,\"script\")}}),b.each([\"get\",\"post\"],(function(e,t){b[t]=function(e,n,r,i){return h(n)&&(i=i||r,r=n,n=void 0),b.ajax(b.extend({url:e,type:t,dataType:i,data:n,success:r},b.isPlainObject(e)&&e))}})),b.ajaxPrefilter((function(e){var t;for(t in e.headers)\"content-type\"===t.toLowerCase()&&(e.contentType=e.headers[t]||\"\")})),b._evalUrl=function(e,t,n){return b.ajax({url:e,type:\"GET\",dataType:\"script\",cache:!0,async:!1,global:!1,converters:{\"text script\":function(){}},dataFilter:function(e){b.globalEval(e,t,n)}})},b.fn.extend({wrapAll:function(e){var t;return this[0]&&(h(e)&&(e=e.call(this[0])),t=b(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map((function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e})).append(this)),this},wrapInner:function(e){return h(e)?this.each((function(t){b(this).wrapInner(e.call(this,t))})):this.each((function(){var t=b(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)}))},wrap:function(e){var t=h(e);return this.each((function(n){b(this).wrapAll(t?e.call(this,n):e)}))},unwrap:function(e){return this.parent(e).not(\"body\").each((function(){b(this).replaceWith(this.childNodes)})),this}}),b.expr.pseudos.hidden=function(e){return!b.expr.pseudos.visible(e)},b.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},b.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var $t={0:200,1223:204},_t=b.ajaxSettings.xhr();d.cors=!!_t&&\"withCredentials\"in _t,d.ajax=_t=!!_t,b.ajaxTransport((function(t){var n,r;if(d.cors||_t&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];for(a in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i[\"X-Requested-With\"]||(i[\"X-Requested-With\"]=\"XMLHttpRequest\"),i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,\"abort\"===e?s.abort():\"error\"===e?\"number\"!=typeof s.status?o(0,\"error\"):o(s.status,s.statusText):o($t[s.status]||s.status,s.statusText,\"text\"!==(s.responseType||\"text\")||\"string\"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n(\"error\"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout((function(){n&&r()}))},n=n(\"abort\");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}})),b.ajaxPrefilter((function(e){e.crossDomain&&(e.contents.script=!1)})),b.ajaxSetup({accepts:{script:\"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"},contents:{script:/\\b(?:java|ecma)script\\b/},converters:{\"text script\":function(e){return b.globalEval(e),e}}}),b.ajaxPrefilter(\"script\",(function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type=\"GET\")})),b.ajaxTransport(\"script\",(function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(r,i){t=b(\"" - ], - "text/plain": [ - ":Curve [qh] (energy)" - ] - }, - "execution_count": 8, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1001" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "dispersion1D['energy'].hvplot()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "dispersion1D.easyscience.add_variable('S', ['qh'], sl.structure_factor(dispersion1D['qh'].__array__()))" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [qh] (S)" - ] - }, - "execution_count": 10, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1106" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "dispersion1D['S'].hvplot()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "dispersion3D = xr.Dataset()\n", - "dispersion3D.easyscience.add_coordinate('qh', np.linspace(-2, 2, 501))\n", - "dispersion3D.easyscience.add_coordinate('qk', np.linspace(-2, 2, 51))\n", - "dispersion3D.easyscience.add_coordinate('ql', np.linspace(-2, 2, 51))" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "points = dispersion3D.easyscience.generate_points(['qh', 'qk', 'ql'])\n", - "dispersion3D.easyscience.add_variable('w', ['qh', 'qk', 'ql'], sl.dispersion(points[0, :]).unstack())\n", - "dispersion3D.easyscience.add_variable('S', ['qh', 'qk', 'ql'], sl.structure_factor_assume(points).unstack())" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":DynamicMap [qk,ql]\n", - " :Curve [qh] (w)" - ] - }, - "execution_count": 14, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1211" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "dispersion3D['w'].hvplot(groupby=['qk', 'ql'])" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":DynamicMap [qk]\n", - " :Image [qh,ql] (S)" - ] - }, - "execution_count": 15, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1489" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "dispersion3D['S'].hvplot(x='qh', y='ql', groupby=['qk'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Fitting" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "sl2 = spinLadder.from_pars(12.1, 1.4)\n", - "sl2_disp = xr.Dataset()\n", - "sl2_disp.easyscience.add_coordinate('qh', np.linspace(0, 1, 101))\n", - "sl2_disp.easyscience.add_variable('w', ['qh'], sl2.dispersion(sl2_disp.qh) + 0.1*(np.random.random(sl2_disp.qh.shape) - 0.5))" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [qh] (w)" - ] - }, - "execution_count": 18, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1727" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "sl2_disp.hvplot()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "from easyscience.fitting import Fitter\n", - "f = Fitter()\n", - "f.initialize(sl, sl.dispersion)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \n" - ] - } - ], - "source": [ - "print(sl.Jr, sl.Jl)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \n" - ] - } - ], - "source": [ - "res = sl2_disp['w'].easyscience.fit(f)\n", - "print(sl.Jr, sl.Jl)\n", - "sl2_disp['w_fit'] = res.y_calc" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":NdOverlay [Variable]\n", - " :Curve [qh] (value)" - ] - }, - "execution_count": 22, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "1832" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "sl2_disp.hvplot(y=['w', 'w_fit'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Playing in 4D space" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [], - "source": [ - "import scipy\n", - "def do_kdtree(combined_x_y_arrays, points):\n", - " mytree = scipy.spatial.cKDTree(combined_x_y_arrays)\n", - " dist, indexes = mytree.query(points)\n", - " return indexes\n", - "\n", - "dispersion3D.easyscience.add_dimension('e', np.linspace(8, 20, 101))\n", - "\n", - "m = np.zeros(np.prod(list(dispersion3D.dims.values())))\n", - "indices = do_kdtree(dispersion3D.easyscience.generate_points(['qh', 'qk', 'ql', 'e']).T, xr.concat([dispersion3D.easyscience.generate_points(['qh', 'qk', 'ql']).unstack()[0], dispersion3D.easyscience.generate_points(['qh', 'qk', 'ql']).unstack()[1], dispersion3D.easyscience.generate_points(['qh', 'qk', 'ql']).unstack()[2], dispersion3D['w']], dim='fit_dim').stack(all_x=['qh', 'qk', 'ql']).T)\n", - "m[indices] = np.array(dispersion3D['S']).reshape((1, -1))" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":DynamicMap [qk,ql]\n", - " :Image [qh,e] (w_map)" - ] - }, - "execution_count": 37, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "2590" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "dispersion3D.easyscience.add_variable('w_map', ['qh', 'qk', 'ql', 'e'], m.reshape((dispersion3D.dims['qh'], dispersion3D.dims['qk'], dispersion3D.dims['ql'], dispersion3D.dims['e'])))\n", - "dispersion3D['w_map'].hvplot(groupby=['qk', 'ql'], x='qh', y='e')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.3" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/legacy/examples_old/example1.py b/legacy/examples_old/example1.py deleted file mode 100644 index 14609f7a..00000000 --- a/legacy/examples_old/example1.py +++ /dev/null @@ -1,30 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import numpy as np - -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -# This is a simple example of creating an object which has fitable parameters - -b = BaseObj('line', - m=Parameter('m', 1), - c=Parameter('c', 1)) - - -def fit_fun(x): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return b.c.value + b.m.value * x - - -f = Fitter() -f.initialize(b, fit_fun) - -x = np.array([1, 2, 3]) -y = np.array([2, 4, 6]) - 1 - -f_res = f.fit(x, y) - -print(f_res.goodness_of_fit) diff --git a/legacy/examples_old/example1_dream.py b/legacy/examples_old/example1_dream.py deleted file mode 100644 index 0b5621be..00000000 --- a/legacy/examples_old/example1_dream.py +++ /dev/null @@ -1,45 +0,0 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" - -import numpy as np - -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -# This is a simple example of creating an object which has fitable parameters - -b = BaseObj("line", m=Parameter("m", 1), c=Parameter("c", 1)) - - -def fit_fun(x): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return b.c.value + b.m.value * x - - -f = Fitter() -f.initialize(b, fit_fun) -f.switch_minimizer("bumps") - -x = np.array([1, 2, 3]) -y = np.array([2, 4, 6]) - 1 - -method = "dream" -dream_kwargs = { - k: v - for k, v in [ - ("samples", 10000), - ("burn", 100), - ("pop", 10), - ("init", "eps"), - ("thin", 1), - ("alpha", 0.01), - ("outliers", "none"), - ("trim", False), - ("steps", 0), - ] -} - -f_res = f.fit(x, y, method="dream", minimizer_kwargs=dream_kwargs) - -print(f_res.chi2) diff --git a/legacy/examples_old/example2.py b/legacy/examples_old/example2.py deleted file mode 100644 index 6e1acbd2..00000000 --- a/legacy/examples_old/example2.py +++ /dev/null @@ -1,53 +0,0 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" - -import numpy as np - -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -# In this case we have inherited from `BaseObj` to create a class which has fitable attributes. - - -class Line(BaseObj): - """ - Simple descriptor of a line. - """ - - _m = 1 - _c = 0 - - def __init__(self): - super().__init__(self.__class__.__name__, *self._defaults) - - @property - def _defaults(self): - return [Parameter("m", self._m), Parameter("c", self._c)] - - @property - def gradient(self): - return self.m.value - - @property - def intercept(self): - return self.c.value - - def fit_func(self, x: np.ndarray) -> np.ndarray: - return self.gradient * x + self.intercept - - def __repr__(self): - return f"Line: m={self.m}, c={self.c}" - - -l = Line() -f = Fitter() -f.initialize(l, l.fit_func) - -x = np.array([1, 2, 3]) -y = np.array([2, 4, 6]) - 1 - -f_res = f.fit(x, y) - -print(f_res.chi2) -print(l) diff --git a/legacy/examples_old/example3.py b/legacy/examples_old/example3.py deleted file mode 100644 index 49d6ede9..00000000 --- a/legacy/examples_old/example3.py +++ /dev/null @@ -1,101 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -from typing import Callable - -import numpy as np - -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -# In this case we have inherited from `BaseObj` to create a class which has fitable attributes. -# This class does not know about the `Calculator`, only the interface. - - -class Calculator: - def __init__(self, m=1, c=0): - self.m = m - self.c = c - - def calculate(self, x): - return self.m * x + self.c - - -class Interface: - def __init__(self): - self.calculator = Calculator() - - def get_value(self, value_label: str): - return getattr(self.calculator, value_label, None) - - def set_value(self, value_label: str, value): - print(f'Value of {value_label} set to {value}') - setattr(self.calculator, value_label, value) - - def fit_func(self, x): - return self.calculator.calculate(x) - - -class Line(BaseObj): - _defaults = [Parameter('m', 1), - Parameter('c', 0)] - - def __init__(self, interface=None): - super().__init__(self.__class__.__name__, - *self._defaults) - self.interface = interface - if self.interface: - for parameter in self.get_fit_parameters(): - name = parameter.name - setattr(self.__class__.__dict__[name], - '_callback', - property(self.__gitem(name), self.__sitem(self, name))) - - @property - def gradient(self): - if self.interface: - return self.interface.get_value('m') - else: - return self.m.value - - @property - def intercept(self): - if self.interface: - return self.interface.get_value('c') - else: - return self.c.value - - def fit_func(self, x: np.ndarray) -> np.ndarray: - if self.interface: - return self.interface.fit_func(x) - else: - raise NotImplementedError - - def __repr__(self): - return f'Line: m={self.m}, c={self.c}' - - @staticmethod - def __gitem(key: str) -> Callable: - def inner(obj): - obj.interface.get_value(key) - return lambda obj: inner(obj) - - @staticmethod - def __sitem(obj, key): - def inner(value): - obj.interface.set_value(key, value) - return inner - - -l = Line(interface=Interface()) -l.interface -f = Fitter(l, l.fit_func) - -x = np.array([1, 2, 3]) -y = np.array([2, 4, 6]) - 1 - -f_res = f.fit(x, y) - -print(f_res) -print(l) \ No newline at end of file diff --git a/legacy/examples_old/example4.py b/legacy/examples_old/example4.py deleted file mode 100644 index 720ff961..00000000 --- a/legacy/examples_old/example4.py +++ /dev/null @@ -1,448 +0,0 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" - -import json -from abc import ABCMeta -from abc import abstractmethod -from typing import Callable -from typing import List - -import numpy as np - -from easyscience import global_object -from easyscience.fitting import Fitter -from easyscience.Objects.component_serializer import ComponentSerializer -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -# This is a much more complex case where we have calculators, interfaces, interface factory and an -# inherited object (from `BaseObj`). In this case the Line class is available with/without an interface -# With an interface it connects to one of the calculator interfaces. This calculator interface then translates -# interface commands to calculator specific commands - - -class Calculator1: - """ - Generic calculator in the style of crysPy - """ - - def __init__(self, m: float = 1, c: float = 0): - """ - Create a calculator object with m and c - - :param m: gradient - :type m: float - :param c: intercept - :type c: float - """ - self.m = m - self.c = c - - def calculate(self, x_array: np.ndarray) -> np.ndarray: - """ - For a given x calculate the corresponding y - - :param x_array: array of data points to be calculated - :type x_array: np.ndarray - :return: points calculated at `x` - :rtype: np.ndarray - """ - return self.m * x_array + self.c - - -class Calculator2: - """ - Isolated calculator. This calculator can't have values set, it can - only load/save data and calculate from it. i.e in the style of crysfml - """ - - def __init__(self): - """ """ - self._data = {"m": 0, "c": 0} - - def calculate(self, x_array: np.ndarray) -> np.ndarray: - """ - For a given x calculate the corresponding y - - :param x_array: array of data points to be calculated - :type x_array: np.ndarray - :return: points calculated at `x` - :rtype: np.ndarray - """ - return self._data["m"] * x_array + self._data["c"] - - def export_data(self) -> str: - return json.dumps(self._data) - - def import_data(self, input_str: str): - self._data = json.loads(input_str) - - -class InterfaceTemplate(ComponentSerializer, metaclass=ABCMeta): - """ - This class is a template and defines all properties that an interface should have. - """ - - _interfaces = [] - _global_object = global_object - - def __init_subclass__(cls, is_abstract: bool = False, **kwargs): - """ - Initialise all subclasses so that they can be created in the factory - - :param is_abstract: Is this a subclass which shouldn't be dded - :type is_abstract: bool - :param kwargs: key word arguments - :type kwargs: dict - :return: None - :rtype: noneType - """ - super().__init_subclass__(**kwargs) - if not is_abstract: - cls._interfaces.append(cls) - - @abstractmethod - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - pass - - @abstractmethod - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - pass - - @abstractmethod - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - pass - - -class Interface1(InterfaceTemplate): - """ - A simple example interface using Calculator1 - """ - - def __init__(self): - # This interface will use calculator1 - self.calculator = Calculator1() - - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - return getattr(self.calculator, value_label, None) - - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - if self._global_object.debug: - print(f"Interface1: Value of {value_label} set to {value}") - setattr(self.calculator, value_label, value) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self.calculator.calculate(x_array) - - -class Interface2(InterfaceTemplate): - """ - This is a more complex template. Here we need to export_data data, - transfer it to the calculator (get and calculate) and import data - from the calculator (set) - """ - - def __init__(self): - """ - Set up a calculator and a local dict - """ - self.calculator = Calculator2() - self._data: dict = {} - - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - self._data = json.loads(self.calculator.export_data()) - return getattr(self._data, value_label, None) - - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - if self._global_object.debug: - print(f"Interface2: Value of {value_label} set to {value}") - self._data = json.loads(self.calculator.export_data()) - if value_label in self._data.keys(): - self._data[value_label] = value - self.calculator.import_data(json.dumps(self._data)) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self.calculator.calculate(x_array) - - -class InterfaceFactory: - """ - This class allows for the creation and transference of interfaces. - """ - - def __init__(self): - self._interfaces: List[InterfaceTemplate] = InterfaceTemplate._interfaces - self._current_interface = None - self.__interface_obj = None - self.create() - - def create(self, interface_name: str = None): - """ - Create an interface to a calculator from those initialized. Interfaces can be selected - by `interface_name` where `interface_name` is one of `obj.available_interfaces`. This - interface can now be accessed by obj() - - :param interface_name: name of interface to be created - :type interface_name: str - :return: None - :rtype: noneType - """ - if interface_name is None: - interface_name = self._interfaces[0].__name__ - - interfaces = self.available_interfaces - if interface_name in interfaces: - self._current_interface = self._interfaces[interfaces.index(interface_name)] - self.__interface_obj = self._current_interface() - - def switch(self, new_interface: str): - """ - Changes the current interface to a new interface. The current interface is destroyed and - all ComponentSerializer parameters carried over to the new interface. i.e. pick up where you left off. - - :param new_interface: name of new interface to be created - :type new_interface: str - :return: None - :rtype: noneType - """ - serialized = self.__interface_obj.as_dict() - interfaces = self.available_interfaces - if new_interface in interfaces: - self._current_interface: InterfaceTemplate = self._interfaces[ - interfaces.index(new_interface) - ] - self.__interface_obj = self._current_interface.from_dict(serialized) - else: - raise AttributeError - - @property - def available_interfaces(self) -> List[str]: - """ - Return all available interfaces. - - :return: List of available interface names - :rtype: List[str] - """ - return [this_interface.__name__ for this_interface in self._interfaces] - - @property - def current_interface(self): - """ - Returns the constructor for the currently selected interface - - :return: Interface constructor - :rtype: InterfaceTemplate - """ - return self._current_interface - - def fit_func(self, x_array: np.ndarray, *args, **kwargs) -> np.ndarray: - """ - Pass through to the underlying interfaces fitting function. - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :param args: positional arguments for the fitting function - :type args: Any - :param kwargs: key/value pair arguments for the fitting function. - :type kwargs: Any - :return: points calculated at positional values `x` - :rtype: np.ndarray - """ - return self.__interface_obj.fit_function(x_array, *args, **kwargs) - - def generate_bindings(self, name): - """ - Automatically bind a `Parameter` to the corresponding interface. - - :param name: parameter name - :type name: str - :return: binding property - :rtype: property - """ - return property(self.__get_item(name), self.__set_item(self, name)) - - def __call__(self, *args, **kwargs) -> InterfaceTemplate: - return self.__interface_obj - - @staticmethod - def __get_item(key: str) -> Callable: - """ - Access the value of a key by a callable object - - :param key: name of parameter to be retrieved - :type key: str - :return: function to get key - :rtype: Callable - """ - - def inner(obj): - obj().get_value(key) - - return lambda obj: inner(obj) - - @staticmethod - def __set_item(obj, key): - """ - Set the value of a key by a callable object - - :param obj: object to be created from - :type obj: InterfaceFactory - :param key: name of parameter to be set - :type key: str - :return: function to set key - :rtype: Callable - """ - - def inner(value): - obj().set_value(key, value) - - return inner - - -class Line(BaseObj): - """ - Simple descriptor of a line. - """ - - _defaults = [Parameter("m", 1), Parameter("c", 0)] - - def __init__(self, interface_factory: InterfaceFactory = None): - """ - Create a line and add an interface if requested - - :param interface_factory: interface controller object - :type interface_factory: InterfaceFactory - """ - super().__init__(self.__class__.__name__, *self._defaults) - self.interface = interface_factory - self._set_interface() - - def _set_interface(self): - if self.interface: - # If an interface is given, generate bindings - for parameter in self.get_fit_parameters(): - name = parameter.name - self.interface.generate_bindings(name) - - @property - def gradient(self): - if self.interface: - return self.interface().get_value("m") - else: - return self.m.value - - @property - def intercept(self): - if self.interface: - return self.interface().get_value("c") - else: - return self.c.value - - def __repr__(self): - return f"Line: m={self.m}, c={self.c}" - - -interface = InterfaceFactory() -line = Line(interface_factory=interface) -f = Fitter(line, interface.fit_func) - -# y = 2x -1 -x = np.array([1, 2, 3], dtype=np.float64) -y = np.array([2, 4, 6], dtype=np.float64) - 1 - -f_res = f.fit(x, y) - -print("\n######### Interface 1 #########\n") -print(f_res) -print(line) - -# This gets the interface name `'Interface2'` -other_interface = interface.available_interfaces[1] -# Switch over the interfaces -line.interface.switch(other_interface) -# Reset the values so we don't cheat -line.m.value = 1 -line.c.value = 0 -f_res = f.fit(x, y) - -print("\n######### Interface 2 #########\n") -print(f_res) -print(line) diff --git a/legacy/examples_old/example5_broken.py b/legacy/examples_old/example5_broken.py deleted file mode 100644 index a7967ee8..00000000 --- a/legacy/examples_old/example5_broken.py +++ /dev/null @@ -1,367 +0,0 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" - -import json -from abc import ABCMeta -from abc import abstractmethod -from typing import Callable - -import numpy as np - -from easyscience import global_object -from easyscience.fitting import Fitter -from easyscience.Objects.Base import BaseObj -from easyscience.Objects.Base import Parameter -from easyscience.Objects.component_serializer import ComponentSerializer - -# from easyscience.Objects.Base import LoggedProperty -from easyscience.Objects.Inferface import InterfaceFactoryTemplate - -# This is a much more complex case where we have calculators, interfaces, interface factory and an -# inherited object (from `BaseObj`). In this case the Line class is available with/without an interface -# With an interface it connects to one of the calculator interfaces. This calculator interface then translates -# interface commands to calculator specific commands - - -class Calculator1: - """ - Generic calculator in the style of crysPy - """ - - def __init__(self, m: float = 1, c: float = 0): - """ - Create a calculator object with m and c - - :param m: gradient - :type m: float - :param c: intercept - :type c: float - """ - self.m = m - self.c = c - - def calculate(self, x_array: np.ndarray) -> np.ndarray: - """ - For a given x calculate the corresponding y - - :param x_array: array of data points to be calculated - :type x_array: np.ndarray - :return: points calculated at `x` - :rtype: np.ndarray - """ - return self.m * x_array + self.c - - -class Calculator2: - """ - Isolated calculator. This calculator can't have values set, it can - only load/save data and calculate from it. i.e in the style of crysfml - """ - - def __init__(self): - """ """ - self._data = {"m": 0, "c": 0} - - def calculate(self, x_array: np.ndarray) -> np.ndarray: - """ - For a given x calculate the corresponding y - - :param x_array: array of data points to be calculated - :type x_array: np.ndarray - :return: points calculated at `x` - :rtype: np.ndarray - """ - return self._data["m"] * x_array + self._data["c"] - - def export_data(self) -> str: - return json.dumps(self._data) - - def import_data(self, input_str: str): - self._data = json.loads(input_str) - - -class InterfaceTemplate(ComponentSerializer, metaclass=ABCMeta): - """ - This class is a template and defines all properties that an interface should have. - """ - - _interfaces = [] - _global_object = global_object - - def __init_subclass__(cls, is_abstract: bool = False, **kwargs): - """ - Initialise all subclasses so that they can be created in the factory - - :param is_abstract: Is this a subclass which shouldn't be dded - :type is_abstract: bool - :param kwargs: key word arguments - :type kwargs: dict - :return: None - :rtype: noneType - """ - super().__init_subclass__(**kwargs) - if not is_abstract: - cls._interfaces.append(cls) - - @abstractmethod - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - pass - - @abstractmethod - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - pass - - @abstractmethod - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - pass - - -class Interface1(InterfaceTemplate): - """ - A simple example interface using Calculator1 - """ - - def __init__(self): - # This interface will use calculator1 - self.calculator = Calculator1() - - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - return getattr(self.calculator, value_label, None) - - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - if self._global_object.debug: - print(f"Interface1: Value of {value_label} set to {value}") - setattr(self.calculator, value_label, value) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self.calculator.calculate(x_array) - - -class Interface2(InterfaceTemplate): - """ - This is a more complex template. Here we need to export_data data, - transfer it to the calculator (get and calculate) and import data - from the calculator (set) - """ - - def __init__(self): - """ - Set up a calculator and a local dict - """ - self.calculator = Calculator2() - self._data: dict = {} - - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - self._data = json.loads(self.calculator.export_data()) - return getattr(self._data, value_label, None) - - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - if self._global_object.debug: - print(f"Interface2: Value of {value_label} set to {value}") - self._data = json.loads(self.calculator.export_data()) - if value_label in self._data.keys(): - self._data[value_label] = value - self.calculator.import_data(json.dumps(self._data)) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self.calculator.calculate(x_array) - - -class InterfaceFactory(InterfaceFactoryTemplate): - def __init__(self): - super(InterfaceFactory, self).__init__(InterfaceTemplate._interfaces) - - def generate_bindings(self, name, *args, **kwargs) -> property: - """ - Automatically bind a `Parameter` to the corresponding interface. - - :param name: parameter name - :type name: str - :return: binding property - :rtype: property - """ - return property(self.__get_item(name), self.__set_item(self, name)) - - @staticmethod - def __get_item(key: str) -> Callable: - """ - Access the value of a key by a callable object - - :param key: name of parameter to be retrieved - :type key: str - :return: function to get key - :rtype: Callable - """ - - def inner(obj): - obj().get_value(key) - - return lambda obj: inner(obj) - - @staticmethod - def __set_item(obj, key): - """ - Set the value of a key by a callable object - - :param obj: object to be created from - :type obj: InterfaceFactory - :param key: name of parameter to be set - :type key: str - :return: function to set key - :rtype: Callable - """ - - def inner(value): - obj().set_value(key, value) - - return inner - - -class Line(BaseObj): - """ - Simple descriptor of a line. - """ - - _defaults = [Parameter("m", 1), Parameter("c", 0)] - - def __init__(self, interface_factory: InterfaceFactory = None): - """ - Create a line and add an interface if requested. - - :param interface_factory: interface controller object - :type interface_factory: InterfaceFactory - """ - self.interface = interface_factory - super().__init__(self.__class__.__name__, *self._defaults) - self._set_interface() - - def _set_interface(self): - if self.interface: - # If an interface is given, generate bindings - for parameter in self.get_fit_parameters(): - name = parameter.name - self.set_binding(name, self.interface.generate_bindings) - - @property - def gradient(self): - # if self.interface: - # return self.interface().get_value('m') - # else: - return self.m.value - - @property - def intercept(self): - # if self.interface: - # return self.interface().get_value('c') - # else: - return self.c.value - - def __repr__(self): - return f"Line: m={self.m}, c={self.c}" - - -global_object.debug = True - -interface = InterfaceFactory() -line = Line(interface_factory=interface) -f = Fitter(line, interface.fit_func) - -# y = 2x -1 -x = np.array([1, 2, 3], dtype=np.float64) -y = 2 * x - 1 - -f_res = f.fit(x, y) - -print("\n######### Interface 1 #########\n") -print(f_res) -print(line) - -a = line.c - -# Now lets change fitting engine -f.switch_minimizer("bumps") -# Reset the values so we don't cheat -line.m = 1 -line.c = 0 -f_res = f.fit(x, y, weights=0.1 * np.ones_like(x)) -print("\n######### bumps fitting #########\n") -print(f_res) -print(line) diff --git a/legacy/examples_old/example6_broken.py b/legacy/examples_old/example6_broken.py deleted file mode 100644 index fe2dbbf2..00000000 --- a/legacy/examples_old/example6_broken.py +++ /dev/null @@ -1,446 +0,0 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" - -import json -from abc import ABCMeta -from abc import abstractmethod -from typing import Callable - -import numpy as np - -from easyscience import global_object -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.Variable import Parameter -from easyscience.Objects.component_serializer import ComponentSerializer -from easyscience.Objects.Inferface import InterfaceFactoryTemplate - -# This is a much more complex case where we have calculators, interfaces, interface factory and an -# inherited object (from `BaseObj`). In this case the Line class is available with/without an interface -# With an interface it connects to one of the calculator interfaces. This calculator interface then translates -# interface commands to calculator specific commands - - -class Calculator1: - """ - Generic calculator in the style of crysPy - """ - - class calcLine: - def __init__(self, m: float = 1, c: float = 0): - self.m = m - self.c = c - - def __call__(self, *args, **kwargs): - return self.m * args[0] + self.c - - class calcCurve: - def __init__(self, A=0, p=0, x_shift=0): - self.A = A - self.p = p - self.x_shift = x_shift - - def __call__(self, *args, **kwargs): - return self.A * np.sin((2 * np.pi / self.p) * (x + self.x_shift)) - - def __init__(self): - """ - Create a calculator object with m and c - - :param m: gradient - :type m: float - :param c: intercept - :type c: float - """ - self._line = [] - self._curve = [] - - def add_line(self, m: float = 1, c: float = 0): - self._line.append(self.calcLine(m, c)) - - def add_curve(self, A=0, p=0, x_shift=0): - self._curve.append(self.calcCurve(A, p, x_shift)) - - @property - def lines(self): - return self._line - - @property - def curves(self): - return self._curve - - def calculate(self, x_array: np.ndarray) -> np.ndarray: - """ - For a given x calculate the corresponding y - - :param x_array: array of data points to be calculated - :type x_array: np.ndarray - :return: points calculated at `x` - :rtype: np.ndarray - """ - y = np.zeros_like(x_array) - for line in self._line: - y += line(x_array) - for curve in self._curve: - y += curve(x_array) - return y - - -class InterfaceTemplate(ComponentSerializer, metaclass=ABCMeta): - """ - This class is a template and defines all properties that an interface should have. - """ - - _interfaces = [] - _global_object = global_object - - def __init_subclass__(cls, is_abstract: bool = False, **kwargs): - """ - Initialise all subclasses so that they can be created in the factory - - :param is_abstract: Is this a subclass which shouldn't be dded - :type is_abstract: bool - :param kwargs: key word arguments - :type kwargs: dict - :return: None - :rtype: noneType - """ - super().__init_subclass__(**kwargs) - if not is_abstract: - cls._interfaces.append(cls) - - @abstractmethod - def create_curve(self, A, p, x_shift): - pass - - @abstractmethod - def get_curve_value(self, curve, par, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - pass - - @abstractmethod - def set_curve_value(self, curve, par, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - pass - - @abstractmethod - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - pass - - -class Interface1(InterfaceTemplate): - """ - A simple example interface using Calculator1 - """ - - def __init__(self): - # This interface will use calculator1 - self.calculator = Calculator1() - - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - return getattr(self.calculator, value_label, None) - - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - if self._global_object.debug: - print(f"Interface1: Value of {value_label} set to {value}") - setattr(self.calculator, value_label, value) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self.calculator.calculate(x_array) - - -class Calculator2: - """ - Isolated calculator. This calculator can't have values set, it can - only load/save data and calculate from it. i.e in the style of crysfml - """ - - def __init__(self): - """ """ - self._data = {"m": 0, "c": 0} - - def calculate(self, x_array: np.ndarray) -> np.ndarray: - """ - For a given x calculate the corresponding y - - :param x_array: array of data points to be calculated - :type x_array: np.ndarray - :return: points calculated at `x` - :rtype: np.ndarray - """ - return self._data["m"] * x_array + self._data["c"] - - def export_data(self) -> str: - return json.dumps(self._data) - - def import_data(self, input_str: str): - self._data = json.loads(input_str) - - -class Interface2(InterfaceTemplate): - """ - This is a more complex template. Here we need to export_data data, - transfer it to the calculator (get and calculate) and import data - from the calculator (set) - """ - - def __init__(self): - """ - Set up a calculator and a local dict - """ - self.calculator = Calculator2() - self._data: dict = {} - - def get_value(self, value_label: str) -> float: - """ - Method to get a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :return: associated value - :rtype: float - """ - self._data = json.loads(self.calculator.export_data()) - return getattr(self._data, value_label, None) - - def set_value(self, value_label: str, value: float): - """ - Method to set a value from the calculator - - :param value_label: parameter name to get - :type value_label: str - :param value: new numeric value - :type value: float - :return: None - :rtype: noneType - """ - if self._global_object.debug: - print(f"Interface2: Value of {value_label} set to {value}") - self._data = json.loads(self.calculator.export_data()) - if value_label in self._data.keys(): - self._data[value_label] = value - self.calculator.import_data(json.dumps(self._data)) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self.calculator.calculate(x_array) - - -class InterfaceFactory(InterfaceFactoryTemplate): - def __init__(self): - super(InterfaceFactory, self).__init__(InterfaceTemplate._interfaces) - - def generate_bindings(self, name, *args, **kwargs) -> property: - """ - Automatically bind a `Parameter` to the corresponding interface. - - :param name: parameter name - :type name: str - :return: binding property - :rtype: property - """ - return property(self.__get_item(name), self.__set_item(self, name)) - - @staticmethod - def __get_item(key: str) -> Callable: - """ - Access the value of a key by a callable object - - :param key: name of parameter to be retrieved - :type key: str - :return: function to get key - :rtype: Callable - """ - - def inner(obj): - obj().get_value(key) - - return lambda obj: inner(obj) - - @staticmethod - def __set_item(obj, key): - """ - Set the value of a key by a callable object - - :param obj: object to be created from - :type obj: InterfaceFactory - :param key: name of parameter to be set - :type key: str - :return: function to set key - :rtype: Callable - """ - - def inner(value): - obj().set_value(key, value) - - return inner - - -class Line(BaseObj): - """ - Simple descriptor of a line. - """ - - _defaults = [Parameter("m", 1), Parameter("c", 0)] - - def __init__(self, interface_factory: InterfaceFactory = None): - """ - Create a line and add an interface if requested - - :param interface_factory: interface controller object - :type interface_factory: InterfaceFactory - """ - self.interface = interface_factory - super().__init__(self.__class__.__name__, *self._defaults) - self._set_interface() - - def _set_interface(self): - if self.interface: - # If an interface is given, generate bindings - for parameter in self.get_fit_parameters(): - name = parameter.name - self.set_binding(name, self.interface.generate_bindings) - - def __repr__(self): - return f"Line: m={self.m}, c={self.c}" - - -class Curve(BaseObj): - """ - Simple descriptor of a line. - """ - - _defaults = [Parameter("A", 1.0), Parameter("p", np.pi), Parameter("x_shift", 0.0)] - - def __init__(self, interface_factory: InterfaceFactory = None): - """ - Create a line and add an interface if requested - - :param interface_factory: interface controller object - :type interface_factory: InterfaceFactory - """ - self.interface = interface_factory - super().__init__(self.__class__.__name__, *self._defaults) - self._set_interface() - - def _set_interface(self): - if self.interface: - # If an interface is given, generate bindings - for parameter in self.get_fit_parameters(): - name = parameter.name - self.set_binding(name, self.interface.generate_bindings) - - def __repr__(self): - return f"Curve: A={self.A}, p={self.p}, x_shift={self.x_shift}" - - -class Model(BaseObj): - """ - Simple descriptor of a line. - """ - - _defaults = [Line, Curve] - - def __init__(self, interface_factory: InterfaceFactory = None): - """ - Create a line and add an interface if requested - - :param interface_factory: interface controller object - :type interface_factory: InterfaceFactory - """ - self.interface = interface_factory - super().__init__( - self.__class__.__name__, *[default(interface) for default in self._defaults] - ) - - def __repr__(self): - this_str = "Hybrid Model:\n" - for name, item in self._kwargs.items(): - this_str += f"{name}: " - for par in item._defaults: - this_str += f"{par.name} = {par.value}, " - this_str = this_str[:-2] + "\n" - return this_str - - -interface = InterfaceFactory() -hybrid = Model(interface_factory=interface) -f = Fitter(hybrid, interface.fit_func) -print(hybrid) - -# y = 2x -1 -x = np.array([1, 2, 3]) -y = 2 * x - 1 - -f_res = f.fit(x, y) - -print("\n######### Interface 1 #########\n") -print(f_res) -print(hybrid) - -# Now lets change fitting engine -f.switch_minimizer("bumps") -# Reset the values so we don't cheat -hybrid.m = 1 -hybrid.c = 0 -f_res = f.fit(x, y, weights=0.1 * np.ones_like(x)) -print("\n######### bumps fitting #########\n") -print(f_res) -print(hybrid) diff --git a/legacy/examples_old/example_dataset.py b/legacy/examples_old/example_dataset.py deleted file mode 100644 index 4a656802..00000000 --- a/legacy/examples_old/example_dataset.py +++ /dev/null @@ -1,56 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import time - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr - -d = xr.Dataset() - -nx = 5E3 -x_min = 0 -x_max = 3*np.pi - -x = np.linspace(x_min, x_max, num=int(nx)) - -d.easyscience.add_coordinate('x', x) -d.easyscience.add_variable('y', ['x'], np.sin(x), auto_sigma=False) - -d['y'].plot() -plt.show() - - -d.easyscience.remove_variable('y') -d.easyscience.add_coordinate('y', x + np.pi) -d.easyscience.add_variable('z', ['x', 'y'], np.sin(x).reshape((-1, 1))*np.cos(x).reshape((1, -1)) + (0.5 - np.random.random(size=(int(nx), int(nx))))) - -def func(x, *args, **kwargs): - return np.sin(x[:, 0]) * np.cos(x[:, 1]) - -bdims, f = d['z'].easyscience.fit_prep(func) -d['x_broadcast'], d['y_broadcast'] = bdims - -print('Applying func - No dask') -t = time.time() -d['computed_no_dask'] = xr.apply_ufunc(f, d['x_broadcast'], d['y_broadcast']) -temp = d['z'] - d['computed_no_dask'] -print(f'Time taken: {time.time() - t}') -temp.plot() -plt.show() - -print('Applying func - Dask') -t = time.time() -to_chunk = ['x_broadcast', 'y_broadcast', 'z'] -for name in to_chunk: - d[name] = d[name].chunk({'x': 4000, 'y': 4000}) -d['computed_dask'] = xr.apply_ufunc(f, d['x_broadcast'], d['y_broadcast'], dask='parallelized') -temp = d['z'] - d['computed_dask'] -temp.compute() -print(f'Time taken: {time.time() - t}') -temp.plot() -plt.show() - -print('All done :-)') diff --git a/legacy/examples_old/example_dataset2.py b/legacy/examples_old/example_dataset2.py deleted file mode 100644 index b235bc4f..00000000 --- a/legacy/examples_old/example_dataset2.py +++ /dev/null @@ -1,50 +0,0 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -d = xr.Dataset() - -b = BaseObj("line", m=Parameter("m", 1), c=Parameter("c", 1)) - - -def fit_fun(x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return b.c.value + b.m.value * x - - -f = Fitter() -f.initialize(b, fit_fun) - -nx = 1e3 -x_min = 0 -x_max = 100 - -x = np.linspace(x_min, x_max, num=int(nx)) -y = 2 * x - 1 + 5 * (np.random.random(size=x.shape) - 0.5) - -d.easyscience.add_coordinate("x", x) -d.easyscience.add_variable("y", ["x"], y, auto_sigma=True) - - -def post(result, addition=10): - return result + addition - - -d["y"].easyscience.postcompute_func = post - -# d['y'] = d['y'].chunk({'x': 1000}) -f_res = d["y"].easyscience.fit(f, dask="parallelized") - -print(f_res.chi2) - -d["y"].plot() -d["computed"] = f_res.y_calc -d["computed"].plot() -plt.show() diff --git a/legacy/examples_old/example_dataset2pt2.py b/legacy/examples_old/example_dataset2pt2.py deleted file mode 100644 index 83c9cbe0..00000000 --- a/legacy/examples_old/example_dataset2pt2.py +++ /dev/null @@ -1,57 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -d = xr.Dataset() - -m_starting_point = 1 -c_starting_point = 1 - - -b = BaseObj('line', - m=Parameter('m', m_starting_point), - c=Parameter('c', c_starting_point)) - - -def fit_fun(x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return b.c.raw_value + b.m.raw_value * x - - -nx = 1E3 -x_min = 0 -x_max = 100 - -x = np.linspace(x_min, x_max, num=int(nx)) -y = 2*x - 1 + 5*(np.random.random(size=x.shape) - 0.5) - -d.easyscience.add_coordinate('x', x) -d.easyscience.add_variable('y', ['x'], y, auto_sigma=False) - -f = Fitter() -f.initialize(b, fit_fun) - -fig, ax = plt.subplots(2, 3, sharey='row') -for idx, minimizer in enumerate(['lmfit', 'bumps', 'dfo_ls']): - - b.m = m_starting_point - b.c = c_starting_point - f.switch_minimizer(minimizer) - - f_res = d['y'].easyscience.fit(f, vectorize=True) - print(f_res.p) - - d['y'].plot(ax=ax[0, idx]) - f_res.y_calc.unstack().plot(ax=ax[0, idx]) - temp = d['y'] - f_res.y_calc - temp.plot(ax=ax[1, idx]) - ax[0, idx].set_title(f'Minimizer - {minimizer}') -ax[1, 0].set_ylabel('Difference') -plt.show() diff --git a/legacy/examples_old/example_dataset2pt2_broken.py b/legacy/examples_old/example_dataset2pt2_broken.py deleted file mode 100644 index ef4dcbab..00000000 --- a/legacy/examples_old/example_dataset2pt2_broken.py +++ /dev/null @@ -1,57 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.Base import BaseObj -from easyscience.Objects.Base import Parameter - -d = xr.Dataset() - -m_starting_point = 1 -c_starting_point = 1 - - -b = BaseObj('line', - m=Parameter('m', m_starting_point), - c=Parameter('c', c_starting_point)) - - -def fit_fun(x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return b.c.value + b.m.value * x - - -nx = 1E3 -x_min = 0 -x_max = 100 - -x = np.linspace(x_min, x_max, num=int(nx)) -y = 2*x - 1 + 5*(np.random.random(size=x.shape) - 0.5) - -d.easyscience.add_coordinate('x', x) -d.easyscience.add_variable('y', ['x'], y, auto_sigma=False) - -f = Fitter() -f.initialize(b, fit_fun) - -fig, ax = plt.subplots(2, 3, sharey='row') -for idx, minimizer in enumerate(['lmfit', 'bumps', 'dfo_ls']): - - b.m = m_starting_point - b.c = c_starting_point - f.switch_minimizer(minimizer) - - f_res = d['y'].easyscience.fit(f, vectorize=True) - print(f_res.p) - - d['y'].plot(ax=ax[0, idx]) - f_res.y_calc.unstack().plot(ax=ax[0, idx]) - temp = d['y'] - f_res.y_calc - temp.plot(ax=ax[1, idx]) - ax[0, idx].set_title(f'Minimizer - {minimizer}') -ax[1, 0].set_ylabel('Difference') -plt.show() diff --git a/legacy/examples_old/example_dataset3.py b/legacy/examples_old/example_dataset3.py deleted file mode 100644 index 4164dc5e..00000000 --- a/legacy/examples_old/example_dataset3.py +++ /dev/null @@ -1,92 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import time - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -d = xr.Dataset() - -nx = 5E2 -x_min = 0 -x_max = 3 - -sin_offest = - 0.35 -cos_offset = 0.14 - -x = np.linspace(x_min, x_max, num=int(nx)) - -d.easyscience.add_coordinate('x', x) -d.easyscience.add_coordinate('y', x + 1) -d.easyscience.add_variable('z', ['x', 'y'], np.sin(2*np.pi*(x + sin_offest)).reshape((-1, 1))*np.cos(2*np.pi*(x + cos_offset)).reshape((1, -1)) + 2*(0.5 - np.random.random(size=(int(nx), int(nx))))) - -s_off_start_point = 0 -c_off_start_point = 0 - -b = BaseObj('line', - s_off=Parameter('s_off', s_off_start_point), - c_off=Parameter('c_off', c_off_start_point)) - - -def fit_fun(x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return np.sin(2*np.pi*(x[:, 0] + b.s_off.value)) * np.cos(2*np.pi*(x[:, 1] + b.c_off.value)) - - -f = Fitter() -f.initialize(b, fit_fun) - -print('Performing fit - No dask') -t = time.time() -f_res = d['z'].easyscience.fit(f) -d['computed_no_dask'] = f_res.y_calc -temp = d['z'] - d['computed_no_dask'] -print(f'Time taken: {time.time() - t}') - -print(f_res.p0) -print(f_res.p) - -fig, axes = plt.subplots(ncols=2, nrows=2) -d['z'].plot(ax=axes[0, 0]) -d['computed_no_dask'].plot(ax=axes[0, 1]) -temp.plot(ax=axes[1, 0]) -plt.tight_layout() -plt.show() - - - -b.s_off.value = s_off_start_point -b.c_off.value = c_off_start_point - -f = Fitter() -f.initialize(b, fit_fun) - -print('Performing fit - Dask') -t = time.time() -to_chunk = ['z'] -print('Chunking -->') -for name in to_chunk: - d[name] = d[name].chunk({'x': 100, 'y': 100}) -print('Fitting -->') -f_res = d['z'].easyscience.fit(f, dask='parallelized') -d['computed_dask'] = f_res.y_calc -temp = d['z'] - d['computed_dask'] -print(f'Time taken: {time.time() - t}') - -print(f_res.p0) -print(f_res.p) - -fig, axes = plt.subplots(ncols=2, nrows=2) -d['z'].plot(ax=axes[0, 0]) -d['computed_dask'].plot(ax=axes[0, 1]) -temp.plot(ax=axes[1, 0]) -plt.tight_layout() -plt.show() - -print('All done :-)') diff --git a/legacy/examples_old/example_dataset3pt2.py b/legacy/examples_old/example_dataset3pt2.py deleted file mode 100644 index 187306dd..00000000 --- a/legacy/examples_old/example_dataset3pt2.py +++ /dev/null @@ -1,62 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -d = xr.Dataset() - -nx = 5E2 -x_min = 0 -x_max = 3 - -sin_offest = - 0.35 -cos_offset = 0.14 - -x = np.linspace(x_min, x_max, num=int(nx)) - -d.easyscience.add_coordinate('x', x) -d.easyscience.add_coordinate('y', x + 1) -d.easyscience.add_variable('z', ['x', 'y'], np.sin(2*np.pi*(x + sin_offest)).reshape((-1, 1))*np.cos(2*np.pi*(x + cos_offset)).reshape((1, -1)) + 2*(0.5 - np.random.random(size=(int(nx), int(nx))))) - -s_off_start_point = 0 -c_off_start_point = 0 - -b = BaseObj('line', - s_off=Parameter('s_off', s_off_start_point), - c_off=Parameter('c_off', c_off_start_point)) - - -def fit_fun(x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return np.sin(2*np.pi*(x[:, 0] + b.s_off.value)) * np.cos(2*np.pi*(x[:, 1] + b.c_off.value)) - - -fig, ax = plt.subplots(2, 3, sharey=True, sharex=True) -cbar_ax1 = fig.add_axes([0.85, 0.15, 0.05, 0.3]) -cbar_ax2 = fig.add_axes([0.85, 0.60, 0.05, 0.3]) - -for idx, minimizer in enumerate(['lmfit', 'bumps', 'dfo_ls']): - b.s_off = s_off_start_point - b.c_off = c_off_start_point - - f = Fitter() - f.initialize(b, fit_fun) - - f_res = d['z'].easyscience.fit(f) - d[f'computed_{minimizer}'] = f_res.y_calc - d[f'dz_{minimizer}'] = d['z'] - d[f'computed_{minimizer}'] - - p1 = d[f'computed_{minimizer}'].plot(ax=ax[0, idx], cbar_kwargs={'cax': cbar_ax1}) - p2 = d[f'dz_{minimizer}'].plot(ax=ax[1, idx], cbar_kwargs={'cax': cbar_ax2}) - ax[0, idx].set_title(f'{minimizer}') - ax[1, idx].set_title('s_off - {:0.03f}\nc_off - {:0.03f}'.format(b.s_off.value, b.c_off.value)) - ax[0, idx].set_aspect('equal', 'box') - ax[1, idx].set_aspect('equal', 'box') -fig.subplots_adjust(right=0.8) -plt.show() diff --git a/legacy/examples_old/example_dataset4.py b/legacy/examples_old/example_dataset4.py deleted file mode 100644 index 1e975d4e..00000000 --- a/legacy/examples_old/example_dataset4.py +++ /dev/null @@ -1,49 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -d = xr.Dataset() - -b = BaseObj('line', - m=Parameter('m', 1), - c=Parameter('c', 1)) - - -def fit_fun(x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return b.c.value + b.m.value * x - - -f = Fitter() -f.initialize(b, fit_fun) - -nx = 1E3 -x_min = 0 -x_max = 100 - -x = np.linspace(x_min, x_max, num=int(nx)) -y1 = 2*x - 1 + 5*(np.random.random(size=x.shape) - 0.5) -x2 = x + 20 -y2 = 2*x2 - 1 + 5*(np.random.random(size=x2.shape) - 0.5) - -d.easyscience.add_coordinate('x1', x) -d.easyscience.add_variable('y1', ['x1'], y1, auto_sigma=True) -d.easyscience.add_coordinate('x2', x2) -d.easyscience.add_variable('y2', ['x2'], y2, auto_sigma=True) - -res = d.easyscience.fit(f, ['y1', 'y2']) - -fig, axs = plt.subplots(1, len(res), sharey=True) -for idx, r in enumerate(res): - r.y_obs.plot(ax=axs[idx]) - r.y_calc.plot(ax=axs[idx]) - axs[idx].set_title(f'Dataset {idx}') -plt.show() - diff --git a/legacy/examples_old/example_dataset4_2.py b/legacy/examples_old/example_dataset4_2.py deleted file mode 100644 index 9bba2ffd..00000000 --- a/legacy/examples_old/example_dataset4_2.py +++ /dev/null @@ -1,67 +0,0 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - -import matplotlib.pyplot as plt - -import numpy as np -from easyscience.Datasets.xarray import xr -from easyscience.fitting import Fitter -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter - -d = xr.Dataset() - - -class Wavey(BaseObj): - def __init__(self, - amplitude: Parameter, - phase: Parameter, - period: Parameter): - super(Wavey, self).__init__('SimpleWave', - amplitude=amplitude, phase=phase, - period=period) - - def __repr__(self): - return (f'{self.name} - A={self.amplitude}, ph={self.phase}, period={self.period}') - - @classmethod - def from_params(cls, amplitude: float = 1, phase: float = 0, period: float = 2*np.pi): - amplitude = Parameter('amplitude', amplitude, min=0) - phase = Parameter('phase', phase) - period = Parameter('period', period, min=0, max=2*np.pi) - return cls(amplitude, phase, period) - - def fit_fun(self, x, *args, **kwargs): - # In the real case we would gust call the evaluation fn without reference to the BaseObj - return self.amplitude.value * np.sin((x + self.phase.value)/self.period.value) - -b = Wavey.from_params() -bb = Wavey.from_params(1.1, 0.1, 1.9*np.pi) -f = Fitter() - -f.initialize(b, b.fit_fun) - -nx = 1E3 -x_min = 0 -x_max = 100 - -x = np.linspace(x_min, x_max, num=int(nx)) -y1 = bb.fit_fun(x) + .75*(0.5 - np.random.random(size=x.shape)) -x2 = x + 20 -y2 = bb.fit_fun(x2) + .75*(0.5 - np.random.random(size=x2.shape)) - -d.easyscience.add_coordinate('x1', x) -d.easyscience.add_variable('y1', ['x1'], y1, auto_sigma=True) -d.easyscience.add_coordinate('x2', x2) -d.easyscience.add_variable('y2', ['x2'], y2, auto_sigma=True) - -res = d.easyscience.fit(f, ['y1', 'y2']) - -fig, axs = plt.subplots(1, len(res), sharey=True) -for idx, r in enumerate(res): - r.y_obs.plot(ax=axs[idx]) - r.y_calc.plot(ax=axs[idx]) - axs[idx].set_title(f'Dataset {idx}') -plt.show() -print(f'Actual = {bb}') -print(f'Fitted = {b}') diff --git a/pixi.toml b/pixi.toml index a3237349..bbbca890 100644 --- a/pixi.toml +++ b/pixi.toml @@ -3,14 +3,13 @@ ########### [workspace] - # Supported platforms for the lock file (pixi.lock) platforms = [ 'win-64', # Set minimum supported version for glibc to be 2.35 to ensure packages # like `crysfml` that only have wheels for glibc 2.35+ # (manylinux_2_35_x86_64) are used. - #libc = { family = 'glibc', version = '2.35' } + # libc = { family = 'glibc', version = '2.35' } { platform = 'linux-64', glibc = '2.35' }, # Set minimum supported version for macOS to be 14.0 to ensure packages # like `scipp` that only have wheels for macOS 14.0+ (macosx_14_0_arm64) @@ -44,8 +43,8 @@ python = '3.14.*' # editable installations. [feature.dev.dependencies] -nodejs = '*' # Required for Prettier (non-Python formatting) -pixi-kernel = '*' # Pixi Jupyter kernel +nodejs = '*' # Required for Prettier (non-Python formatting) +pixi-kernel = '*' # Pixi Jupyter kernel [feature.dev.pypi-dependencies] pip = '*' @@ -55,7 +54,7 @@ easyscience = { path = '.', editable = true, extras = ['dev'] } # of editable installations. [feature.user.dependencies] -pixi-kernel = '*' # Pixi Jupyter kernel +pixi-kernel = '*' # Pixi Jupyter kernel [feature.user.pypi-dependencies] pip = '*' @@ -66,7 +65,6 @@ easyscience = '*' ############## [environments] - # The `default` feature is always included in all environments. # Additional features can be specified per environment. @@ -87,13 +85,12 @@ user = { features = ['py-max', 'user'] } ####### [tasks] - ################## # πŸ§ͺ Testing Tasks ################## unit-tests = 'python -m pytest tests/unit/ --color=yes -v' -functional-tests = 'python -m pytest tests/functional/ --color=yes -v' +# functional-tests = 'python -m pytest tests/functional/ --color=yes -v' integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v' @@ -152,7 +149,7 @@ raw-metrics-json = 'radon raw -s -j src/' ############# unit-tests-coverage = 'pixi run unit-tests --cov=src/easyscience --cov-report=term-missing' -functional-tests-coverage = 'pixi run functional-tests --cov=src/easyscience --cov-report=term-missing' +# functional-tests-coverage = 'pixi run functional-tests --cov=src/easyscience --cov-report=term-missing' integration-tests-coverage = 'pixi run integration-tests --cov=src/easyscience --cov-report=term-missing' docstring-coverage = 'interrogate -c pyproject.toml src/easyscience' @@ -172,9 +169,9 @@ notebook-tweak = 'python tools/tweak_notebooks.py docs/docs/tutorials/' notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v' notebook-prepare = { depends-on = [ - #'notebook-convert', + # 'notebook-convert', 'notebook-strip', - #'notebook-tweak', + # 'notebook-tweak', ] } ######################## @@ -266,7 +263,7 @@ clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +" post-install = { depends-on = [ 'npm-config', 'prettier-install', - #'pre-commit-setup', + # 'pre-commit-setup', ] } ########################## diff --git a/prettierrc.toml b/prettierrc.toml index b98c86eb..d8a956f5 100644 --- a/prettierrc.toml +++ b/prettierrc.toml @@ -1,22 +1,24 @@ plugins = [ - "prettier-plugin-toml", # use the TOML plugin + 'prettier-plugin-toml', # use the TOML plugin ] -endOfLine = 'lf' # change line endings to LF -proseWrap = 'always' # change wrapping in Markdown files -semi = false # remove semicolons -singleQuote = true # use single quotes instead of double quotes -tabWidth = 2 # change tab width to 2 spaces -useTabs = false # use spaces instead of tabs +endOfLine = 'lf' # change line endings to LF +proseWrap = 'always' # change wrapping in Markdown files +semi = false # remove semicolons +singleQuote = true # use single quotes instead of double quotes +tabWidth = 2 # change tab width to 2 spaces +useTabs = false # use spaces instead of tabs -printWidth = 79 # wrap lines at 79 characters +printWidth = 79 # wrap lines at 79 characters [[overrides]] -files = ["*.md"] +files = ['*.md'] + [overrides.options] -printWidth = 72 # wrap Markdown files at 72 characters +printWidth = 72 # wrap Markdown files at 72 characters [[overrides]] -files = ["*.yml", "*.yaml"] +files = ['*.yml', '*.yaml'] + [overrides.options] -printWidth = 88 # wrap YAML files at 88 characters +printWidth = 88 # wrap YAML files at 88 characters diff --git a/pyproject.toml b/pyproject.toml index 89d28050..58e9ed21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ [project] name = 'easyscience' -dynamic = ['version'] # Use versioningit to manage the version +dynamic = ['version'] # Use versioningit to manage the version description = 'Core building blocks for EasyScience' authors = [{ name = 'EasyScience contributors' }] readme = 'README.md' @@ -24,54 +24,54 @@ classifiers = [ ] requires-python = '>=3.11' dependencies = [ - 'asteval', # Safely evaluate Python expressions from strings - 'lmfit', # Non-linear least squares fitting - 'bumps>=1.0.4', # Bayesian uncertainty estimation - 'dfo-ls', # Derivative-free optimization - 'numpy', # Numerical computing - 'scipp', # Handling and analysis of scientific data + 'asteval', # Safely evaluate Python expressions from strings + 'lmfit', # Non-linear least squares fitting + 'bumps>=1.0.4', # Bayesian uncertainty estimation + 'dfo-ls', # Derivative-free optimization + 'numpy', # Numerical computing + 'scipp', # Handling and analysis of scientific data ] [project.optional-dependencies] dev = [ - 'pooch', # Data downloader - 'matplotlib', # Plotting library - 'jupyterlab', # Jupyter notebooks - 'ipykernel', # Jupyter kernel (required for running notebooks) - 'ipywidgets', # Widgets (needed for interactive matplotlib backends) - 'ipympl', # Matplotlib Jupyter widget backend (%matplotlib widget) - 'IPython', # Interactive Python shell - 'GitPython', # Interact with Git repositories - 'build', # Building the package - 'pre-commit', # Pre-commit hooks - 'jinja2', # Templating - 'nbmake', # Building notebooks - 'nbstripout', # Strip output from notebooks - 'nbqa', # Linting and formatting notebooks - 'pytest', # Testing - 'pytest-cov', # Test coverage - 'pytest-xdist', # Enable parallel testing - 'ruff', # Linting and formatting code - 'radon', # Code complexity and maintainability - 'validate-pyproject[all]', # Validate pyproject.toml - 'versioningit', # Automatic versioning from git tags - 'jupytext', # Jupyter notebook text format support - 'jupyterquiz', # Quizzes in Jupyter notebooks - 'pydoclint', # Docstring linter - 'format-docstring', # Docstring formatter - 'docstripy', # Convert docstrings to other formats - 'interrogate', # Docstring coverage checker - 'copier', # Template management - 'mike', # MkDocs: Versioned documentation support - 'mkdocs', # Static site generator - 'mkdocs-material', # Documentation framework on top of MkDocs - 'mkdocs-autorefs', # MkDocs: Auto-references support - 'mkdocs-jupyter', # MkDocs: Jupyter notebook support - 'mkdocs-plugin-inline-svg', # MkDocs: Inline SVG support - 'mkdocs-markdownextradata-plugin', # MkDocs: Markdown extra data support, such as global variables - 'mkdocstrings-python', # MkDocs: Python docstring support - 'pyyaml', # YAML parser - 'spdx-headers', # SPDX license header validation + 'pooch', # Data downloader + 'matplotlib', # Plotting library + 'jupyterlab', # Jupyter notebooks + 'ipykernel', # Jupyter kernel (required for running notebooks) + 'ipywidgets', # Widgets (needed for interactive matplotlib backends) + 'ipympl', # Matplotlib Jupyter widget backend (%matplotlib widget) + 'IPython', # Interactive Python shell + 'GitPython', # Interact with Git repositories + 'build', # Building the package + 'pre-commit', # Pre-commit hooks + 'jinja2', # Templating + 'nbmake', # Building notebooks + 'nbstripout', # Strip output from notebooks + 'nbqa', # Linting and formatting notebooks + 'pytest', # Testing + 'pytest-cov', # Test coverage + 'pytest-xdist', # Enable parallel testing + 'ruff', # Linting and formatting code + 'radon', # Code complexity and maintainability + 'validate-pyproject[all]', # Validate pyproject.toml + 'versioningit', # Automatic versioning from git tags + 'jupytext', # Jupyter notebook text format support + 'jupyterquiz', # Quizzes in Jupyter notebooks + 'pydoclint', # Docstring linter + 'format-docstring', # Docstring formatter + 'docstripy', # Convert docstrings to other formats + 'interrogate', # Docstring coverage checker + 'copier', # Template management + 'mike', # MkDocs: Versioned documentation support + 'mkdocs', # Static site generator + 'mkdocs-material', # Documentation framework on top of MkDocs + 'mkdocs-autorefs', # MkDocs: Auto-references support + 'mkdocs-jupyter', # MkDocs: Jupyter notebook support + 'mkdocs-plugin-inline-svg', # MkDocs: Inline SVG support + 'mkdocs-markdownextradata-plugin', # MkDocs: Markdown extra data support, such as global variables + 'mkdocstrings-python', # MkDocs: Python docstring support + 'pyyaml', # YAML parser + 'spdx-headers', # SPDX license header validation ] [project.urls] @@ -102,7 +102,7 @@ packages = ['src/easyscience'] allow-direct-references = true [tool.hatch.version] -source = 'versioningit' # Use versioningit to manage the version +source = 'versioningit' # Use versioningit to manage the version ################################ # Configuration for versioningit @@ -118,9 +118,9 @@ source = 'versioningit' # Use versioningit to manage the version # pixi.lock update without any changes to the source code. [tool.versioningit.format] -distance = '{base_version}+dev{distance}' # example: 1.2.3.post4+dev3 -dirty = '{base_version}+dirty{distance}' # example: 0.5.8+dirty3 -distance-dirty = '{base_version}+devdirty{distance}' # example: 0.5.8+devdirty3 +distance = '{base_version}+dev{distance}' # example: 1.2.3.post4+dev3 +dirty = '{base_version}+dirty{distance}' # example: 0.5.8+dirty3 +distance-dirty = '{base_version}+devdirty{distance}' # example: 0.5.8+devdirty3 # Configure how versioningit detects versions from Git # - 'match' ensures it only considers tags starting with 'v' @@ -138,9 +138,8 @@ default-tag = 'v999.0.0' # https://interrogate.readthedocs.io/en/latest/ [tool.interrogate] -fail-under = 45 # Minimum docstring coverage percentage to pass +fail-under = 45 # Minimum docstring coverage percentage to pass verbose = 1 -exclude = ['src/easyscience/legacy'] ####################################### # Configuration for coverage/pytest-cov @@ -150,13 +149,13 @@ exclude = ['src/easyscience/legacy'] # https://coverage.readthedocs.io/en/latest/ [tool.coverage.run] -branch = true # Measure branch coverage as well -source = ['src'] # Limit coverage to the source code directory +branch = true # Measure branch coverage as well +source = ['src'] # Limit coverage to the source code directory [tool.coverage.report] show_missing = true # Show missing lines -skip_covered = false # Skip files with 100% coverage in the report -fail_under = 80 # Minimum coverage percentage to pass +skip_covered = false # Skip files with 100% coverage in the report +fail_under = 80 # Minimum coverage percentage to pass ########################## # Configuration for pytest @@ -178,33 +177,33 @@ testpaths = ['tests'] # https://docs.astral.sh/ruff/rules/ [tool.ruff] -exclude = ['tests', 'legacy', 'src/easyscience/legacy'] +exclude = ['tests'] indent-width = 4 -line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] -preview = true # Enable new rules that are not yet stable, like DOC +line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] +preview = true # Enable new rules that are not yet stable, like DOC builtins = [ 'display', -] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 +] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 # Formatting options for Ruff [tool.ruff.format] -docstring-code-format = true # Whether to format code snippets in docstrings -docstring-code-line-length = 72 # Line length for code snippets in docstrings -indent-style = 'space' # PEP 8 recommends using spaces over tabs -quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) +docstring-code-format = true # Whether to format code snippets in docstrings +docstring-code-line-length = 72 # Line length for code snippets in docstrings +indent-style = 'space' # PEP 8 recommends using spaces over tabs +quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) # Linting rules to use with Ruff [tool.ruff.lint] select = [ - # 'E', # General PEP 8 style errors - 'E9', # Runtime errors (e.g., syntax errors, undefined names) - # 'F', # Pyflakes-specific checks (e.g., unused variables, imports) - 'F63', # Issues related to invalid escape sequences in strings + # 'E', # General PEP 8 style errors + 'E9', # Runtime errors (e.g., syntax errors, undefined names) + # 'F', # Pyflakes-specific checks (e.g., unused variables, imports) + 'F63', # Issues related to invalid escape sequences in strings 'F7', # Type-related errors (e.g., mismatched argument types, unsupported operations) - 'F82', # Import-related errors (e.g., unresolved imports, reimported modules) - 'I', # Import sorting issues (e.g., unsorted imports) - # 'S', # Security-related issues (e.g., use of insecure functions or libraries) + 'F82', # Import-related errors (e.g., unresolved imports, reimported modules) + 'I', # Import sorting issues (e.g., unsorted imports) + # 'S', # Security-related issues (e.g., use of insecure functions or libraries) # 'W', # General PEP 8 warnings (e.g., lines too long, trailing whitespace) ] @@ -212,31 +211,31 @@ select = [ # Ignore specific rules globally ignore = [ - 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ + 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ # Replaced by 'D' plus pydocstyle and pydoclint - 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc # Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout - 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ + 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ ] # Ignore specific rules in certain files or directories [tool.ruff.lint.per-file-ignores] '*/__init__.py' = [ - 'F401', # re-exports are intentional in __init__.py + 'F401', # re-exports are intentional in __init__.py ] 'tests/**' = [ - 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann - 'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d - 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc - 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - 'S101', # https://docs.astral.sh/ruff/rules/assert/ + 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + 'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d + 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ + 'S101', # https://docs.astral.sh/ruff/rules/assert/ ] 'docs/**' = [ - 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - 'T201', # https://docs.astral.sh/ruff/rules/print/ + 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ + 'T201', # https://docs.astral.sh/ruff/rules/print/ ] 'docs/docs/tutorials/**' = [ - 'E402', # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ + 'E402', # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ ] # Specific options for certain rules @@ -258,7 +257,7 @@ max-complexity = 10 # https://peps.python.org/pep-0008/#maximum-line-length # Use 99 characters as the project-wide maximum for regular code lines. # Use 72 characters for docstrings. -max-line-length = 99 # See also `line-length` in [tool.ruff] +max-line-length = 99 # See also `line-length` in [tool.ruff] max-doc-length = 72 [tool.ruff.lint.pydocstyle] @@ -285,7 +284,6 @@ max-positional-args = 6 # the parameter declarations in the code (in function's signature). [tool.pydoclint] -exclude = 'src/easyscience/legacy' style = 'numpy' check-style-mismatch = true check-arg-defaults = true @@ -299,7 +297,6 @@ allow-init-docstring = true # https://github.com/jsh9/format-docstring [tool.format_docstring] -exclude = 'src/easyscience/legacy' docstring_style = 'numpy' line_length = 72 fix_rst_backticks = true diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index ae709682..2e27db53 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -7,14 +7,12 @@ # Must be executed before any other imports global_object = GlobalObject() -global_object.instantiate_stack() -global_object.stack.enabled = False -from .base_classes import ObjBase # noqa: E402 -from .fitting import AvailableMinimizers # noqa: E402 +from .base_classes import EasyList # noqa: E402 +from .base_classes import ModelBase # noqa: E402 from .fitting import Fitter # noqa: E402 -from .legacy import CollectionBase # noqa: E402 +from .fitting.minimizers import Minimizers # noqa: E402 from .variable import DescriptorNumber # noqa: E402 from .variable import Parameter # noqa: E402 @@ -23,9 +21,9 @@ __all__ = [ __version__, global_object, - ObjBase, - CollectionBase, - AvailableMinimizers, + ModelBase, + EasyList, + Minimizers, Fitter, DescriptorNumber, Parameter, diff --git a/src/easyscience/base_classes/__init__.py b/src/easyscience/base_classes/__init__.py index 6c465346..500ae1a8 100644 --- a/src/easyscience/base_classes/__init__.py +++ b/src/easyscience/base_classes/__init__.py @@ -1,11 +1,7 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from ..legacy.collection_base import CollectionBase -from ..legacy.obj_base import ObjBase -from .based_base import BasedBase +from .easy_base import EasyBase from .easy_list import EasyList from .model_base import ModelBase -from .new_base import NewBase - -__all__ = [BasedBase, CollectionBase, ObjBase, ModelBase, NewBase, EasyList] +from .serializer_base import SerializerBase diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py deleted file mode 100644 index 4044fb7b..00000000 --- a/src/easyscience/base_classes/based_base.py +++ /dev/null @@ -1,269 +0,0 @@ -# SPDX-FileCopyrightText: 2025 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from inspect import signature -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Set - -from easyscience import global_object - -from ..io import SerializerComponent -from ..variable import Parameter -from ..variable.descriptor_base import DescriptorBase - -if TYPE_CHECKING: - from ..fitting.calculators import InterfaceFactoryTemplate - - -class BasedBase(SerializerComponent): - __slots__ = ['_name', '_global_object', 'user_data', '_kwargs'] - - _REDIRECT = {} - - def __init__( - self, - name: str, - interface: Optional[InterfaceFactoryTemplate] = None, - unique_name: Optional[str] = None, - ): - self._global_object = global_object - if unique_name is None: - unique_name = self._global_object.generate_unique_name(self.__class__.__name__) - self._unique_name = unique_name - self._name = name - self._global_object.map.add_vertex(self, obj_type='created') - self.interface = interface - self.user_data: dict = {} - - @property - def _arg_spec(self) -> Set[str]: - base_cls = getattr(self, '__old_class__', self.__class__) - sign = signature(base_cls.__init__) - names = [ - param.name - for param in sign.parameters.values() - if param.kind == param.POSITIONAL_OR_KEYWORD - ] - return set(names[1:]) - - def __reduce__(self) -> tuple: - """ - Make the class picklable. - - Due to the nature of the dynamic class definitions special - measures need to be taken. - - Returns - ------- - tuple - Tuple consisting of how to make the object. - """ - state = self.encode() - cls = getattr(self, '__old_class__', self.__class__) - return cls.from_dict, (state,) - - @property - def unique_name(self) -> str: - """Get the unique name of the object.""" - return self._unique_name - - @unique_name.setter - def unique_name(self, new_unique_name: str): - """ - Set a new unique name for the object. - - The old name is still kept in the map. - - Parameters - ---------- - new_unique_name : str - New unique name for the object. - - Raises - ------ - TypeError - If ``new_unique_name`` is not a string. - """ - if not isinstance(new_unique_name, str): - raise TypeError('Unique name has to be a string.') - self._unique_name = new_unique_name - self._global_object.map.add_vertex(self) - - @property - def name(self) -> str: - """ - Get the common name of the object. - - Returns - ------- - str - Common name of the object. - """ - return self._name - - @name.setter - def name(self, new_name: str): - """ - Set a new common name for the object. - - Parameters - ---------- - new_name : str - New name for the object. - """ - self._name = new_name - - @property - def interface(self) -> InterfaceFactoryTemplate: - """Get the current interface of the object.""" - return self._interface - - @interface.setter - def interface(self, new_interface: InterfaceFactoryTemplate): - """ - Set the current interface to the object and generate bindings if - possible. - - iF.e. ``` def __init__(self, bar, interface=None, **kwargs): - super().__init__(self, **kwargs) self.foo = bar - self.interface = interface # As final step after initialization - to set correct bindings. ``` - """ - self._interface = new_interface - if new_interface is not None: - self.generate_bindings() - - def generate_bindings(self): - """ - Generate or re-generate bindings to an interface (if exists). - - Raises - ------ - AttributeError - If ``interface`` has not been set. - """ - if self.interface is None: - raise AttributeError( - 'Interface error for generating bindings. `interface` has to be set.' - ) - interfaceable_children = [ - key - for key in self._global_object.map.get_edges(self) - if issubclass(type(self._global_object.map.get_item_by_key(key)), BasedBase) - ] - for child_key in interfaceable_children: - child = self._global_object.map.get_item_by_key(child_key) - child.interface = self.interface - self.interface.generate_bindings(self) - - def switch_interface(self, new_interface_name: str): - """Switch or create a new interface.""" - if self.interface is None: - raise AttributeError( - 'Interface error for generating bindings. `interface` has to be set.' - ) - self.interface.switch(new_interface_name) - self.generate_bindings() - - def get_parameters(self) -> List[Parameter]: - """ - Get all parameter objects as a list. - - Returns - ------- - List[Parameter] - List of ``Parameter`` objects. - """ - par_list = [] - for key, item in self._kwargs.items(): - if hasattr(item, 'get_parameters'): - par_list = [*par_list, *item.get_parameters()] - elif isinstance(item, Parameter): - par_list.append(item) - return par_list - - def _get_linkable_attributes(self) -> List[DescriptorBase]: - """ - Get all objects which can be linked against as a list. - - Returns - ------- - List[DescriptorBase] - List of ``Descriptor``/`Parameter` objects. - """ - item_list = [] - for key, item in self._kwargs.items(): - if hasattr(item, '_get_linkable_attributes'): - item_list = [*item_list, *item._get_linkable_attributes()] - elif issubclass(type(item), (DescriptorBase)): - item_list.append(item) - return item_list - - def get_fit_parameters(self) -> List[Parameter]: - """ - Get all objects which can be fitted (and are not fixed) as a - list. - - Returns - ------- - List[Parameter] - List of ``Parameter`` objects which can be used in fitting. - """ - fit_list = [] - for key, item in self._kwargs.items(): - if hasattr(item, 'get_fit_parameters'): - fit_list = [*fit_list, *item.get_fit_parameters()] - elif isinstance(item, Parameter): - if item.independent and not item.fixed: - fit_list.append(item) - return fit_list - - def __dir__(self) -> Iterable[str]: - """ - This creates auto-completion and helps out in iPython notebooks. - - Returns - ------- - Iterable[str] - List of function and parameter names for auto- completion. - """ - new_class_objs = list(k for k in dir(self.__class__) if not k.startswith('_')) - return sorted(new_class_objs) - - def __copy__(self) -> BasedBase: - """Return a copy of the object.""" - temp = self.as_dict(skip=['unique_name']) - new_obj = self.__class__.from_dict(temp) - return new_obj - - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Convert an object into a full dictionary using - ``SerializerDict``. This is a shortcut for - ```obj.encode(encoder=SerializerDict)``` - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming - the dictionary. By default, None. - - Returns - ------- - Dict[str, Any] - Encoded object containing all information to reform an - EasyScience object. - """ - # extend skip to include unique_name by default - if skip is None: - skip = [] - if 'unique_name' not in skip: - skip.append('unique_name') - return super().as_dict(skip=skip) diff --git a/src/easyscience/base_classes/collection_base.py b/src/easyscience/base_classes/collection_base.py deleted file mode 100644 index c62e9382..00000000 --- a/src/easyscience/base_classes/collection_base.py +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -""" -.. deprecated:: - This module has been moved to ``easyscience.legacy.collection_base``. - Please update your imports. -""" - -from easyscience import global_object - -from ..legacy.collection_base import CollectionBase # noqa: F401 - -global_object.log.warning( - 'easyscience.base_classes.collection_base is deprecated. ' - 'Please import from easyscience.legacy.collection_base instead.', - stacklevel=2, -) diff --git a/src/easyscience/base_classes/new_base.py b/src/easyscience/base_classes/easy_base.py similarity index 74% rename from src/easyscience/base_classes/new_base.py rename to src/easyscience/base_classes/easy_base.py index e1797166..927f7813 100644 --- a/src/easyscience/base_classes/new_base.py +++ b/src/easyscience/base_classes/easy_base.py @@ -3,7 +3,6 @@ from __future__ import annotations -from inspect import signature from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -12,17 +11,15 @@ from typing import Iterable from typing import List from typing import Optional - from typing import Set from easyscience import global_object -from ..global_object.undo_redo import property_stack -from ..io.serializer_base import SerializerBase +from .serializer_base import SerializerBase -class NewBase: +class EasyBase: """ - This is the new base class for easyscience objects. + This is the base class for easyscience objects. It provides serialization capabilities as well as unique naming and display naming. @@ -38,26 +35,11 @@ def __init__(self, unique_name: Optional[str] = None, display_name: Optional[str if not isinstance(unique_name, str): raise TypeError('Unique name has to be a string.') self._unique_name = unique_name - self._global_object.map.add_vertex(self, obj_type='created') + self._global_object.registry.add(self) if display_name is not None and not isinstance(display_name, str): raise TypeError('Display name must be a string or None') self._display_name = display_name - @property - def _arg_spec(self) -> Set[str]: - """ - This method is used by the serializer to determine which - arguments are needed by the constructor to deserialize the - object. - """ - sign = signature(self.__class__.__init__) - names = [ - param.name - for param in sign.parameters.values() - if param.kind == param.POSITIONAL_OR_KEYWORD - ] - return set(names[1:]) - @property def unique_name(self) -> str: """Get the unique name of the object.""" @@ -68,7 +50,7 @@ def unique_name(self, new_unique_name: str): """ Set a new unique name for the object. - The old name is still kept in the map. + The old name is removed from the registry. Parameters ---------- @@ -79,11 +61,27 @@ def unique_name(self, new_unique_name: str): ------ TypeError If ``new_unique_name`` is not a string. + ValueError + If ``new_unique_name`` is already registered to another object. """ if not isinstance(new_unique_name, str): raise TypeError('Unique name has to be a string.') - self._unique_name = new_unique_name - self._global_object.map.add_vertex(self) + registry = self._global_object.registry + old_unique_name = self._unique_name + if new_unique_name != old_unique_name: + self._unique_name = new_unique_name + try: + registry.add(self) + except ValueError: + # Keep the old name if the new one is taken + self._unique_name = old_unique_name + raise + if old_unique_name in registry and registry[old_unique_name] is self: + # Only drop the old name if it is still ours. If it was + # discarded while this object stayed alive (e.g. in + # ModelBase.from_dict or registry.clear()), it may since + # have been given to another object. + registry.discard(old_unique_name) self._default_unique_name = False @property @@ -102,7 +100,6 @@ def display_name(self) -> str: return display_name @display_name.setter - @property_stack def display_name(self, name: str | None) -> None: """ Set the pretty display name. @@ -137,19 +134,21 @@ def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: Dict[str, Any] Encoded object containing all information to reform an EasyScience object. + + Notes + ----- + A ``unique_name`` that was generated automatically is not + written; only an explicitly supplied one is, so that a + deserialized object gets a fresh name instead of clashing with + the original. Likewise ``display_name`` is omitted when it is + ``None``. Pass ``skip`` to drop further fields. """ serializer = SerializerBase() - temp_dict = serializer._convert_to_dict(self, skip=skip, full_encode=False) - if skip is None: - skip = [] - if self._default_unique_name or 'unique_name' in skip: - temp_dict.pop('unique_name', None) - if self._display_name is None or 'display_name' in skip: - temp_dict.pop('display_name', None) - return temp_dict + skip = list(skip) if skip else [] + return serializer._convert_to_dict(self, skip=skip, full_encode=False) @classmethod - def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase: + def from_dict(cls, obj_dict: Dict[str, Any]) -> EasyBase: """ Re-create an EasyScience object from a full encoded dictionary. @@ -157,11 +156,11 @@ def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase: ---------- obj_dict : Dict[str, Any] Dictionary containing the serialized contents (from - ``SerializerDict``) of an EasyScience object. + ``to_dict``) of an EasyScience object. Returns ------- - NewBase + EasyBase Reformed EasyScience object. Raises @@ -192,7 +191,7 @@ def __dir__(self) -> Iterable[str]: new_class_objs = list(k for k in dir(self.__class__) if not k.startswith('_')) return sorted(new_class_objs) - def __copy__(self) -> NewBase: + def __copy__(self) -> EasyBase: """Return a copy of the object.""" temp = self.to_dict(skip=['unique_name']) new_obj = self.__class__.from_dict(temp) diff --git a/src/easyscience/base_classes/easy_list.py b/src/easyscience/base_classes/easy_list.py index 2effca51..7bc3d825 100644 --- a/src/easyscience/base_classes/easy_list.py +++ b/src/easyscience/base_classes/easy_list.py @@ -16,13 +16,13 @@ from typing import overload from easyscience import global_object -from easyscience.io.serializer_base import SerializerBase +from easyscience.base_classes.serializer_base import SerializerBase from easyscience.variable.descriptor_base import DescriptorBase +from .easy_base import EasyBase from .model_base import ModelBase -from .new_base import NewBase -ProtectedType_ = TypeVar('ProtectedType', bound=NewBase) +ProtectedType_ = TypeVar('ProtectedType', bound=EasyBase) class EasyList(ModelBase, MutableSequence[ProtectedType_]): @@ -30,8 +30,8 @@ class EasyList(ModelBase, MutableSequence[ProtectedType_]): # we would have to overwrite "extend", "remove", "__iadd__", "count", "append", "__iter__" and "clear" def __init__( self, - *args: ProtectedType_ | list[ProtectedType_], - protected_types: list[Type[NewBase]] | Type[NewBase] | None = None, + *args: ProtectedType_ | Iterable[ProtectedType_], + protected_types: list[Type[EasyBase]] | Type[EasyBase] | None = None, unique_name: Optional[str] = None, display_name: Optional[str] = None, **kwargs: Any, @@ -41,11 +41,15 @@ def __init__( Parameters ---------- - *args : ProtectedType_ | list[ProtectedType_] + *args : ProtectedType_ | Iterable[ProtectedType_] Initial items to add to the list. - protected_types : list[Type[NewBase]] | Type[NewBase] | None, default=None - Types that are allowed in the list. Can be a single NewBase - subclass or a list of them. If None,. By default, None. + protected_types : list[Type[EasyBase]] | Type[EasyBase] | None, default=None + Types that are allowed in the list. Can be a single EasyBase + subclass or a list of them. If None, any ``EasyBase`` object + is accepted, including descriptors and parameters. Both bare + descriptors and ``ModelBase`` items contribute to + ``get_all_variables`` and hence to fitting. By default, + None. unique_name : Optional[str], default=None Optional unique name for the list. By default, None. display_name : Optional[str], default=None @@ -56,27 +60,27 @@ def __init__( Raises ------ TypeError - If ``protected_types`` is not a supported NewBase type or + If ``protected_types`` is not a supported EasyBase type or iterable of supported types. """ super().__init__(unique_name=unique_name, display_name=display_name) if protected_types is None: - self._protected_types = [NewBase] - elif isinstance(protected_types, type) and issubclass(protected_types, NewBase): + self._protected_types = [EasyBase] + elif isinstance(protected_types, type) and issubclass(protected_types, EasyBase): self._protected_types = [protected_types] elif isinstance(protected_types, Iterable) and all( - issubclass(t, NewBase) for t in protected_types + issubclass(t, EasyBase) for t in protected_types ): self._protected_types = list(protected_types) else: raise TypeError( - 'protected_types must be a NewBase subclass or an iterable of NewBase subclasses' + 'protected_types must be an EasyBase subclass or an iterable of EasyBase subclasses' ) self._data: List[ProtectedType_] = [] # Add initial items for item in args: - if isinstance(item, list): + if isinstance(item, (list, tuple)): for sub_item in item: self.append(sub_item) else: @@ -266,12 +270,14 @@ def _get_key(self, obj: ProtectedType_) -> str: def get_all_variables(self) -> List[DescriptorBase]: """ - Get all ``Descriptor`` and ``Parameter`` objects from all - elements that are derived from ``ModelBase``. + Get all ``Descriptor`` and ``Parameter`` objects held by this + list. - For each element that is a ``ModelBase`` instance, the element's - own ``get_all_variables()`` method is called and the results are - collected into a single flat list. + Elements that are ``DescriptorBase`` instances (e.g. a bare + ``Parameter``) are collected directly, while elements derived + from ``ModelBase`` contribute the result of their own + ``get_all_variables()`` call. Everything is collected into a + single flat list. Returns ------- @@ -281,7 +287,9 @@ def get_all_variables(self) -> List[DescriptorBase]: """ all_vars: List[DescriptorBase] = [] for item in self._data: - if isinstance(item, ModelBase): + if isinstance(item, DescriptorBase): + all_vars.append(item) + elif isinstance(item, ModelBase): all_vars.extend(item.get_all_variables()) return all_vars @@ -360,11 +368,13 @@ def to_dict(self, skip: list[str] | None = None) -> dict: """ Convert the EasyList to a dictionary for serialization. + The serializer calls ``to_dict`` on nested EasyScience objects. + Parameters ---------- skip : list[str] | None, default=None List of field names as strings to skip when forming the - dictionary. + dictionary. By default, None. Returns ------- @@ -375,7 +385,7 @@ def to_dict(self, skip: list[str] | None = None) -> dict: # protected_types holds classes, which the generic encoder # cannot handle; they are encoded explicitly below. dict_repr = super().to_dict(skip=[*skip, 'protected_types']) - if self._protected_types != [NewBase]: + if self._protected_types != [EasyBase]: dict_repr['protected_types'] = [ {'@module': cls_.__module__, '@class': cls_.__name__} for cls_ in self._protected_types @@ -384,19 +394,19 @@ def to_dict(self, skip: list[str] | None = None) -> dict: return dict_repr @classmethod - def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase: + def from_dict(cls, obj_dict: Dict[str, Any]) -> EasyBase: """ Re-create an EasyScience object from a full encoded dictionary. Parameters ---------- obj_dict : Dict[str, Any] - Dictionary containing the serialized contents (from - ``SerializerDict``) of an EasyScience object. + Dictionary containing the serialized contents + of an EasyScience object. Returns ------- - NewBase + EasyBase Reformed EasyScience object. Raises diff --git a/src/easyscience/base_classes/model_base.py b/src/easyscience/base_classes/model_base.py index dee45ba5..f8d472e1 100644 --- a/src/easyscience/base_classes/model_base.py +++ b/src/easyscience/base_classes/model_base.py @@ -13,13 +13,13 @@ from typing import List from typing import Optional -from ..io import SerializerBase from ..variable import Parameter from ..variable.descriptor_base import DescriptorBase -from .new_base import NewBase +from .easy_base import EasyBase +from .serializer_base import SerializerBase -class ModelBase(NewBase): +class ModelBase(EasyBase): """ This is the base class for all model classes in EasyScience. It provides methods to get parameters for fitting and analysis as well @@ -98,15 +98,6 @@ def get_free_parameters(self) -> List[Parameter]: """ return [param for param in self.get_fittable_parameters() if not param.fixed] - def get_fit_parameters(self) -> List[Parameter]: - """ - This is an alias for ``get_free_parameters``. - - To be removed when fully moved to new base classes and minimizer - can be changed. - """ - return self.get_free_parameters() - @classmethod def from_dict(cls, obj_dict: Dict[str, Any]) -> ModelBase: """ @@ -117,7 +108,7 @@ def from_dict(cls, obj_dict: Dict[str, Any]) -> ModelBase: ---------- obj_dict : Dict[str, Any] Dictionary containing the serialized contents (from - ``SerializerDict``) of an EasyScience object. + ``to_dict``) of an EasyScience object. Returns ------- @@ -147,7 +138,7 @@ class definition. try: temp_param = getattr(cls_instance, key) setattr(cls_instance, '_' + key, value) - cls_instance._global_object.map.prune(temp_param.unique_name) + cls_instance._global_object.registry.discard(temp_param.unique_name) except Exception as e: raise SyntaxError(f"""Could not set parameter {key} during `from_dict` with full deserialized variable. \n' This should be fixed in the class definition. Error: {e}""") from e diff --git a/src/easyscience/base_classes/obj_base.py b/src/easyscience/base_classes/obj_base.py deleted file mode 100644 index 8d44b193..00000000 --- a/src/easyscience/base_classes/obj_base.py +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-FileCopyrightText: 2025 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -""" -.. deprecated:: - This module has been moved to ``easyscience.legacy.obj_base``. - Please update your imports. -""" - -from easyscience import global_object - -from ..legacy.obj_base import ObjBase # noqa: F401 - -global_object.log.warning( - 'easyscience.base_classes.obj_base is deprecated. ' - 'Please import from easyscience.legacy.obj_base instead.', - stacklevel=2, -) diff --git a/src/easyscience/io/serializer_base.py b/src/easyscience/base_classes/serializer_base.py similarity index 73% rename from src/easyscience/io/serializer_base.py rename to src/easyscience/base_classes/serializer_base.py index 3fc22d06..c5c2c089 100644 --- a/src/easyscience/io/serializer_base.py +++ b/src/easyscience/base_classes/serializer_base.py @@ -9,20 +9,15 @@ from enum import Enum from importlib import import_module from inspect import getfullargspec -from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import Dict from typing import List from typing import MutableSequence -from typing import Optional from typing import Tuple import numpy as np -if TYPE_CHECKING: - from .serializer_component import SerializerComponent - _e = json.JSONEncoder() @@ -33,20 +28,19 @@ class SerializerBase: ``encode`` and ``decode`` are abstract methods to be implemented for each serializer. It is expected that the helper function - `_convert_to_dict` will be used as a base for encoding (or the - ``SerializerDict`` as it's more flexible). + `_convert_to_dict` will be used as a base for encoding. """ @abstractmethod - def encode(self, obj: SerializerComponent, skip: Optional[List[str]] = None, **kwargs) -> any: + def encode(self, obj: Any, skip: List[str] | None = None, **kwargs) -> any: """ Abstract implementation of an encoder. Parameters ---------- - obj : SerializerComponent + obj : Any Object to be encoded. - skip : Optional[List[str]], default=None + skip : List[str] | None, default=None List of field names as strings to skip when forming the encoded object. By default, None. **kwargs : @@ -67,7 +61,6 @@ def encode(self, obj: SerializerComponent, skip: Optional[List[str]] = None, **k def decode(cls, obj: Any) -> Any: """ Re-create an EasyScience object from the output of an encoder. - The default decoder is ``SerializerDict``. Parameters ---------- @@ -87,6 +80,10 @@ def get_arg_spec(func: Callable) -> Tuple[Any, List[str]]: Get the full argument specification of a function (typically ``__init__``) + Both positional-or-keyword and keyword-only arguments are + returned, so constructors that use ``*`` to force keyword-only + arguments are serialized correctly. + Parameters ---------- func : Callable @@ -99,7 +96,7 @@ def get_arg_spec(func: Callable) -> Tuple[Any, List[str]]: """ spec = getfullargspec(func) - args = spec.args[1:] + args = spec.args[1:] + spec.kwonlyargs return spec, args @staticmethod @@ -148,14 +145,19 @@ def _encode_objs(obj: Any) -> Dict[str, Any]: def _convert_to_dict( self, - obj: SerializerComponent, - skip: Optional[List[str]] = None, + obj: Any, + skip: List[str] | None = None, full_encode: bool = False, **kwargs, ) -> dict: """A JSON serializable dict representation of an object.""" if skip is None: skip = [] + own_skip = list(skip) + if getattr(obj, '_default_unique_name', False) and 'unique_name' not in own_skip: + own_skip.append('unique_name') + if hasattr(obj, '_display_name') and obj._display_name is None: + own_skip.append('display_name') if full_encode: new_obj = SerializerBase._encode_objs(obj) @@ -175,8 +177,6 @@ def _convert_to_dict( if hasattr(obj, '_arg_spec'): args = obj._arg_spec - redirect = getattr(obj, '_REDIRECT', {}) - def runner(o): if full_encode: return SerializerBase._encode_objs(o) @@ -184,76 +184,36 @@ def runner(o): return o for c in args: - if c not in skip: - if c in redirect.keys(): - if redirect[c] is None: - continue - a = runner(redirect[c](obj)) - else: - try: - a = runner(obj.__getattribute__(c)) - except AttributeError: - try: - a = runner(obj.__getattribute__('_' + c)) - except AttributeError: - err = True - if hasattr(obj, 'kwargs'): - # type: ignore - option = getattr(obj, 'kwargs') - if hasattr(option, c): - v = getattr(option, c) - delattr(option, c) - d.update(runner(v)) # pylint: disable=E1101 - err = False - if hasattr(obj, '_kwargs'): - # type: ignore - option = getattr(obj, '_kwargs') - if hasattr(option, c): - v = getattr(option, c) - delattr(option, c) - d.update(runner(v)) # pylint: disable=E1101 - err = False - if err: - raise NotImplementedError( - 'Unable to automatically determine as_dict ' - 'format from class. MSONAble requires all ' - 'args to be present as either self.argname or ' - 'self._argname, and kwargs to be present under' - 'a self.kwargs variable to automatically ' - 'determine the dict format. Alternatively, ' - 'you can implement both as_dict and from_dict.' - ) - d[c] = self._recursive_encoder( - a, skip=skip, encoder=self, full_encode=full_encode, **kwargs - ) + if c in own_skip: + continue + try: + a = runner(obj.__getattribute__(c)) + except AttributeError: + try: + a = runner(obj.__getattribute__('_' + c)) + except AttributeError: + raise NotImplementedError( + 'Unable to automatically determine to_dict ' + 'format from class. All args must be present ' + 'as either self.argname or self._argname to ' + 'automatically determine the dict format. ' + 'Alternatively, you can implement both to_dict ' + 'and from_dict.' + ) from None + d[c] = self._recursive_encoder( + a, skip=skip, encoder=self, full_encode=full_encode, **kwargs + ) if spec.varargs is not None and getattr(obj, spec.varargs, None) is not None: d.update({spec.varargs: getattr(obj, spec.varargs)}) - if hasattr(obj, '_kwargs'): - if not issubclass(type(obj), MutableSequence): - d_k = list(d.keys()) - for k, v in getattr(obj, '_kwargs').items(): - # We should have already obtained `key` and `_key` - if k not in skip and k not in d_k: - if k[0] == '_' and k[1:] in d_k: - continue - vv = v - if k in redirect.keys(): - if redirect[k] is None: - continue - vv = redirect[k](obj) - v_ = runner(vv) - d[k] = self._recursive_encoder( - v_, - skip=skip, - encoder=self, - full_encode=full_encode, - **kwargs, - ) if isinstance(obj, Enum): d.update({'value': runner(obj.value)}) # pylint: disable=E1101 if hasattr(obj, '_convert_to_dict'): d = obj._convert_to_dict(d, self, skip=skip, **kwargs) - if hasattr(obj, '_global_object') and 'unique_name' not in d and 'unique_name' not in skip: + if ( + hasattr(obj, '_global_object') + and 'unique_name' not in d + and 'unique_name' not in own_skip + ): d['unique_name'] = obj.unique_name return d @@ -304,7 +264,9 @@ def _convert_from_dict(d: Any) -> Any: return np.array(d['data'], dtype=d['dtype']) if issubclass(T_, (list, MutableSequence)): - return [SerializerBase._convert_from_dict(x) for x in d] + # Elements that are serialized EasyScience objects are rebuilt + # by their own ``from_dict``; anything else is decoded here. + return [SerializerBase._deserialize_value(x) for x in d] return d @staticmethod @@ -434,7 +396,10 @@ def _recursive_encoder( ): # strings have encode return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) elif hasattr(obj, 'to_dict') and obj.__class__.__module__.startswith('easy'): - return obj.to_dict(skip=skip) + # EasyScience objects serialize themselves, so that + # ``to_dict`` overrides (e.g. EasyList, Parameter) apply + # when the object is nested as well as at the top level. + return obj.to_dict(skip=list(skip)) else: return [ self._recursive_encoder(it, skip, encoder, full_encode, **kwargs) for it in obj @@ -449,5 +414,5 @@ def _recursive_encoder( ): # strings have encode return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) elif hasattr(obj, 'to_dict') and obj.__class__.__module__.startswith('easy'): - return obj.to_dict(skip=skip) + return obj.to_dict(skip=list(skip)) return obj diff --git a/src/easyscience/fitting/__init__.py b/src/easyscience/fitting/__init__.py index 29e6ddef..a3956527 100644 --- a/src/easyscience/fitting/__init__.py +++ b/src/easyscience/fitting/__init__.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from .available_minimizers import AvailableMinimizers from .fitter import Fitter from .minimizers.utils import FitResults from .sampler import Sampler diff --git a/src/easyscience/fitting/calculators/__init__.py b/src/easyscience/fitting/calculators/__init__.py deleted file mode 100644 index 4104ddae..00000000 --- a/src/easyscience/fitting/calculators/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: 2025 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from .interface_factory import InterfaceFactoryTemplate - -__all__ = [InterfaceFactoryTemplate] diff --git a/src/easyscience/fitting/calculators/interface_factory.py b/src/easyscience/fitting/calculators/interface_factory.py deleted file mode 100644 index ad5fbc24..00000000 --- a/src/easyscience/fitting/calculators/interface_factory.py +++ /dev/null @@ -1,268 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Any -from typing import Callable -from typing import List -from typing import NamedTuple -from typing import Optional -from typing import Type - -from easyscience import global_object - -if TYPE_CHECKING: - from abc import ABCMeta - - from .. import Fitter - - -class InterfaceFactoryTemplate: - """ - This class allows for the creation and transference of interfaces. - """ - - def __init__(self, interface_list: List[ABCMeta], *args, **kwargs): - self._interfaces: List[ABCMeta] = interface_list - self._current_interface: ABCMeta - self.__interface_obj: None = None - self.create(*args, **kwargs) - - def create(self, *args: Any, interface_name: str | None = None, **kwargs: Any) -> None: - """ - Create an interface to a calculator from those initialized. - - Interfaces can be selected by ``interface_name`` where - ``interface_name`` is one of ``obj.available_interfaces``. This - interface can now be accessed by obj(). - - Parameters - ---------- - *args : Any - Positional arguments forwarded to the interface constructor. - interface_name : str | None, default=None - Name of interface to be created. - - **kwargs : Any - Keyword arguments forwarded to the interface constructor. - - Raises - ------ - NotImplementedError - If no interfaces are available to instantiate. - """ - if interface_name is None: - if len(self._interfaces) > 0: - # Fallback name - interface_name = self.return_name(self._interfaces[0]) - else: - raise NotImplementedError - interfaces = self.available_interfaces - if interface_name in interfaces: - self._current_interface = self._interfaces[interfaces.index(interface_name)] - self.__interface_obj = self._current_interface(*args, **kwargs) - - def switch(self, new_interface: str, fitter: Optional[Type[Fitter]] = None) -> None: - """ - Changes the current interface to a new interface. - - The current interface is destroyed and all SerializerComponent - parameters carried over to the new interface. i.e. pick up where - you left off. - - Parameters - ---------- - new_interface : str - Name of new interface to be created. - fitter : Optional[Type[Fitter]], default=None - Fitting interface which contains the fitting object which - may have bindings which will be updated. By default, None. - - Raises - ------ - AttributeError - If ``new_interface`` is not a valid interface name. - """ - interfaces = self.available_interfaces - if new_interface in interfaces: - self._current_interface = self._interfaces[interfaces.index(new_interface)] - self.__interface_obj = self._current_interface() - else: - raise AttributeError('The user supplied interface is not valid.') - if fitter is not None: - if hasattr(fitter, '_fit_object'): - obj = getattr(fitter, '_fit_object') - try: - if hasattr(obj, 'update_bindings'): - obj.update_bindings() - except Exception as e: - global_object.log.getLogger('fitting.calculators').warning( - 'Unable to auto generate bindings.\n%s', e - ) - elif hasattr(fitter, 'generate_bindings'): - try: - fitter.generate_bindings() - except Exception as e: - global_object.log.getLogger('fitting.calculators').warning( - 'Unable to auto generate bindings.\n%s', e - ) - - @property - def available_interfaces(self) -> List[str]: - """ - Return all available interfaces. - - Returns - ------- - List[str] - List of available interface names. - """ - return [self.return_name(this_interface) for this_interface in self._interfaces] - - @property - def current_interface(self) -> ABCMeta: - """ - Returns the constructor for the currently selected interface. - - Returns - ------- - ABCMeta - Interface constructor. - """ - return self._current_interface - - @property - def current_interface_name(self) -> str: - """ - Returns the constructor name for the currently selected - interface. - - Returns - ------- - str - Interface constructor name. - """ - return self.return_name(self._current_interface) - - @property - def fit_func( - self, - ) -> Callable: # , x_array: np.ndarray, *args, **kwargs) -> np.ndarray: - """ - Pass through to the underlying interfaces fitting function. - - Returns - ------- - Callable - Callable proxy to the underlying interface fit function. - """ - - def __fit_func(*args, **kwargs): - return self.__interface_obj.fit_func(*args, **kwargs) - - return __fit_func - - def call(self, *args, **kwargs): - return self.fit_func(*args, **kwargs) - - def generate_bindings(self, model: Any, *args: Any, ifun: Any = None, **kwargs: Any) -> None: - """ - Automatically bind a ``Parameter`` to the corresponding - interface. - - Parameters - ---------- - model : Any - Model whose linkable attributes should be bound. - *args : Any - Positional arguments reserved for interface-specific binding - hooks. - ifun : Any, default=None - Optional interface hook. By default, None. - **kwargs : Any - Keyword arguments reserved for interface-specific binding - hooks. - """ - - class_links = self.__interface_obj.create(model) - props = model._get_linkable_attributes() - props_names = [prop.name for prop in props] - for item in class_links: - for item_key in item.name_conversion.keys(): - if item_key not in props_names: - continue - idx = props_names.index(item_key) - prop = props[idx] - - # Should be fetched this way to ensure we don't get value from callback - if hasattr(prop, 'value_no_call_back'): - # Property object - prop_value = prop.value_no_call_back - else: - # Descriptor object - prop_value = prop.value - - prop._callback = item.make_prop(item_key) - prop._callback.fset(prop_value) - - def __call__(self, *args, **kwargs) -> None: - return self.__interface_obj - - def __reduce__(self): - return ( - self.__state_restore__, - ( - self.__class__, - self.current_interface_name, - ), - ) - - @staticmethod - def __state_restore__(cls, interface_str): - obj = cls() - if interface_str in obj.available_interfaces: - obj.switch(interface_str) - return obj - - @staticmethod - def return_name(this_interface) -> str: - """Return an interfaces name.""" - interface_name = this_interface.__name__ - if hasattr(this_interface, 'name'): - interface_name = getattr(this_interface, 'name') - return interface_name - - -class ItemContainer(NamedTuple): - link_name: str - name_conversion: dict - getter_fn: Callable - setter_fn: Callable - - def make_prop(self, parameter_name) -> property: - return property( - fget=self.__make_getter(parameter_name), - fset=self.__make_setter(parameter_name), - ) - - def convert_key(self, lookup_key: str) -> str: - key = self.name_conversion.get(lookup_key, None) - return key - - def __make_getter(self, get_name: str) -> Callable: - - def get_value(): - inner_key = self.name_conversion.get(get_name, None) - return self.getter_fn(self.link_name, inner_key) - - return get_value - - def __make_setter(self, get_name: str) -> Callable: - - def set_value(value): - inner_key = self.name_conversion.get(get_name, None) - self.setter_fn(self.link_name, **{inner_key: value}) - - return set_value diff --git a/src/easyscience/fitting/engine_base.py b/src/easyscience/fitting/engine_base.py new file mode 100644 index 00000000..f72643a4 --- /dev/null +++ b/src/easyscience/fitting/engine_base.py @@ -0,0 +1,241 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from abc import ABCMeta +from inspect import Parameter as InspectParameter +from inspect import Signature +from inspect import _empty +from typing import Callable +from typing import Dict +from typing import Tuple + +import numpy as np + +from easyscience.variable import Parameter + +PARAMETER_PREFIX = 'p' + + +class EngineBase(metaclass=ABCMeta): + """ + Base for all evaluation engines: minimizers and samplers. + + An engine binds an EasyScience object and a fit function, and + repeatedly evaluates the function while writing values back into the + object's ``Parameter`` instances. ``EngineBase`` owns this shared + system: the parameter cache, the ``Parameter``-writing wrapped + fit function, and value restore on failure. It deliberately declares + no abstract methods: the interfaces are defined on its subclasses + (``MinimizerBase.fit``, ``DreamSampler.run``). + """ + + package: str = None + + def __init__( + self, + obj, + fit_function: Callable, + ): + self._object = obj + self._original_fit_function = fit_function + self._cached_pars: Dict[str, Parameter] = {} + self._cached_pars_vals: Dict[str, Tuple[float, float]] = {} + self._fit_function = None + + def _restore_parameter_values(self) -> None: + for key in self._cached_pars.keys(): + self._cached_pars[key].value = self._cached_pars_vals[key][0] + self._cached_pars[key].error = self._cached_pars_vals[key][1] + + @staticmethod + def validate_arrays(x: np.ndarray, y: np.ndarray, weights: np.ndarray) -> None: + """Validate the (x, y, weights) arrays handed to an engine. + + Parameters + ---------- + x : np.ndarray + Independent variable array. + y : np.ndarray + Dependent variable array. + weights : np.ndarray + Weight array. + + Raises + ------ + ValueError + If any array is non-numeric, the shapes disagree, x or y contain + NaN or infinite values, or the weights are non-finite or + non-positive. + """ + # A non-numeric dtype (an object array holding None, or a string + # array) would otherwise reach np.isfinite and raise an opaque numpy + # TypeError instead of the ValueError documented here. + for name, arr in (('x', x), ('y', y), ('weights', weights)): + if not np.issubdtype(arr.dtype, np.number): + raise ValueError(f'{name} must hold numeric values, got dtype {arr.dtype}.') + + if y.shape != x.shape: + raise ValueError('x and y must have the same shape.') + + if not np.isfinite(x).all(): + raise ValueError('x cannot contain NaN or infinite values.') + if not np.isfinite(y).all(): + raise ValueError('y cannot contain NaN or infinite values.') + + if weights.shape != x.shape: + raise ValueError('Weights must have the same shape as x and y.') + + if not np.isfinite(weights).all(): + raise ValueError('Weights cannot be NaN or infinite.') + + if (weights <= 0).any(): + raise ValueError('Weights must be strictly positive and non-zero.') + + def evaluate( + self, x: np.ndarray, parameters: dict[str, float] | None = None, **kwargs + ) -> np.ndarray: + """ + Evaluate the fit function for values of x. + + Parameters used are either the latest or user supplied. If the + parameters are user supplied, it must be in a dictionary of + {'parameter_name': parameter_value,...}. + + Parameters + ---------- + x : np.ndarray + X values for which the fit function will be evaluated. + parameters : dict[str, float] | None, default=None + Dictionary of parameters which will be used in the fit + function. They must be in a dictionary of {'parameter_name': + parameter_value,...}. By default, None. + **kwargs : + Additional arguments. + + Returns + ------- + np.ndarray + Y values calculated at points x for a set of parameters. + + Raises + ------ + TypeError + If ``parameters`` is not a dictionary. + """ + if parameters is None: + parameters = {} + if not isinstance(parameters, dict): + raise TypeError('parameters must be a dictionary') + + if self._fit_function is None: + # This will also generate self._cached_pars + self._fit_function = self._generate_fit_function() + + parameters = self._prepare_parameters(parameters) + + return self._fit_function(x, **parameters, **kwargs) + + def _prepare_parameters(self, parameters: dict[str, float]) -> dict[str, float]: + """ + Prepare the parameters for the engine. + + Parameters + ---------- + parameters : dict[str, float] + Dict of parameters for the engine with names as keys. + + Returns + ------- + dict[str, float] + Completed parameter dictionary for the engine. + """ + pars = self._cached_pars + + for name, item in pars.items(): + parameter_name = PARAMETER_PREFIX + str(name) + if parameter_name not in parameters.keys(): + parameters[parameter_name] = item.value + return parameters + + def _generate_fit_function(self) -> Callable: + """ + Using the user supplied ``fit_function``, wrap it in such a way + we can update ``Parameter`` on iterations. + + Returns + ------- + Callable + A fit function which is compatible with bumps models. + """ + # Original fit function + func = self._original_fit_function + # Get a list of `Parameters` + self._cached_pars = {} + self._cached_pars_vals = {} + for parameter in self._object.get_free_parameters(): + key = parameter.unique_name + self._cached_pars[key] = parameter + self._cached_pars_vals[key] = (parameter.value, parameter.error) + + # Make a new fit function + def _fit_function(x: np.ndarray, **kwargs) -> np.ndarray: + """ + Wrapped fit function which now has an EasyScience compatible + form. + + Parameters + ---------- + x : np.ndarray + Array of data points to be calculated. + **kwargs : + Key word arguments. + + Returns + ------- + np.ndarray + Points calculated at ``x``. + """ + # Update the `Parameter` values + # TODO THIS IS NOT THREAD SAFE :-( + + for name, value in kwargs.items(): + par_name = name[1:] + if par_name in self._cached_pars.keys(): + # This will take into account constraints + if self._cached_pars[par_name].value != value: + self._cached_pars[par_name].value = value + + # TODO Pre processing here + return_data = func(x) + # TODO Loading or manipulating data here + return return_data + + _fit_function.__signature__ = self._create_signature(self._cached_pars) + return _fit_function + + @staticmethod + def _create_signature(parameters: Dict[int, Parameter]) -> Signature: + """ + Wrap the function signature. + + This is done as lmfit wants the function to be in the form: f = + (x, a=1, b=2)... Where we need to be generic. Note that this + won't hold for much outside of this scope. + """ + wrapped_parameters = [] + wrapped_parameters.append( + InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty) + ) + + for name, parameter in parameters.items(): + default_value = parameter.value + + wrapped_parameters.append( + InspectParameter( + PARAMETER_PREFIX + str(name), + InspectParameter.POSITIONAL_OR_KEYWORD, + annotation=_empty, + default=default_value, + ) + ) + return Signature(wrapped_parameters) diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 7cb948e0..7643b62e 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -4,20 +4,17 @@ import functools from typing import Callable from typing import List -from typing import Optional -from typing import Union import numpy as np -from easyscience import global_object - -from .available_minimizers import AvailableMinimizers -from .available_minimizers import from_string_to_enum from .minimizers import FitResults from .minimizers import MinimizerBase +from .minimizers import Minimizers from .minimizers.factory import factory +from .reshaping import inject_x +from .reshaping import reshape_dataset -DEFAULT_MINIMIZER = AvailableMinimizers.LMFit_leastsq +DEFAULT_MINIMIZER = Minimizers.LMFit_leastsq class Fitter: @@ -34,7 +31,7 @@ def __init__(self, fit_object, fit_function: Callable): self._max_evaluations: int = None self._minimizer: MinimizerBase = None # set in _update_minimizer - self._enum_current_minimizer: AvailableMinimizers = None # set in _update_minimizer + self._enum_current_minimizer: Minimizers = None # set in _update_minimizer self._update_minimizer(DEFAULT_MINIMIZER) def make_model(self, pars=None) -> Callable: @@ -43,13 +40,10 @@ def make_model(self, pars=None) -> Callable: def evaluate(self, pars=None) -> np.ndarray: return self._minimizer.evaluate(pars) - def convert_to_pars_obj(self, pars) -> object: - return self._minimizer.convert_to_pars_obj(pars) - # TODO: remove this method when we are ready to adjust the dependent products def initialize(self, fit_object: object, fit_function: Callable) -> None: """ - Set the model and callable in the calculator interface. + Set the model and callable. Parameters ---------- @@ -62,42 +56,29 @@ def initialize(self, fit_object: object, fit_function: Callable) -> None: self._fit_function = fit_function self._update_minimizer(DEFAULT_MINIMIZER) - # TODO: remove this method when we are ready to adjust the dependent products - def create(self, minimizer_enum: Union[AvailableMinimizers, str] = DEFAULT_MINIMIZER) -> None: - """ - Create the required minimizer. - - Parameters - ---------- - minimizer_enum : Union[AvailableMinimizers, str], default=DEFAULT_MINIMIZER - The enum of the minimization engine to create. By default, - DEFAULT_MINIMIZER. - """ - if isinstance(minimizer_enum, str): - global_object.log.getLogger('fitting').warning( - 'minimizer should be set with enum %s', minimizer_enum - ) - minimizer_enum = from_string_to_enum(minimizer_enum) - self._update_minimizer(minimizer_enum) - - def switch_minimizer(self, minimizer_enum: Union[AvailableMinimizers, str]) -> None: + def switch_minimizer(self, minimizer_enum: Minimizers) -> None: """ Switch minimizer and initialize. Parameters ---------- - minimizer_enum : Union[AvailableMinimizers, str] + minimizer_enum : Minimizers The enum of the minimizer to create and instantiate. - """ - if isinstance(minimizer_enum, str): - global_object.log.getLogger('fitting').warning( - 'minimizer should be set with enum %s', minimizer_enum - ) - minimizer_enum = from_string_to_enum(minimizer_enum) + Raises + ------ + TypeError + If ``minimizer_enum`` is not a ``Minimizers`` + member. + """ + if not isinstance(minimizer_enum, Minimizers): + raise TypeError( + f'minimizer must be a Minimizers member, got {minimizer_enum!r}; ' + f'use Minimizers[name] to convert a name' + ) self._update_minimizer(minimizer_enum) - def _update_minimizer(self, minimizer_enum: AvailableMinimizers) -> None: + def _update_minimizer(self, minimizer_enum: Minimizers) -> None: self._minimizer = factory( minimizer_enum=minimizer_enum, fit_object=self._fit_object, @@ -106,16 +87,16 @@ def _update_minimizer(self, minimizer_enum: AvailableMinimizers) -> None: self._enum_current_minimizer = minimizer_enum @property - def available_minimizers(self) -> List[str]: + def available_minimizers(self) -> List[Minimizers]: """ - Get a list of the names of available fitting minimizers. + Get a list of the available fitting minimizers. Returns ------- - List[str] + List[Minimizers] List of available fitting minimizers. """ - return [minimize.name for minimize in AvailableMinimizers] + return list(Minimizers) @property def minimizer(self) -> MinimizerBase: @@ -211,6 +192,9 @@ def fit_object(self) -> object: """ Get the EasyScience object used as a model. + For a ``Fitter`` this is not one of the supplied fit + objects but a read-only, indexable and iterable aggregate. + Returns ------- object @@ -232,17 +216,17 @@ def fit_object(self, fit_object: object) -> None: self._update_minimizer(self._enum_current_minimizer) def _fit_function_wrapper( - self, real_x: Optional[np.ndarray] = None, flatten: bool = True + self, + real_x: np.ndarray | None = None, + flatten: bool = True, ) -> Callable: """ - Simple fit function which injects the real X (independent) - values into the optimizer function. - - This will also flatten the results if needed. + Wrap the fit function so it evaluates on the real X (independent) + values instead of the optimizer's dummy x, flattening if needed. Parameters ---------- - real_x : Optional[np.ndarray], default=None + real_x : np.ndarray | None, default=None Independent x parameters to be injected. By default, None. flatten : bool, default=True Should the result be a flat 1D array? By default, True. @@ -252,18 +236,7 @@ def _fit_function_wrapper( Callable Wrapped optimizer function. """ - fun = self._fit_function - - @functools.wraps(fun) - def wrapped_fit_function(x, **kwargs): - if real_x is not None: - x = real_x - dependent = fun(x, **kwargs) - if flatten: - dependent = dependent.flatten() - return dependent - - return wrapped_fit_function + return inject_x(self._fit_function, real_x, flatten=flatten) @property def fit(self) -> Callable: @@ -281,9 +254,9 @@ def fit(self) -> Callable: def inner_fit_callable( x: np.ndarray, y: np.ndarray, - weights: Optional[np.ndarray] = None, + weights: np.ndarray | None = None, vectorized: bool = False, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, **kwargs, ) -> FitResults: """ @@ -325,72 +298,7 @@ def inner_fit_callable( return inner_fit_callable - @staticmethod - def _precompute_reshaping( - x: np.ndarray, - y: np.ndarray, - weights: Optional[np.ndarray], - vectorized: bool, - ) -> tuple[np.ndarray, np.ndarray, np.ndarray, Optional[np.ndarray], tuple[int, ...]]: - """ - Check the dimensions of the inputs and reshape if necessary. - - Parameters - ---------- - x : np.ndarray - ND matrix of dependent points. - y : np.ndarray - N-1D matrix of independent points. - weights : Optional[np.ndarray] - Optional weights for the fit. - vectorized : bool - Whether ``x`` already stores vectorized coordinates. - - Returns - ------- - tuple[np.ndarray, np.ndarray, np.ndarray, Optional[np.ndarray], tuple[int, ...]] - Reshaped x values, reshaped input data, flattened y values, - flattened weights, and the original x shape. - - Raises - ------ - ValueError - If the shapes of ``x`` and ``y`` are incompatible. - """ - # Make sure that they are np arrays - x_new = np.array(x) - y_new = np.array(y) - # Get the shape - x_shape = x_new.shape - # Check if the x data is 1D - if len(x_shape) > 1: - # It is ND data - # Check if the data is vectorized. i.e. should x be [NxMx...x Ndims] - if vectorized: - # Assert that the shapes are the same - if np.all(x_shape[:-1] != y_new.shape): - raise ValueError('The shape of the x and y data must be the same') - # If so do nothing but note that the data is vectorized - # x_shape = (-1,) # Should this be done? - else: - # Assert that the shapes are the same - if np.prod(x_new.shape[:-1]) != y_new.size: - raise ValueError('The number of elements in x and y data must be the same') - # Reshape the data to be [len(NxMx..), Ndims] i.e. flatten to columns - x_new = x_new.reshape(-1, x_shape[-1], order='F') - else: - # Assert that the shapes are the same - if np.all(x_shape != y_new.shape): - raise ValueError('The shape of the x and y data must be the same') - # It is 1D data - x_new = x.flatten() - # The optimizer needs a 1D array, flatten the y data - y_new = y_new.flatten() - if weights is not None: - weights = np.array(weights).flatten() - # Make a 'dummy' x array for the fit function - x_for_fit = np.array(range(y_new.size)) - return x_for_fit, x_new, y_new, weights, x_shape + _precompute_reshaping = staticmethod(reshape_dataset) @staticmethod def _post_compute_reshaping( @@ -418,112 +326,3 @@ def _post_compute_reshaping( fit_result.y_calc = np.reshape(fit_result.y_calc, y.shape) fit_result.y_err = np.reshape(fit_result.y_err, y.shape) return fit_result - - def mcmc_sample( - self, - x: np.ndarray, - y: np.ndarray, - weights: np.ndarray, - samples: int = 10000, - burn: int = 2000, - thin: int = 10, - population: Optional[int] = None, - vectorized: bool = False, - sampler_kwargs: Optional[dict] = None, - progress_callback: Optional[Callable[[dict], Optional[bool]]] = None, - abort_test: Optional[Callable[[], bool]] = None, - ) -> dict: - """ - Run Bayesian MCMC sampling using the BUMPS DREAM sampler. - - Works with both a plain ``Fitter`` (single dataset) and a - ``MultiFitter`` (multiple datasets) via polymorphic dispatch: - ``_precompute_reshaping`` and ``_fit_function_wrapper`` are - resolved on the concrete subclass at call time, so multi-dataset - flattening is handled automatically when called on a - ``MultiFitter`` instance. - - Parameters - ---------- - x : np.ndarray - Independent variable array (or list of arrays for - ``MultiFitter``). - y : np.ndarray - Dependent variable array (or list of arrays for - ``MultiFitter``). - weights : np.ndarray - Weight array (or list of arrays for ``MultiFitter``). - samples : int, default=10000 - Number of retained DREAM samples requested from BUMPS. - burn : int, default=2000 - Burn-in steps to discard before collecting samples. - thin : int, default=10 - Thinning interval β€” only every ``thin``-th sample is kept, - which reduces autocorrelation between consecutive draws. - population : Optional[int], default=None - BUMPS DREAM population count (number of parallel chains). - vectorized : bool, default=False - When ``True``, each x array may be multi-dimensional (e.g. - an ``(N, M, 2)`` grid for a 2D model) and is left as-is. - When ``False`` (default), each x array is expected to be - 1-D. - sampler_kwargs : Optional[dict], default=None - Additional keyword arguments forwarded to the BUMPS DREAM - sampler. - progress_callback : Optional[Callable[[dict], Optional[bool]]], default=None - Optional callback invoked at each DREAM generation. The - payload dict includes ``iteration`` and ``sampling: True``. - abort_test : Optional[Callable[[], bool]], default=None - Optional callable that returns ``True`` to abort sampling - early. - - Returns - ------- - dict - Dictionary with keys ``'draws'``, ``'param_names'``, - ``'internal_bumps_object'``, and ``'logp'``. - - Raises - ------ - ValueError - If ``samples``, ``burn``, or ``thin`` are invalid. - RuntimeError - If the active minimizer is not a BUMPS instance. - """ - if not isinstance(samples, int) or samples <= 0: - raise ValueError('samples must be a positive integer.') - if not isinstance(burn, int) or burn < 0: - raise ValueError('burn must be a non-negative integer.') - if not isinstance(thin, int) or thin < 1: - raise ValueError('thin must be a positive integer.') - - x_fit, x_new, y_new, w_new, dims = self._precompute_reshaping(x, y, weights, vectorized) - self._dependent_dims = dims - - original_fit_func = self._fit_function - self.fit_function = self._fit_function_wrapper(x_new, flatten=True) - - try: - minimizer = self.minimizer - if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): - raise RuntimeError( - 'Bayesian sampling requires a BUMPS minimizer. ' - 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' - ) - - result = minimizer.mcmc_sample( - x=x_fit, - y=y_new, - weights=w_new, - samples=samples, - burn=burn, - thin=thin, - population=population, - sampler_kwargs=sampler_kwargs, - progress_callback=progress_callback, - abort_test=abort_test, - ) - finally: - self.fit_function = original_fit_func - - return result diff --git a/src/easyscience/fitting/minimizers/__init__.py b/src/easyscience/fitting/minimizers/__init__.py index 0ee6cc6b..40006813 100644 --- a/src/easyscience/fitting/minimizers/__init__.py +++ b/src/easyscience/fitting/minimizers/__init__.py @@ -5,5 +5,6 @@ from .minimizer_bumps import Bumps from .minimizer_dfo import DFO from .minimizer_lmfit import LMFit +from .minimizers import Minimizers from .utils import FitError from .utils import FitResults diff --git a/src/easyscience/fitting/minimizers/bumps_utils/__init__.py b/src/easyscience/fitting/minimizers/bumps_utils/__init__.py index 35255615..82b5dfc6 100644 --- a/src/easyscience/fitting/minimizers/bumps_utils/__init__.py +++ b/src/easyscience/fitting/minimizers/bumps_utils/__init__.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause from .eval_counter import EvalCounter +from .problem import build_curve_problem +from .problem import parameter_names +from .problem import parameter_snapshot +from .problem import to_bumps_parameter from .progress_monitor import BumpsProgressMonitor - -__all__ = ['BumpsProgressMonitor', 'EvalCounter'] diff --git a/src/easyscience/fitting/minimizers/bumps_utils/problem.py b/src/easyscience/fitting/minimizers/bumps_utils/problem.py new file mode 100644 index 00000000..df471cfc --- /dev/null +++ b/src/easyscience/fitting/minimizers/bumps_utils/problem.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""BUMPS problem construction shared by the ``Bumps`` minimizer and +``DreamSampler``. + +These are free functions rather than ``Bumps`` methods so that any +:class:`~easyscience.fitting.engine_base.EngineBase`: a minimizer or a +sampler, can build a BUMPS ``Curve``/``FitProblem`` without inheriting +from the minimizer. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from bumps.names import Curve +from bumps.names import FitProblem +from bumps.parameter import Parameter as BumpsParameter + +from easyscience.variable import Parameter + +from ...engine_base import PARAMETER_PREFIX +from .eval_counter import EvalCounter + +if TYPE_CHECKING: + from ...engine_base import EngineBase + + +def to_bumps_parameter(par: Parameter) -> BumpsParameter: + """Convert an EasyScience ``Parameter`` to a prefixed ``BumpsParameter``. + + Parameters + ---------- + par : Parameter + EasyScience parameter to convert. + + Returns + ------- + BumpsParameter + Bumps Parameter compatible object, named + ``PARAMETER_PREFIX + par.unique_name``. + """ + return BumpsParameter( + name=PARAMETER_PREFIX + par.unique_name, + value=par.value, + bounds=[par.min, par.max], + fixed=par.fixed, + ) + + +def build_curve_problem( + engine: 'EngineBase', + x: np.ndarray, + y: np.ndarray, + weights: np.ndarray, +) -> tuple[FitProblem, EvalCounter, Curve]: + """Build a BUMPS ``FitProblem`` around an engine's wrapped fit function. + + Wraps ``engine._generate_fit_function()`` in an :class:`EvalCounter`, + converts the engine's cached parameters via + :func:`to_bumps_parameter`, and assembles + ``Curve(fit_func, x, y, dy=1/weights, **bumps_pars)`` into a + ``FitProblem``. + + Parameters + ---------- + engine : EngineBase + The engine (minimizer or sampler) supplying the fit function and + parameter cache. + x : np.ndarray + Independent variable array. + y : np.ndarray + Dependent variable array. + weights : np.ndarray + Weight array; converted to ``dy = 1 / weights``. + + Returns + ------- + tuple[FitProblem, EvalCounter, Curve] + The assembled problem, the evaluation counter wrapping the fit + function, and the ``Curve`` model itself. + The ``Curve`` is returned because ``FitProblem.fitness`` is + deprecated in current BUMPS. + """ + fit_func = EvalCounter(engine._generate_fit_function()) + + bumps_pars = { + PARAMETER_PREFIX + str(name): to_bumps_parameter(par) + for name, par in engine._cached_pars.items() + } + + curve = Curve(fit_func, x, y, dy=1 / weights, **bumps_pars) + return FitProblem(curve), fit_func, curve + + +def parameter_names(problem: FitProblem) -> list[str]: + """Return the problem's parameter names with the prefix stripped. + + Parameters + ---------- + problem : FitProblem + A BUMPS problem built by :func:`build_curve_problem`. + + Returns + ------- + list[str] + Parameter names in problem order, without ``PARAMETER_PREFIX``. + """ + return [(p.name or '')[len(PARAMETER_PREFIX) :] for p in problem._parameters] + + +def parameter_snapshot(problem: FitProblem, point: np.ndarray | None) -> dict: + """Snapshot the problem's parameter values as ``{name: value}``. + + Parameters + ---------- + problem : FitProblem + A BUMPS problem built by :func:`build_curve_problem`. + point : np.ndarray | None + Parameter values to report; when ``None`` the problem's current + values (``problem.getp()``) are used. + + Returns + ------- + dict + Mapping of prefix-stripped parameter names to ``float`` values. + """ + labels = problem.labels() + values = problem.getp() if point is None else point + snapshot = {} + for label, value in zip(labels, values): + snapshot[label[len(PARAMETER_PREFIX) :]] = float(value) + return snapshot diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index 335d288c..f4cca004 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -3,21 +3,19 @@ from typing import Callable -from .. import available_minimizers -from ..available_minimizers import AvailableMinimizers +from . import minimizers from .minimizer_base import MinimizerBase +from .minimizers import Minimizers -if available_minimizers.lmfit_engine_available: +if minimizers.lmfit_engine_available: from .minimizer_lmfit import LMFit -if available_minimizers.dfo_engine_available: +if minimizers.dfo_engine_available: from .minimizer_dfo import DFO -if available_minimizers.bumps_engine_available: +if minimizers.bumps_engine_available: from .minimizer_bumps import Bumps -def factory( - minimizer_enum: AvailableMinimizers, fit_object, fit_function: Callable -) -> MinimizerBase: +def factory(minimizer_enum: Minimizers, fit_object, fit_function: Callable) -> MinimizerBase: if minimizer_enum.package == 'lm': minimizer = LMFit(obj=fit_object, fit_function=fit_function, minimizer_enum=minimizer_enum) diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 48eef544..ca5e1647 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -1,81 +1,58 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from abc import ABCMeta from abc import abstractmethod -from inspect import Parameter as InspectParameter -from inspect import Signature -from inspect import _empty from typing import Any from typing import Callable -from typing import Dict from typing import List -from typing import Tuple -from typing import Union import numpy as np -# causes circular import when Parameter is imported -# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers +from ..engine_base import EngineBase +from .minimizers import Minimizers from .utils import FitError from .utils import FitResults -MINIMIZER_PARAMETER_PREFIX = 'p' - -class MinimizerBase(metaclass=ABCMeta): +class MinimizerBase(EngineBase): """ This template class is the basis for all minimizer engines in ``EasyScience``. """ - package: str = None - def __init__( self, - obj, #: ObjBase, + obj, #: ModelBase, fit_function: Callable, - minimizer_enum: AvailableMinimizers, - ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 + minimizer_enum: Minimizers, + ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 if minimizer_enum.method not in self.supported_methods(): raise FitError(f'Method {minimizer_enum.method} not available in {self.__class__}') - self._object = obj - self._original_fit_function = fit_function + super().__init__(obj=obj, fit_function=fit_function) self._minimizer_enum = minimizer_enum self._method = minimizer_enum.method - self._cached_pars: Dict[str, Parameter] = {} - self._cached_pars_vals: Dict[str, Tuple[float]] = {} self._cached_model = None - self._fit_function = None @property - def enum(self) -> AvailableMinimizers: + def enum(self) -> Minimizers: return self._minimizer_enum @property def name(self) -> str: return self._minimizer_enum.name - def _restore_parameter_values(self) -> None: - for key in self._cached_pars.keys(): - self._cached_pars[key].value = self._cached_pars_vals[key][0] - self._cached_pars[key].error = self._cached_pars_vals[key][1] - @abstractmethod def fit( self, x: np.ndarray, y: np.ndarray, weights: np.ndarray, - model: Callable | None = None, - parameters: List[Parameter] | None = None, method: str | None = None, tolerance: float | None = None, max_evaluations: int | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, **kwargs, ) -> FitResults: """ @@ -89,18 +66,16 @@ def fit( Measured points. weights : np.ndarray Weights for supplied measured points. - model : Callable | None, default=None - Optional Model which is being fitted to. By default, None. - parameters : List[Parameter] | None, default=None - Optional parameters for the fit. By default, None. method : str | None, default=None Method for the minimizer to use. By default, None. tolerance : float | None, default=None Requested convergence tolerance. By default, None. max_evaluations : int | None, default=None Maximum number of objective evaluations. By default, None. - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional progress callback. By default, None. + progress_callback : Callable[[dict], None] | None, default=None + Optional progress callback. Its return value is ignored by + every backend; use ``abort_test`` to stop a running fit. By + default, None. **kwargs : Additional arguments for the fitting function. @@ -110,50 +85,6 @@ def fit( Fit results. """ - def evaluate( - self, x: np.ndarray, minimizer_parameters: dict[str, float] | None = None, **kwargs - ) -> np.ndarray: - """ - Evaluate the fit function for values of x. - - Parameters used are either the latest or user supplied. If the - parameters are user supplied, it must be in a dictionary of - {'parameter_name': parameter_value,...}. - - Parameters - ---------- - x : np.ndarray - X values for which the fit function will be evaluated. - minimizer_parameters : dict[str, float] | None, default=None - Dictionary of parameters which will be used in the fit - function. They must be in a dictionary of {'parameter_name': - parameter_value,...}. By default, None. - **kwargs : - Additional arguments. - - Returns - ------- - np.ndarray - Y values calculated at points x for a set of parameters. - - Raises - ------ - TypeError - If ``minimizer_parameters`` is not a dictionary. - """ - if minimizer_parameters is None: - minimizer_parameters = {} - if not isinstance(minimizer_parameters, dict): - raise TypeError('minimizer_parameters must be a dictionary') - - if self._fit_function is None: - # This will also generate self._cached_pars - self._fit_function = self._generate_fit_function() - - minimizer_parameters = self._prepare_parameters(minimizer_parameters) - - return self._fit_function(x, **minimizer_parameters, **kwargs) - def _get_method_kwargs(self, passed_method: str | None = None) -> dict[str, str]: if passed_method is not None: if passed_method not in self.supported_methods(): @@ -165,24 +96,6 @@ def _get_method_kwargs(self, passed_method: str | None = None) -> dict[str, str] return {} - @abstractmethod - def convert_to_pars_obj(self, par_list: List[Parameter] | None = None) -> Any: - """ - Create an engine compatible container with the ``Parameters`` - converted from the base object. - - Parameters - ---------- - par_list : List[Parameter] | None, default=None - If only a single/selection of parameter is required. Specify - as a list. By default, None. - - Returns - ------- - Any - Engine Parameters compatible object. - """ - @staticmethod @abstractmethod def supported_methods() -> List[str]: @@ -207,120 +120,6 @@ def all_methods() -> List[str]: List of all available methods. """ - @staticmethod - @abstractmethod - def convert_to_par_object(obj): # todo after constraint changes, add type hint: obj: ObjBase - """ - Convert an ``EasyScience.variable.Parameter`` object to an - engine Parameter object. - """ - - def _prepare_parameters(self, parameters: dict[str, float]) -> dict[str, float]: - """ - Prepare the parameters for the minimizer. - - Parameters - ---------- - parameters : dict[str, float] - Dict of parameters for the minimizer with names as keys. - - Returns - ------- - dict[str, float] - Completed parameter dictionary for the minimizer. - """ - pars = self._cached_pars - - for name, item in pars.items(): - parameter_name = MINIMIZER_PARAMETER_PREFIX + str(name) - if parameter_name not in parameters.keys(): - parameters[parameter_name] = item.value - return parameters - - def _generate_fit_function(self) -> Callable: - """ - Using the user supplied ``fit_function``, wrap it in such a way - we can update ``Parameter`` on iterations. - - Returns - ------- - Callable - A fit function which is compatible with bumps models. - """ - # Original fit function - func = self._original_fit_function - # Get a list of `Parameters` - self._cached_pars = {} - self._cached_pars_vals = {} - for parameter in self._object.get_fit_parameters(): - key = parameter.unique_name - self._cached_pars[key] = parameter - self._cached_pars_vals[key] = (parameter.value, parameter.error) - - # Make a new fit function - def _fit_function(x: np.ndarray, **kwargs) -> np.ndarray: - """ - Wrapped fit function which now has an EasyScience compatible - form. - - Parameters - ---------- - x : np.ndarray - Array of data points to be calculated. - **kwargs : - Key word arguments. - - Returns - ------- - np.ndarray - Points calculated at ``x``. - """ - # Update the `Parameter` values and the callback if needed - # TODO THIS IS NOT THREAD SAFE :-( - - for name, value in kwargs.items(): - par_name = name[1:] - if par_name in self._cached_pars.keys(): - # This will take into account constraints - if self._cached_pars[par_name].value != value: - self._cached_pars[par_name].value = value - - # Since we are calling the parameter fset will be called. - # TODO Pre processing here - return_data = func(x) - # TODO Loading or manipulating data here - return return_data - - _fit_function.__signature__ = self._create_signature(self._cached_pars) - return _fit_function - - @staticmethod - def _create_signature(parameters: Dict[int, Parameter]) -> Signature: - """ - Wrap the function signature. - - This is done as lmfit wants the function to be in the form: f = - (x, a=1, b=2)... Where we need to be generic. Note that this - won't hold for much outside of this scope. - """ - wrapped_parameters = [] - wrapped_parameters.append( - InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty) - ) - - for name, parameter in parameters.items(): - default_value = parameter.value - - wrapped_parameters.append( - InspectParameter( - MINIMIZER_PARAMETER_PREFIX + str(name), - InspectParameter.POSITIONAL_OR_KEYWORD, - annotation=_empty, - default=default_value, - ) - ) - return Signature(wrapped_parameters) - @staticmethod def _error_from_jacobian( jacobian: np.ndarray, residuals: np.ndarray, confidence: float = 0.95 diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 315d856c..44c05a20 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -3,8 +3,6 @@ from __future__ import annotations -import copy -import math from typing import TYPE_CHECKING from typing import Any from typing import Callable @@ -13,45 +11,46 @@ from bumps.fitters import FIT_AVAILABLE_IDS from bumps.fitters import FITTERS from bumps.fitters import FitDriver -from bumps.names import Curve from bumps.names import FitProblem from bumps.parameter import Parameter as BumpsParameter +from scipy.optimize import OptimizeResult -# causes circular import when Parameter is imported -# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers +from ..engine_base import PARAMETER_PREFIX from .bumps_utils import BumpsProgressMonitor from .bumps_utils import EvalCounter -from .minimizer_base import MINIMIZER_PARAMETER_PREFIX +from .bumps_utils import build_curve_problem +from .bumps_utils import parameter_names +from .bumps_utils import parameter_snapshot +from .bumps_utils import to_bumps_parameter from .minimizer_base import MinimizerBase +from .minimizers import Minimizers from .utils import FitError from .utils import FitResults if TYPE_CHECKING: - from bumps.dream.state import MCMCDraw + from bumps.fitters import FitBase -FIT_AVAILABLE_IDS_FILTERED = copy.copy(FIT_AVAILABLE_IDS) -# Considered experimental -FIT_AVAILABLE_IDS_FILTERED.remove('pt') +# 'pt' (parallel tempering) is considered experimental and is not exposed. +FIT_AVAILABLE_IDS_FILTERED = [fit_id for fit_id in FIT_AVAILABLE_IDS if fit_id != 'pt'] class Bumps(MinimizerBase): """ This is a wrapper to Bumps: https://bumps.readthedocs.io/ It allows for the Bumps fitting engine to use parameters declared in an - ``EasyScience.base_classes.ObjBase``. + ``easyscience.base_classes.ModelBase``. """ package = 'bumps' def __init__( self, - obj: object, #: ObjBase, + obj: object, fit_function: Callable, - minimizer_enum: AvailableMinimizers | None = None, - ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 + minimizer_enum: Minimizers | None = None, + ): """ Initialize the fitting engine. @@ -61,7 +60,7 @@ def __init__( Object containing the ``Parameter`` instances to fit. fit_function : Callable Callable returning model y values for the supplied x values. - minimizer_enum : AvailableMinimizers | None, default=None + minimizer_enum : Minimizers | None, default=None Selected BUMPS minimizer configuration. By default, None. """ super().__init__(obj=obj, fit_function=fit_function, minimizer_enum=minimizer_enum) @@ -70,7 +69,8 @@ def __init__( @staticmethod def all_methods() -> list[str]: - return FIT_AVAILABLE_IDS_FILTERED + # Copy so callers cannot mutate the module-level list in place. + return list(FIT_AVAILABLE_IDS_FILTERED) @staticmethod def supported_methods() -> list[str]: @@ -83,12 +83,10 @@ def fit( x: np.ndarray, y: np.ndarray, weights: np.ndarray, - model: Callable | None = None, - parameters: list[Parameter] | None = None, method: str | None = None, tolerance: float | None = None, max_evaluations: int | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, abort_test: Callable[[], bool] | None = None, minimizer_kwargs: dict | None = None, engine_kwargs: dict | None = None, @@ -105,10 +103,6 @@ def fit( Measured points. weights : np.ndarray Weights for supplied measured points. - model : Callable | None, default=None - Optional Model which is being fitted to. By default, None. - parameters : list[Parameter] | None, default=None - Optional parameters for the fit. By default, None. method : str | None, default=None Method for minimization. By default, None. tolerance : float | None, default=None @@ -118,17 +112,22 @@ def fit( ``steps`` parameter. If ``None``, the default value defined by the selected BUMPS fitter (``fitclass.settings``) is used. By default, None. - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback for progress updates. The payload field - ``iteration`` carries the BUMPS optimizer step index. By - default, None. + progress_callback : Callable[[dict], None] | None, default=None + Optional callback for progress updates. The field + ``iteration`` carries the BUMPS optimizer step index. The + return value is ignored: use ``abort_test`` to stop a + running fit. By default, None. abort_test : Callable[[], bool] | None, default=None Optional callback that returns ``True`` to signal that the fit should be aborted. Called periodically during the BUMPS - optimizer iteration loop. + optimizer iteration loop, and once more after the optimizer + returns in order to distinguish an aborted fit from a + converged one, so it must be side-effect free. An aborted fit + returns ``FitResults(success=False)`` rather than raising. minimizer_kwargs : dict | None, default=None Additional keyword arguments passed to the BUMPS minimizer. - By default, None. + The mapping is copied before use, so it is never mutated. By + default, None. engine_kwargs : dict | None, default=None Additional engine keyword arguments. By default, None. **kwargs : Any @@ -137,74 +136,74 @@ def fit( Returns ------- FitResults - Fit results. + Fit results. ``FitResults.iterations`` is the number of BUMPS + *optimizer steps* consumed (the last reported step index plus + one), which is what ``max_evaluations`` tests against; it is + not comparable to LMFit's ``nfev`` or DFO-LS' ``nf``. The + objective-call count is reported separately as + ``FitResults.n_evaluations``, which is the cross-backend + consistent figure. Note that BUMPS derives the step index from + whatever the selected fitter reports to its monitors, so the + granularity of a "step" varies between fitters. Raises ------ FitError - If the BUMPS fit fails. + If the BUMPS fit raises. A fit that merely fails to converge + is reported as ``FitResults(success=False)`` instead. ValueError - If the input shapes or weights are invalid. + If the input shapes or weights are invalid, or if + ``progress_callback`` is not callable. """ method_dict = self._get_method_kwargs(method) x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) - if y.shape != x.shape: - raise ValueError('x and y must have the same shape.') - - if weights.shape != x.shape: - raise ValueError('Weights must have the same shape as x and y.') + self.validate_arrays(x, y, weights) - if not np.isfinite(weights).all(): - raise ValueError('Weights cannot be NaN or infinite.') - - if (weights <= 0).any(): - raise ValueError('Weights must be strictly positive and non-zero.') + if progress_callback is not None and not callable(progress_callback): + raise ValueError('progress_callback must be callable') if engine_kwargs is None: engine_kwargs = {} - if minimizer_kwargs is None: - minimizer_kwargs = {} + minimizer_kwargs = {} if minimizer_kwargs is None else dict(minimizer_kwargs) minimizer_kwargs.update(engine_kwargs) method_str = method_dict.get('method', self._method) fitclass = self._resolve_fitclass(method_str) - # Resolve BUMPS-native defaults so the budget reported back to the caller (and - # used by the budget-exhaustion check in `_gen_fit_results`) reflects the values - # actually consumed by the fitter, even when the caller passes None. + # Only values the caller supplied explicitly are pushed back into + # `minimizer_kwargs`. BUMPS pairs an independent `ftol`/`xtol` default per + # fitter (`newton` combines ftol=1e-6 with xtol=1e-12, `amoeba` ftol=1e-8 with + # xtol=1e-6). fitter_settings = dict(fitclass.settings) - if max_evaluations is None: + + if max_evaluations is not None: + minimizer_kwargs['steps'] = max_evaluations + else: max_evaluations = fitter_settings.get('steps') - if tolerance is None: - ftol = fitter_settings.get('ftol') - xtol = fitter_settings.get('xtol') - tols = [t for t in (ftol, xtol) if t is not None] - tolerance = min(tols) if tols else None if tolerance is not None: minimizer_kwargs['ftol'] = tolerance # tolerance for change in function value minimizer_kwargs['xtol'] = ( tolerance # tolerance for change in parameter value, could be an independent value ) - if max_evaluations is not None: - minimizer_kwargs['steps'] = max_evaluations + else: + # Report the stricter of the two BUMPS defaults; nothing is written back. + ftol = fitter_settings.get('ftol') + xtol = fitter_settings.get('xtol') + tols = [t for t in (ftol, xtol) if t is not None] + tolerance = min(tols) if tols else None - if model is None: - model_function = self._make_model(parameters=parameters) - model = model_function(x, y, weights) + # The Curve comes back directly from the helper. + problem, self._eval_counter, model = build_curve_problem(self, x, y, weights) self._cached_model = model self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()} - problem = FitProblem(model) - monitors = [] if progress_callback is not None: - if not callable(progress_callback): - raise ValueError('progress_callback must be callable') monitors.append( BumpsProgressMonitor(problem, progress_callback, self._build_progress_payload) ) @@ -219,35 +218,54 @@ def fit( ) driver.clip() - # Why do we do this? Because a fitting template has to have global_object instantiated outside pre-runtime - from easyscience import global_object - - stack_status = global_object.stack.enabled - global_object.stack.enabled = False - try: # Drive the fit through the local FitDriver instance so the supplied # `monitors` (including the optional progress callback monitor) are # invoked. `bumps.fitters.fit` constructs its own driver. - x, fx = driver.fit() - from scipy.optimize import OptimizeResult + best_x, fx = driver.fit() + + # BUMPS signals a failed optimization by returning `None` in place of a + # parameter vector (e.g. Levenberg-Marquardt landing on non-finite + # values); `FitDriver.fit` skips `problem.setp` in that case. Poll + # `abort_test` once more to tell a user-cancelled run apart from a + # converged one, since BUMPS stops quietly either way. + if best_x is None: + success = False + message = 'BUMPS returned no solution; the fit did not converge' + elif abort_test is not None and abort_test(): + success = False + message = 'Fit aborted before convergence' + else: + success = True + message = 'Fit converged' # BUMPS' `MonitorRunner.history.step` is populated by the driver itself # (independently of any user-supplied monitors) and exposes the canonical - # last-step index reached by the fitter, so we use it as `nit`. - history_step = getattr(getattr(driver, 'monitor_runner', None), 'history', None) - nit_value = int(history_step.step[0]) if history_step is not None else None + # last-step index reached by the fitter, so we use it as `nit`. `Trace` + # indexes into an internal list, so an empty trace raises `IndexError` + # rather than returning a default β€” that happens when the fit is aborted + # before the fitter reports its first step. + history = getattr(getattr(driver, 'monitor_runner', None), 'history', None) + step_trace = getattr(history, 'step', None) + nit_value = int(step_trace[0]) if step_trace is not None and len(step_trace) else None + model_results = OptimizeResult( - x=x, - dx=driver.stderr(), + # `driver.stderr()` derives the errors from the covariance at the + # solution, so it cannot be evaluated without one. + x=best_x, + dx=driver.stderr() if best_x is not None else None, fun=fx, - success=True, - status=0, - message='successful termination', + success=success, + status=0 if success else 1, + message=message, nit=nit_value, ) model_results.state = driver.fitter.state - self._set_parameter_fit_result(model_results, stack_status, problem._parameters) + + if best_x is None: + self._restore_parameter_values() + else: + self._set_parameter_fit_result(model_results, parameter_names(problem)) results = self._gen_fit_results( model_results, max_evaluations=max_evaluations, @@ -255,27 +273,43 @@ def fit( ) except Exception as e: self._restore_parameter_values() - raise FitError(e) - finally: - global_object.stack.enabled = stack_status + raise FitError(e) from e return results @staticmethod - def _resolve_fitclass(method: str): + def _resolve_fitclass(method: str) -> type[FitBase]: + """ + Look up the BUMPS fitter class registered under ``method``. + + Parameters + ---------- + method : str + A BUMPS fitter id, e.g. ``'amoeba'``. + + Returns + ------- + type[FitBase] + The matching BUMPS fitter class. + + Raises + ------ + FitError + If no registered fitter carries that id. + """ for fitclass in FITTERS: if fitclass.id == method: return fitclass raise FitError(f'Unknown BUMPS fitting method: {method}') def _build_progress_payload( - self, problem, iteration: int, point: np.ndarray, nllf: float + self, problem: FitProblem, iteration: int, point: np.ndarray | None, nllf: float ) -> dict: # Use the nllf already computed by the fitter to avoid a costly # model re-evaluation, and let BUMPS apply its own chisq scaling. chi2 = float(problem.chisq(nllf=nllf, norm=False)) reduced_chi2 = float(problem.chisq(nllf=nllf, norm=True)) - parameter_values = self._current_parameter_snapshot(problem, point) + parameter_values = parameter_snapshot(problem, point) return { 'iteration': iteration, @@ -286,434 +320,10 @@ def _build_progress_payload( 'finished': False, } - def _current_parameter_snapshot(self, problem, point: np.ndarray) -> dict: - labels = problem.labels() - values = problem.getp() if point is None else point - snapshot = {} - for label, value in zip(labels, values): - dict_name = label[len(MINIMIZER_PARAMETER_PREFIX) :] - snapshot[dict_name] = float(value) - return snapshot - - def convert_to_pars_obj(self, par_list: list[Parameter] | None = None) -> list[BumpsParameter]: - """ - Create a container with the ``Parameters`` converted from the - base object. - - Parameters - ---------- - par_list : list[Parameter] | None, default=None - If only a single/selection of parameter is required. Specify - as a list. By default, None. - - Returns - ------- - list[BumpsParameter] - Bumps Parameters list. - """ - if par_list is None: - # Assume that we have a ObjBase for which we can obtain a list - par_list = self._object.get_fit_parameters() - pars_obj = [self.__class__.convert_to_par_object(obj) for obj in par_list] - return pars_obj - - # For some reason I have to double staticmethod :-/ - @staticmethod - def convert_to_par_object(obj: Parameter) -> BumpsParameter: - """ - Convert an ``EasyScience.variable.Parameter`` object to a bumps - Parameter object. - - Parameters - ---------- - obj : Parameter - EasyScience parameter to convert. - - Returns - ------- - BumpsParameter - Bumps Parameter compatible object. - """ - - value = obj.value - - return BumpsParameter( - name=MINIMIZER_PARAMETER_PREFIX + obj.unique_name, - value=value, - bounds=[obj.min, obj.max], - fixed=obj.fixed, - ) - - def _make_model(self, parameters: list[BumpsParameter] | None = None) -> Callable: - """ - Generate a bumps model from the supplied ``fit_function`` and - parameters in the base object. Note that this makes a callable - as it needs to be initialized with *x*, *y*, *weights* - - Weights are converted to dy (standard deviation of y). - - Parameters - ---------- - parameters : list[BumpsParameter] | None, default=None - Optional BUMPS parameters to bind into the model. - - Returns - ------- - Callable - Callable to make a bumps Curve model. - """ - fit_func = EvalCounter(self._generate_fit_function()) - self._eval_counter = fit_func - - def _outer(obj): - - def _make_func(x, y, weights): - bumps_pars = {} - if not parameters: - for name, par in obj._cached_pars.items(): - bumps_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = ( - obj.convert_to_par_object(par) - ) - else: - for par in parameters: - bumps_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = ( - obj.convert_to_par_object(par) - ) - return Curve(fit_func, x, y, dy=1 / weights, **bumps_pars) - - return _make_func - - return _outer(self) - - def mcmc_sample( - self, - x: np.ndarray, - y: np.ndarray, - weights: np.ndarray, - samples: int = 10000, - burn: int = 2000, - thin: int = 10, - population: int | None = None, - resume_state: MCMCDraw | None = None, - sampler_kwargs: dict | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, - abort_test: Callable[[], bool] | None = None, - ) -> dict: - """ - Run Bayesian MCMC sampling using the BUMPS DREAM sampler. - - Builds a BUMPS ``FitProblem`` from the current model and runs - the DREAM sampler. This is the public minimizer-level entry - point for Bayesian sampling; the higher-level - ``MultiFitter.mcmc_sample`` delegates to this method after - flattening multi-dataset arrays. - - Parameters - ---------- - x : np.ndarray - Flattened independent variable array. - y : np.ndarray - Flattened dependent variable array. - weights : np.ndarray - Flattened weight array. - samples : int, default=10000 - Number of raw samples to draw across all chains, before thinning. - A guaranteed minimum, not an exact count: DREAM advances in - blocks of 10 generations (one generation = one draw per chain) - and stops at the first block boundary at or past ``samples``. - burn : int, default=2000 - Burn-in generations to discard. BUMPS counts ``burn`` in - generations while ``samples`` counts raw draws, so ``burn=500`` - discards ``500 * n_chains`` raw samples. - thin : int, default=10 - Thinning interval β€” only every ``thin``-th generation is stored. - population : int | None, default=None - BUMPS DREAM population count per parameter (number of parallel - chains): BUMPS creates ``ceil(population * n_parameters)`` chains. - resume_state : MCMCDraw | None, default=None - A BUMPS ``MCMCDraw`` state object from a previous - ``mcmc_sample()`` call (e.g. ``PosteriorResults.sampler_state``). - When provided, DREAM **continues** the saved chain instead of - starting cold. The population, parameter count, and parameter - names must match the current model β€” a ``ValueError`` is raised - otherwise. - - ``samples`` must be the **total** number of raw samples, not an - increment: to extend an existing chain of ``N`` raw samples by - ``M``, pass ``samples=N + M`` (DREAM keeps only the last - ``samples`` draws in its buffer). The `Sampler.extend` helper - computes this for you. - - ``burn`` is forced to 0 on resume: a previously-converged chain is - never re-burned. - - The ``population`` and ``initializer`` parameters - have **no effect** when ``resume_state`` is provided β€” they - are determined by the saved state. - - Resuming against *different* data is undefined behaviour (the - chain's likelihood changes underneath it). - sampler_kwargs : dict | None, default=None - Additional keyword arguments forwarded to - ``bumps.fitters.fit``. - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback for progress updates during sampling. The - payload dict includes ``iteration`` (DREAM generation - number) and ``sampling: True``. - abort_test : Callable[[], bool] | None, default=None - Optional callback that returns ``True`` to signal that - sampling should be aborted. Called periodically during the - DREAM sampling loop. - - Returns - ------- - dict - Dictionary with keys ``'draws'``, ``'param_names'``, - ``'internal_bumps_object'``, and ``'logp'``. - - Raises - ------ - ValueError - If the input shapes or weights are invalid, if - ``progress_callback`` is not callable, or if ``resume_state`` - is incompatible with the current model (parameter count, - names/order, or population mismatch). - FitError - If DREAM sampling was aborted by the user (via - ``abort_test``). - Exception - Re-raised from DREAM fitting if any unexpected error occurs - (parameter values are restored beforehand). - """ - from bumps.fitters import DreamFit - from bumps.names import FitProblem - - x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) - - if not isinstance(samples, int) or samples <= 0: - raise ValueError('samples must be a positive integer.') - if not isinstance(burn, int) or burn < 0: - raise ValueError('burn must be a non-negative integer.') - if not isinstance(thin, int) or thin < 1: - raise ValueError('thin must be a positive integer.') - - if y.shape != x.shape: - raise ValueError('x and y must have the same shape.') - - if not np.isfinite(x).all(): - raise ValueError('x cannot contain NaN or infinite values.') - if not np.isfinite(y).all(): - raise ValueError('y cannot contain NaN or infinite values.') - - if weights.shape != x.shape: - raise ValueError('Weights must have the same shape as x and y.') - - if not np.isfinite(weights).all(): - raise ValueError('Weights cannot be NaN or infinite.') - - if (weights <= 0).any(): - raise ValueError('Weights must be strictly positive and non-zero.') - - # Build the BUMPS Curve model using the minimizer's existing machinery - model_func = self._make_model() - curve = model_func(x, y, weights) - problem = FitProblem(curve) - - pop = population - if resume_state is not None: - pop, burn = self._validate_resume_state(problem, resume_state, population, burn) - - # Build DREAM kwargs. Use the resolved ``pop``, not the raw - # ``population`` argument β€” on resume ``pop`` is the negative - # absolute chain count that reproduces the saved state's - # population, which BUMPS requires to match. - dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} - if pop is not None: - dream_kwargs['pop'] = pop - if sampler_kwargs: - dream_kwargs.update(sampler_kwargs) - - # Build monitors (same pattern as classical Bumps.fit()) - monitors = [] - if progress_callback is not None: - if not callable(progress_callback): - raise ValueError('progress_callback must be callable') - # Compute total DREAM steps for progress display (burn + sampling generations). - # BUMPS DREAM default population count is 10 when not specified by the user. - # A negative ``pop`` (resume) is an absolute chain count. - _dream_default_pop = 10 - pop_val = abs(pop) if pop is not None else _dream_default_pop - _total_steps = burn + (samples + pop_val - 1) // pop_val - monitors.append( - BumpsProgressMonitor( - problem, - progress_callback, - lambda problem, iteration, point, nllf: { - **self._build_sample_progress_payload(problem, iteration, point, nllf), - 'total_steps': _total_steps, - }, - ) - ) - - driver = FitDriver( - fitclass=DreamFit, - problem=problem, - monitors=monitors, - abort_test=abort_test if abort_test is not None else (lambda: False), - **dream_kwargs, - ) - driver.clip() - - from easyscience import global_object - - stack_status = global_object.stack.enabled - global_object.stack.enabled = False - - try: - fit_kwargs = {} - if resume_state is not None: - # Defensive copy: BUMPS mutates the state object in-place - # (via MCMCDraw.resize() β€” see bumps/dream/core.py allocate_state) - # during resume. Without a copy, the caller's original state - # object is silently altered, making it impossible to compare - # pre- and post-resume state (shape mismatch). See - # https://github.com/easyscience/core/pull/257 - fit_kwargs['fit_state'] = copy.deepcopy(resume_state) - x_opt, fx = driver.fit(**fit_kwargs) - result_state = getattr(driver.fitter, 'state', None) - if result_state is None: - raise FitError('Sampling aborted by user') - except Exception: - self._restore_parameter_values() - raise - finally: - global_object.stack.enabled = stack_status - - _draw = result_state.draw() - draws = _draw.points - param_names = [p.name[len(MINIMIZER_PARAMETER_PREFIX) :] for p in problem._parameters] - logp = _draw.logp - - return { - 'draws': draws, - 'param_names': param_names, - 'internal_bumps_object': result_state, - 'logp': logp, - } - - def _validate_resume_state( - self, - problem: FitProblem, - resume_state: MCMCDraw, - population: int | None, - burn: int, - ) -> tuple[int, int]: - """Check that ``resume_state`` is compatible with ``problem`` and - resolve the population and burn values to use when resuming. - - Parameters - ---------- - problem : FitProblem - The freshly built BUMPS ``FitProblem`` for the current model. - resume_state : MCMCDraw - The saved chain state to resume from. - population : int | None - The caller-supplied population scale factor, or ``None``. - burn : int - The caller-supplied burn-in, ignored (with a warning) on resume. - - Returns - ------- - tuple[int, int] - ``(population, burn)`` to pass to DREAM. The population is - returned as a **negative** number, which BUMPS' - ``initpop.generate`` reads as an absolute chain count, exactly - reproducing the saved state's population. ``burn`` is always 0: - a previously converged chain is never re-burned. - - Raises - ------ - ValueError - If ``resume_state`` is incompatible with the current model - (parameter count, names/order, or population mismatch). - """ - from easyscience import global_object - - logger = global_object.log.getLogger('fitting.bumps') - - # Parameter count - n_params = len(problem._parameters) - if n_params != resume_state.Nvar: - raise ValueError( - f'resume_state has {resume_state.Nvar} parameters but the current ' - f'model has {n_params}. The model must have the same ' - f'number of fitted parameters as when the saved chain was created.' - ) - - prefix = MINIMIZER_PARAMETER_PREFIX - fresh_names = [(p.name or '')[len(prefix) :] for p in problem._parameters] - state_labels = list(resume_state.labels) - if state_labels and all(lbl.startswith(prefix) for lbl in state_labels): - state_names = [lbl[len(prefix) :] for lbl in state_labels] - if fresh_names != state_names: - raise ValueError( - f'Parameter names/order mismatch between the current model ' - f'and resume_state.\n' - f' Current model : {fresh_names}\n' - f' resume_state : {state_names}' - ) - else: - logger.warning( - 'resume_state does not carry parameter names (it was most ' - 'likely reloaded from disk, where BUMPS does not preserve ' - 'labels). Parameter-name validation is skipped; the saved ' - 'chain is matched to the current model by parameter order. ' - 'Ensure this is the same model, with parameters in the same ' - 'order, used to create the chain.' - ) - - # Population. BUMPS creates ``ceil(population * n_params)`` chains - # and requires the resumed state's chain count to match. - if population is not None: - expected_npop = math.ceil(population * n_params) - if expected_npop != resume_state.Npop: - raise ValueError( - f'Requested population ({population}) would produce ' - f'{expected_npop} chains but the saved state has ' - f'{resume_state.Npop} chains. The population cannot ' - f'be changed on resume.' - ) - if burn > 0: - logger.warning( - f'burn={burn} ignored on resume: a previously converged ' - f'chain is not re-burned. Forcing burn=0.' - ) - - # A negative ``pop`` is read by ``bumps.initpop.generate`` as an - # absolute chain count, exactly reproducing the saved population - # without having to recover the original scale factor. - return -int(resume_state.Npop), 0 - - def _build_sample_progress_payload( - self, problem, iteration: int, point: np.ndarray, nllf: float - ) -> dict: - """ - Build a progress payload for Bayesian DREAM sampling steps. - - Called by :class:`BumpsProgressMonitor` at each DREAM - generation. The payload includes ``sampling: True`` so - downstream consumers can distinguish sampling progress from - classical fitting progress. - """ - payload = self._build_progress_payload(problem, iteration, point, nllf) - payload['sampling'] = True - return payload - def _set_parameter_fit_result( self, fit_result: Any, - stack_status: bool, - par_list: list[BumpsParameter], + par_names: list[str], ) -> None: """ Update parameters to their final values and assign a std error @@ -723,28 +333,18 @@ def _set_parameter_fit_result( ---------- fit_result : Any BUMPS OptimizeResult containing best-fit values and errors. - stack_status : bool - Whether the undo stack was enabled. - par_list : list[BumpsParameter] - List of BUMPS parameter objects. + par_names : list[str] + Cached-parameter names in BUMPS problem order, already + stripped of ``PARAMETER_PREFIX``. As seen in + :func:`~easyscience.fitting.minimizers.bumps_utils.parameter_names`. """ - from easyscience import global_object - pars = self._cached_pars x_result = np.asarray(fit_result.x) - stderr = np.asarray(fit_result.dx) + stderr = None if fit_result.dx is None else np.asarray(fit_result.dx) - if stack_status: - self._restore_parameter_values() - global_object.stack.enabled = True - global_object.stack.beginMacro('Fitting routine') - - for index, name in enumerate([par.name for par in par_list]): - dict_name = name[len(MINIMIZER_PARAMETER_PREFIX) :] - pars[dict_name].value = x_result[index] - pars[dict_name].error = stderr[index] - if stack_status: - global_object.stack.endMacro() + for index, name in enumerate(par_names): + pars[name].value = x_result[index] + pars[name].error = None if stderr is None else stderr[index] def _gen_fit_results( self, @@ -774,8 +374,10 @@ def _gen_fit_results( """ results = FitResults() + # `hasattr`, not a truthiness test: every `FitResults` field starts out + # falsy, so testing the current value would silently discard every kwarg. for name, value in kwargs.items(): - if getattr(results, name, False): + if hasattr(results, name): setattr(results, name, value) n_evaluations = None if self._eval_counter is None else self._eval_counter.count # BUMPS exposes `nit` as the last reported optimizer step index rather than the @@ -801,41 +403,36 @@ def _gen_fit_results( pars = self._cached_pars item = {} for index, name in enumerate(self._cached_model.pars.keys()): - dict_name = name[len(MINIMIZER_PARAMETER_PREFIX) :] + dict_name = name[len(PARAMETER_PREFIX) :] item[name] = pars[dict_name].value results.p0 = self._p_0 results.p = item results.x = self._cached_model.x results.y_obs = self._cached_model.y - results.y_calc = self.evaluate(results.x, minimizer_parameters=results.p) + results.y_calc = self.evaluate(results.x, parameters=results.p) results.y_err = self._cached_model.dy results.n_evaluations = n_evaluations results.iterations = n_steps_used - results.message = '' + results.message = '' if fit_results.success else fit_results.message + if stopped_on_budget: + from easyscience import global_object + results.message = ( f'Fit stopped: reached maximum optimizer steps ({max_evaluations}); ' f'objective evaluated {n_evaluations} times' ) - if stopped_on_budget: - from easyscience import global_object - if tolerance is None: - global_object.log.getLogger('fitting.bumps').warning( - f'Fit did not converge within the maximum optimizer steps of {max_evaluations} ' - f'({n_evaluations} objective evaluations). ' - 'Consider increasing the maximum number of evaluations or adjusting the tolerance.' - ) + reason = 'Fit did not converge within' else: - global_object.log.getLogger('fitting.bumps').warning( - f'Fit did not reach the desired tolerance of {tolerance} within the maximum optimizer steps of {max_evaluations} ' - f'({n_evaluations} objective evaluations). ' - 'Consider increasing the maximum number of evaluations or adjusting the tolerance.' - ) - - # results.residual = results.y_obs - results.y_calc - # results.goodness_of_fit = np.sum(results.residual**2) + reason = f'Fit did not reach the desired tolerance of {tolerance} within' + global_object.log.getLogger('fitting.bumps').warning( + f'{reason} the maximum optimizer steps of {max_evaluations} ' + f'({n_evaluations} objective evaluations). ' + 'Consider increasing the maximum number of evaluations or adjusting the tolerance.' + ) + results.minimizer_engine = self.__class__ results.fit_args = None results.engine_result = fit_results diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 319db849..98299d02 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -10,13 +10,11 @@ import dfols import numpy as np -# causes circular import when Parameter is imported -# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers -from .minimizer_base import MINIMIZER_PARAMETER_PREFIX +from ..engine_base import PARAMETER_PREFIX from .minimizer_base import MinimizerBase +from .minimizers import Minimizers from .utils import FitError from .utils import FitResults @@ -46,10 +44,10 @@ class DFO(MinimizerBase): def __init__( self, - obj: object, #: ObjBase, + obj: object, #: ModelBase, fit_function: Callable, - minimizer_enum: AvailableMinimizers | None = None, - ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 + minimizer_enum: Minimizers | None = None, + ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 """ Initialize the fitting engine. @@ -59,7 +57,7 @@ def __init__( Object containing the ``Parameter`` instances to fit. fit_function : Callable Callable returning model y values for the supplied x values. - minimizer_enum : AvailableMinimizers | None, default=None + minimizer_enum : Minimizers | None, default=None Selected DFO minimizer configuration. By default, None. """ super().__init__(obj=obj, fit_function=fit_function, minimizer_enum=minimizer_enum) @@ -78,12 +76,10 @@ def fit( x: np.ndarray, y: np.ndarray, weights: np.ndarray, - model: Callable | None = None, - parameters: List[Parameter] | None = None, method: str | None = None, tolerance: float | None = None, max_evaluations: int | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, callback: Callable[[DFOCallbackState], None] | None = None, **kwargs, ) -> FitResults: @@ -98,18 +94,15 @@ def fit( Measured points. weights : np.ndarray Weights for supplied measured points. - model : Callable | None, default=None - Optional Model which is being fitted to. By default, None. - parameters : List[Parameter] | None, default=None - Optional parameters for the fit. By default, None. method : str | None, default=None Method for minimization. By default, None. tolerance : float | None, default=None Requested optimizer tolerance. By default, None. max_evaluations : int | None, default=None Maximum number of evaluations. By default, None. - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback receiving normalized progress payloads. + progress_callback : Callable[[dict], None] | None, default=None + Optional callback receiving normalized progress payloads. Its + return value is ignored. callback : Callable[[DFOCallbackState], None] | None, default=None Optional native DFO callback. **kwargs : @@ -126,56 +119,28 @@ def fit( ------ FitError If the DFO fit fails. - ValueError - If the input shapes, weights, or tolerance are invalid. """ x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) - if y.shape != x.shape: - raise ValueError('x and y must have the same shape.') - - if weights.shape != x.shape: - raise ValueError('Weights must have the same shape as x and y.') - - if not np.isfinite(weights).all(): - raise ValueError('Weights cannot be NaN or infinite.') - - if (weights <= 0).any(): - raise ValueError('Weights must be strictly positive and non-zero.') + self.validate_arrays(x, y, weights) # Bridge progress_callback into the DFO callback mechanism if progress_callback is not None and callback is None: callback = self._make_progress_adapter(progress_callback) - if model is None: - model_function = self._make_model( - parameters=parameters, - callback=callback, - ) - model = model_function(x, y, weights) - elif callback is not None: - model = self._wrap_model_with_callback( - model, - self._get_callback_parameter_names(parameters), - callback, - ) + model_function = self._make_model(callback=callback) + model = model_function(x, y, weights) self._cached_model = model self._cached_model.x = x self._cached_model.y = y self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()} - # Why do we do this? Because a fitting template has to have global_object instantiated outside pre-runtime - from easyscience import global_object - - stack_status = global_object.stack.enabled - global_object.stack.enabled = False - kwargs = self._prepare_kwargs(tolerance, max_evaluations, **kwargs) try: model_results = self._dfo_fit(self._cached_pars, model, **kwargs) - self._set_parameter_fit_result(model_results, stack_status) + self._set_parameter_fit_result(model_results) results = self._gen_fit_results(model_results, weights) except FitError: self._restore_parameter_values() @@ -183,22 +148,10 @@ def fit( except Exception as e: self._restore_parameter_values() raise FitError(e) - finally: - global_object.stack.enabled = stack_status return results - def convert_to_pars_obj(self, par_list: List[Parameter] | None = None): - """Required by interface but not needed for DFO-LS.""" - pass - - @staticmethod - def convert_to_par_object(obj) -> None: - """Required by interface but not needed for DFO-LS.""" - pass - def _make_model( self, - parameters: List[Parameter] | None = None, callback: Callable[[DFOCallbackState], None] | None = None, ) -> Callable: """ @@ -208,8 +161,6 @@ def _make_model( Parameters ---------- - parameters : List[Parameter] | None, default=None - Optional parameter subset to include in the model. callback : Callable[[DFOCallbackState], None] | None, default=None Optional callback invoked on each objective evaluation. @@ -223,13 +174,10 @@ def _make_model( def _outer(obj: DFO): def _make_func(x, y, weights): - dfo_pars = {} - if not parameters: - for name, par in obj._cached_pars.items(): - dfo_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.value - else: - for par in parameters: - dfo_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = par.value + dfo_pars = { + PARAMETER_PREFIX + str(name): par.value + for name, par in obj._cached_pars.items() + } def _residuals(pars_values: List[float]) -> np.ndarray: for idx, par_name in enumerate(dfo_pars.keys()): @@ -246,13 +194,6 @@ def _residuals(pars_values: List[float]) -> np.ndarray: return _outer(self) - def _get_callback_parameter_names( - self, parameters: List[Parameter] | None = None - ) -> list[str]: - if parameters is not None: - return [MINIMIZER_PARAMETER_PREFIX + parameter.unique_name for parameter in parameters] - return [MINIMIZER_PARAMETER_PREFIX + name for name in self._cached_pars.keys()] - @staticmethod def _wrap_model_with_callback( model: Callable, @@ -302,7 +243,7 @@ def wrapped_model(pars_values: List[float]) -> np.ndarray: @staticmethod def _make_progress_adapter( - progress_callback: Callable[[dict], bool | None], + progress_callback: Callable[[dict], None], ) -> Callable[['DFOCallbackState'], None]: """ Create a DFO callback that translates DFOCallbackState into the @@ -310,8 +251,8 @@ def _make_progress_adapter( Parameters ---------- - progress_callback : Callable[[dict], bool | None] - Standard progress callback (dict -> bool|None). + progress_callback : Callable[[dict], None] + Standard progress callback (dict -> None). Returns ------- @@ -324,7 +265,7 @@ def adapter(state: 'DFOCallbackState') -> None: dof = max(np.asarray(state.residuals).size - len(state.best_parameters), 1) reduced_chi2 = chi2 / dof if dof > 0 else chi2 param_snapshot = { - name[len(MINIMIZER_PARAMETER_PREFIX) :]: float(val) + name[len(PARAMETER_PREFIX) :]: float(val) for name, val in state.best_parameters.items() } payload = { @@ -339,9 +280,7 @@ def adapter(state: 'DFOCallbackState') -> None: return adapter - def _set_parameter_fit_result( - self, fit_result: Any, stack_status: bool, ci: float = 0.95 - ) -> None: + def _set_parameter_fit_result(self, fit_result: Any, ci: float = 0.95) -> None: """ Update parameters to their final values and assign a std error to them. @@ -350,28 +289,17 @@ def _set_parameter_fit_result( ---------- fit_result : Any Fit object which contains info on the fit. - stack_status : bool - Whether the undo stack was enabled. ci : float, default=0.95 Confidence interval for calculating errors. Default 95%. By default, 0.95. """ - from easyscience import global_object - pars = self._cached_pars - if stack_status: - self._restore_parameter_values() - global_object.stack.enabled = True - global_object.stack.beginMacro('Fitting routine') error_matrix = self._error_from_jacobian(fit_result.jacobian, fit_result.resid, ci) for idx, par in enumerate(pars.values()): par.value = fit_result.x[idx] par.error = error_matrix[idx, idx] - if stack_status: - global_object.stack.endMacro() - def _gen_fit_results(self, fit_results: Any, weights: np.ndarray, **kwargs: Any) -> FitResults: """ Convert fit results into the unified ``FitResults`` format. @@ -411,7 +339,7 @@ def _gen_fit_results(self, fit_results: Any, weights: np.ndarray, **kwargs: Any) results.p0 = self._p_0 results.x = self._cached_model.x results.y_obs = self._cached_model.y - results.y_calc = self.evaluate(results.x, minimizer_parameters=results.p) + results.y_calc = self.evaluate(results.x, parameters=results.p) # `weights` here are 1/sigma (residuals are multiplied by them in `_make_model`). # `FitResults.chi2` divides residuals by `y_err`, so `y_err` must be sigma, not the weight. results.y_err = 1 / np.asarray(weights) diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index 31fb543f..b9f47db0 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -11,13 +11,11 @@ from lmfit import Parameters as LMParameters from lmfit.model import ModelResult -# causes circular import when Parameter is imported -# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers -from .minimizer_base import MINIMIZER_PARAMETER_PREFIX +from ..engine_base import PARAMETER_PREFIX from .minimizer_base import MinimizerBase +from .minimizers import Minimizers from .utils import FitError from .utils import FitResults @@ -27,17 +25,17 @@ class LMFit(MinimizerBase): # noqa: S101 This is a wrapper to the extended Levenberg-Marquardt Fit: https://lmfit.github.io/lmfit-py/ It allows for the lmfit fitting engine to use parameters declared in an - ``EasyScience.base_classes.ObjBase``. + ``easyscience.base_classes.ModelBase``. """ package = 'lmfit' def __init__( self, - obj: object, #: ObjBase, + obj: object, #: ModelBase, fit_function: Callable, - minimizer_enum: AvailableMinimizers | None = None, - ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 + minimizer_enum: Minimizers | None = None, + ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 """ Initialize the minimizer. @@ -47,7 +45,7 @@ def __init__( Object containing the ``Parameter`` instances to fit. fit_function : Callable Callable returning model y values for the supplied x values. - minimizer_enum : AvailableMinimizers | None, default=None + minimizer_enum : Minimizers | None, default=None Selected LMFit minimizer configuration. By default, None. """ super().__init__(obj=obj, fit_function=fit_function, minimizer_enum=minimizer_enum) @@ -85,12 +83,10 @@ def fit( x: np.ndarray, y: np.ndarray, weights: np.ndarray = None, - model: LMModel | None = None, - parameters: LMParameters | None = None, method: str | None = None, tolerance: float | None = None, max_evaluations: int | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, minimizer_kwargs: dict | None = None, engine_kwargs: dict | None = None, **kwargs, @@ -106,18 +102,15 @@ def fit( Measured points. weights : np.ndarray, default=None Weights for supplied measured points. By default, None. - model : LMModel | None, default=None - Optional Model which is being fitted to. By default, None. - parameters : LMParameters | None, default=None - Optional parameters for the fit. By default, None. method : str | None, default=None Minimizer method. By default, None. tolerance : float | None, default=None Requested optimizer tolerance. By default, None. max_evaluations : int | None, default=None Maximum number of function evaluations. By default, None. - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback receiving normalized progress payloads. + progress_callback : Callable[[dict], None] | None, default=None + Optional callback receiving normalized progress payloads. Its + return value is ignored. minimizer_kwargs : dict | None, default=None Additional keyword arguments passed to LMFit's minimizer. By default, None. @@ -137,22 +130,10 @@ def fit( ------ FitError If the LMFit optimization fails. - ValueError - If the input shapes or weights are invalid. """ x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) - if y.shape != x.shape: - raise ValueError('x and y must have the same shape.') - - if weights.shape != x.shape: - raise ValueError('Weights must have the same shape as x and y.') - - if not np.isfinite(weights).all(): - raise ValueError('Weights cannot be NaN or infinite.') - - if (weights <= 0).any(): - raise ValueError('Weights must be strictly positive and non-zero.') + self.validate_arrays(x, y, weights) if engine_kwargs is None: engine_kwargs = {} @@ -160,15 +141,8 @@ def fit( method_kwargs = self._get_method_kwargs(method) fit_kws_dict = self._get_fit_kws(method, tolerance, minimizer_kwargs) - # Why do we do this? Because a fitting template has to have global_object instantiated outside pre-runtime - from easyscience import global_object - - stack_status = global_object.stack.enabled - global_object.stack.enabled = False - try: - if model is None: - model = self._make_model() + model = self._make_model() self._last_iteration = None iter_cb = self._create_iter_callback(progress_callback) @@ -183,18 +157,16 @@ def fit( **engine_kwargs, **kwargs, ) - self._set_parameter_fit_result(model_results, stack_status) + self._set_parameter_fit_result(model_results) results = self._gen_fit_results(model_results, iterations=self._last_iteration) except Exception as e: self._restore_parameter_values() raise FitError(e) - finally: - global_object.stack.enabled = stack_status return results def _create_iter_callback( self, - progress_callback: Callable[[dict], bool | None] | None, + progress_callback: Callable[[dict], None] | None, ) -> Callable | None: def iter_cb(params, iteration: int, residuals: np.ndarray, *args, **kwargs) -> bool: @@ -218,12 +190,12 @@ def _build_progress_payload(self, params, iteration: int, residuals: np.ndarray) reduced_chi2 = chi2 / degrees_of_freedom if degrees_of_freedom > 0 else chi2 parameter_values = { - parameter_name[len(MINIMIZER_PARAMETER_PREFIX) :]: float(parameter.value) + parameter_name[len(PARAMETER_PREFIX) :]: float(parameter.value) for parameter_name, parameter in params.items() - if parameter_name.startswith(MINIMIZER_PARAMETER_PREFIX) + if parameter_name.startswith(PARAMETER_PREFIX) } for parameter_name, parameter in self._cached_pars.items(): - lmfit_parameter_name = f'{MINIMIZER_PARAMETER_PREFIX}{parameter_name}' + lmfit_parameter_name = f'{PARAMETER_PREFIX}{parameter_name}' if lmfit_parameter_name not in params: parameter_values[parameter_name] = float(parameter.value) @@ -241,65 +213,17 @@ def _get_fit_kws( ) -> dict[str:str]: if minimizer_kwargs is None: minimizer_kwargs = {} + # `method` is usually None, because `Fitter.fit` + # does not pass one; the minimizer's own method is what actually runs, + # so it decides which tolerance keyword the backend accepts. + effective_method = method if method is not None else self._method if tolerance is not None: - if method in [None, 'least_squares', 'leastsq']: + if effective_method in ['least_squares', 'leastsq']: minimizer_kwargs['ftol'] = tolerance - if method in ['differential_evolution', 'powell', 'cobyla']: + if effective_method in ['differential_evolution', 'powell', 'cobyla']: minimizer_kwargs['tol'] = tolerance return minimizer_kwargs - def convert_to_pars_obj(self, parameters: List[Parameter] | None = None) -> LMParameters: - """ - Create an lmfit compatible container with the ``Parameters`` - converted from the base object. - - Parameters - ---------- - parameters : List[Parameter] | None, default=None - If only a single/selection of parameter is required. Specify - as a list. By default, None. - - Returns - ------- - LMParameters - Lmfit Parameters compatible object. - """ - if parameters is None: - # Assume that we have a ObjBase for which we can obtain a list - parameters = self._object.get_fit_parameters() - lm_parameters = LMParameters().add_many([ - self.convert_to_par_object(parameter) for parameter in parameters - ]) - return lm_parameters - - @staticmethod - def convert_to_par_object(parameter: Parameter) -> LMParameter: - """ - Convert an EasyScience Parameter object to a lmfit Parameter - object. - - Parameters - ---------- - parameter : Parameter - EasyScience parameter to convert. - - Returns - ------- - LMParameter - Lmfit Parameter compatible object. - """ - value = parameter.value - - return LMParameter( - MINIMIZER_PARAMETER_PREFIX + parameter.unique_name, - value=value, - vary=not parameter.fixed, - min=parameter.min, - max=parameter.max, - expr=None, - brute_step=None, - ) - def _make_model(self, pars: LMParameters | None = None) -> LMModel: """ Generate a lmfit model from the supplied ``fit_function`` and @@ -326,7 +250,7 @@ def _make_model(self, pars: LMParameters | None = None) -> LMModel: model = LMModel( fit_func, independent_vars=['x'], - param_names=[MINIMIZER_PARAMETER_PREFIX + str(key) for key in pars.keys()], + param_names=[PARAMETER_PREFIX + str(key) for key in pars.keys()], ) # Assign values from the `Parameter` to the model for name, item in pars.items(): @@ -336,7 +260,7 @@ def _make_model(self, pars: LMParameters | None = None) -> LMModel: value = item.value model.set_param_hint( - MINIMIZER_PARAMETER_PREFIX + str(name), + PARAMETER_PREFIX + str(name), value=value, min=item.min, max=item.max, @@ -346,7 +270,7 @@ def _make_model(self, pars: LMParameters | None = None) -> LMModel: self._cached_model = model return model - def _set_parameter_fit_result(self, fit_result: ModelResult, stack_status: bool) -> None: + def _set_parameter_fit_result(self, fit_result: ModelResult) -> None: """ Update parameters to their final values and assign a std error to them. @@ -355,24 +279,18 @@ def _set_parameter_fit_result(self, fit_result: ModelResult, stack_status: bool) ---------- fit_result : ModelResult Fit object which contains info on the fit. - stack_status : bool - Whether the undo stack was enabled. """ - from easyscience import global_object - pars = self._cached_pars - if stack_status: - self._restore_parameter_values() - global_object.stack.enabled = True - global_object.stack.beginMacro('Fitting routine') for name in pars.keys(): - pars[name].value = fit_result.params[MINIMIZER_PARAMETER_PREFIX + str(name)].value + pars[name].value = fit_result.params[PARAMETER_PREFIX + str(name)].value if fit_result.errorbars: - pars[name].error = fit_result.params[MINIMIZER_PARAMETER_PREFIX + str(name)].stderr + pars[name].error = fit_result.params[PARAMETER_PREFIX + str(name)].stderr else: - pars[name].error = 0.0 - if stack_status: - global_object.stack.endMacro() + # No covariance available (gradient-free method, aborted fit, or a + # parameter at a bound). None keeps that distinguishable from a + # genuine zero uncertainty and clears any stale error from a + # previous fit. + pars[name].error = None def _gen_fit_results(self, fit_results: ModelResult, **kwargs: Any) -> FitResults: """ diff --git a/src/easyscience/fitting/available_minimizers.py b/src/easyscience/fitting/minimizers/minimizers.py similarity index 54% rename from src/easyscience/fitting/available_minimizers.py rename to src/easyscience/fitting/minimizers/minimizers.py index 311bf35e..b8593c1b 100644 --- a/src/easyscience/fitting/available_minimizers.py +++ b/src/easyscience/fitting/minimizers/minimizers.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from enum import Enum +from enum import unique from easyscience import global_object @@ -48,7 +49,8 @@ class AvailableMinimizer: enum_id: int -class AvailableMinimizers(AvailableMinimizer, Enum): +@unique +class Minimizers(AvailableMinimizer, Enum): if lmfit_engine_available: LMFit = 'lm', 'leastsq', 11 LMFit_leastsq = 'lm', 'leastsq', 12 @@ -66,39 +68,3 @@ class AvailableMinimizers(AvailableMinimizer, Enum): if dfo_engine_available: DFO = 'dfo', 'leastsq', 31 DFO_leastsq = 'dfo', 'leastsq', 32 - - -# Temporary solution to convert string to enum -def from_string_to_enum(minimizer_name: str) -> AvailableMinimizers: - if minimizer_name == 'LMFit': - minmizer_enum = AvailableMinimizers.LMFit - elif minimizer_name == 'LMFit_leastsq': - minmizer_enum = AvailableMinimizers.LMFit_leastsq - elif minimizer_name == 'LMFit_powell': - minmizer_enum = AvailableMinimizers.LMFit_powell - elif minimizer_name == 'LMFit_cobyla': - minmizer_enum = AvailableMinimizers.LMFit_cobyla - elif minimizer_name == 'LMFit_differential_evolution': - minmizer_enum = AvailableMinimizers.LMFit_differential_evolution - elif minimizer_name == 'LMFit_scipy_least_squares': - minmizer_enum = AvailableMinimizers.LMFit_scipy_least_squares - - elif minimizer_name == 'Bumps': - minmizer_enum = AvailableMinimizers.Bumps - elif minimizer_name == 'Bumps_simplex': - minmizer_enum = AvailableMinimizers.Bumps_simplex - elif minimizer_name == 'Bumps_newton': - minmizer_enum = AvailableMinimizers.Bumps_newton - elif minimizer_name == 'Bumps_lm': - minmizer_enum = AvailableMinimizers.Bumps_lm - - elif minimizer_name == 'DFO': - minmizer_enum = AvailableMinimizers.DFO - elif minimizer_name == 'DFO_leastsq': - minmizer_enum = AvailableMinimizers.DFO_leastsq - else: - raise ValueError( - f'Invalid minimizer name: {minimizer_name}. The following minimizers are available: {[minimize.name for minimize in AvailableMinimizers]}' # noqa: E501 - ) - - return minmizer_enum diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index e2d88943..b90bd607 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -1,13 +1,17 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from collections.abc import Sequence from typing import Callable import numpy as np -from ..base_classes import CollectionBase +from ..base_classes import EasyList +from ..base_classes import ModelBase from .fitter import Fitter from .minimizers import FitResults +from .reshaping import inject_x_multi +from .reshaping import reshape_datasets class MultiFitter(Fitter): @@ -20,28 +24,53 @@ class MultiFitter(Fitter): The inherited ``fit`` wrapper from ``Fitter`` is used unchanged, including support for forwarding progress callbacks to the active minimizer. + """ def __init__( self, - fit_objects: list | None = None, - fit_functions: list[Callable] | None = None, + fit_objects: Sequence | None = None, + fit_functions: Sequence[Callable] | None = None, ): - # Create a dummy core object to hold all the fit objects. - self._fit_objects = CollectionBase('multi', *fit_objects) - self._fit_functions = fit_functions + # Both arguments default to None so the constructor can be called + # empty; normalise to empty sequences so nothing below has to + # special-case None. + if fit_objects is None: + fit_objects = [] + if fit_functions is None: + fit_functions = [] + # Aggregate the fit objects so a single object can be sent to Fitter. + # *-unpacking keeps any sequence (list, tuple, etc) working. + self._fit_objects = EasyList(*fit_objects, protected_types=ModelBase) + self._fit_functions = list(fit_functions) # Initialize with the first of the fit_functions, without this it is - # not possible to change the fitting engine. - super().__init__(self._fit_objects, self._fit_functions[0]) + # not possible to change the fitting engine. With no functions given + # the Fitter is created with ``None``; the minimizer only stores the + # callable, so this is harmless until a fit is attempted. + first_fit_function = self._fit_functions[0] if self._fit_functions else None + super().__init__(self._fit_objects, first_fit_function) + + @property + def fit_functions(self) -> list[Callable]: + """ + Get the per-dataset fit functions, in dataset order. + + Returns + ------- + list[Callable] + One fit function per dataset. + """ + return list(self._fit_functions) def _fit_function_wrapper( - self, real_x: list[np.ndarray] | None = None, flatten: bool = True + self, + real_x: list[np.ndarray] | None = None, + flatten: bool = True, ) -> Callable: """ - Simple fit function which injects the N real X (independent) - values into the optimizer function. - - This will also flatten the results if needed. + Wrap the per-dataset fit functions into one function that + evaluates each on its real X (independent) values and + concatenates the results, flattening if needed. Parameters ---------- @@ -56,81 +85,9 @@ def _fit_function_wrapper( Callable Wrapped optimizer function. """ - # Extract of a list of callable functions - wrapped_fns = [] - for this_x, this_fun in zip(real_x, self._fit_functions): - self._fit_function = this_fun - wrapped_fns.append(Fitter._fit_function_wrapper(self, this_x, flatten=flatten)) - - def wrapped_fun(x, **kwargs): - # Generate an empty Y based on x - y = np.zeros_like(x) - i = 0 - # Iterate through wrapped functions, passing the WRONG x, the correct - # x was injected in the step above. - for idx, dim in enumerate(self._dependent_dims): - ep = i + np.prod(dim) - y[i:ep] = wrapped_fns[idx](x, **kwargs) - i = ep - return y - - return wrapped_fun - - @staticmethod - def _precompute_reshaping( - x: list[np.ndarray], - y: list[np.ndarray], - weights: list[np.ndarray] | None, - vectorized: bool, - ) -> tuple[np.ndarray, list[np.ndarray], np.ndarray, np.ndarray | None, list[tuple[int, ...]]]: - """ - Convert an array of X's and Y's to an acceptable shape for - fitting. - - Parameters - ---------- - x : list[np.ndarray] - List of independent variables. - y : list[np.ndarray] - List of dependent variables. - weights : list[np.ndarray] | None - Optional weights for each dataset. - vectorized : bool - When ``True``, each x array may be multi-dimensional (e.g. - an ``(N, M, 2)`` grid for a 2D model) and is left as-is. - When ``False`` (default), each x array is expected to be - 1-D. + return inject_x_multi(self._fit_functions, real_x, self._dependent_dims, flatten=flatten) - Returns - ------- - tuple[np.ndarray, list[np.ndarray], np.ndarray, np.ndarray | None, list[tuple[int, ...]]] - Reshaped x values, reshaped input data, flattened y values, - flattened weights, and stored dependent dimensions. - """ - if weights is None: - weights = [None] * len(x) - _, _x_new, _y_new, _weights, _dims = Fitter._precompute_reshaping( - x[0], y[0], weights[0], vectorized - ) - x_new = [_x_new] - y_new = [_y_new] - w_new = [_weights] - dims = [_dims] - for _x, _y, _w in zip(x[1::], y[1::], weights[1::]): - _, _x_new, _y_new, _weights, _dims = Fitter._precompute_reshaping( - _x, _y, _w, vectorized - ) - x_new.append(_x_new) - y_new.append(_y_new) - w_new.append(_weights) - dims.append(_dims) - y_new = np.hstack(y_new) - if w_new[0] is None: - w_new = None - else: - w_new = np.hstack(w_new) - x_fit = np.linspace(0, y_new.size - 1, y_new.size) - return x_fit, x_new, y_new, w_new, dims + _precompute_reshaping = staticmethod(reshape_datasets) def _post_compute_reshaping( self, diff --git a/src/easyscience/fitting/reshaping.py b/src/easyscience/fitting/reshaping.py new file mode 100644 index 00000000..c68a7739 --- /dev/null +++ b/src/easyscience/fitting/reshaping.py @@ -0,0 +1,216 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Data reshaping and fit-function wrapping shared by ``Fitter`` and ``Sampler``. + +Both fitting and sampling provide the engine witha flat 1-D ``y`` array +and a dummy 1-D ``x`` index array, while the user's fit function is called +with the real ``x``. The helpers here do the translation in one place: +``reshape_dataset``/``inject_x`` do one dataset, +``reshape_datasets``/``inject_x_multi`` do a list of datasets. +""" + +from __future__ import annotations + +import functools +from typing import Callable + +import numpy as np + + +def reshape_dataset( + x: np.ndarray, + y: np.ndarray, + weights: np.ndarray | None, + vectorized: bool, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray | None, tuple[int, ...]]: + """ + Check the dimensions of the inputs and reshape if necessary. + + Parameters + ---------- + x : np.ndarray + Independent points; 1-D, or ND with the coordinate components on + the last axis. + y : np.ndarray + Dependent points, one per observation. + weights : np.ndarray | None + Optional weights for the fit. + vectorized : bool + Whether ``x`` already stores vectorized coordinates. + + Returns + ------- + tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray | None, tuple[int, ...]] + Dummy x index array for the engine, reshaped x values, flattened + y values, flattened weights, and the shape of ``y`` (the dependent + dimensions; its product is the number of observations, which the + multi-dataset helpers use to slice the combined output). + + Raises + ------ + ValueError + If the shapes of ``x`` and ``y`` are incompatible. + """ + # Make sure that they are np arrays + x_new = np.array(x) + y_new = np.array(y) + # Get the shapes + x_shape = x_new.shape + y_shape = y_new.shape + # Check if the x data is 1D + if len(x_shape) > 1: + # It is ND data + # Check if the data is vectorized. i.e. should x be [NxMx...x Ndims] + if vectorized: + # Assert that the shapes are the same + if np.all(x_shape[:-1] != y_new.shape): + raise ValueError('The shape of the x and y data must be the same') + # If so do nothing but note that the data is vectorized + # x_shape = (-1,) # Should this be done? + else: + # Assert that the shapes are the same + if np.prod(x_new.shape[:-1]) != y_new.size: + raise ValueError('The number of elements in x and y data must be the same') + # Reshape the data to be [len(NxMx..), Ndims] i.e. flatten to columns + x_new = x_new.reshape(-1, x_shape[-1], order='F') + else: + # Assert that the shapes are the same + if np.all(x_shape != y_new.shape): + raise ValueError('The shape of the x and y data must be the same') + # It is 1D data + x_new = x.flatten() + # The optimizer needs a 1D array, flatten the y data + y_new = y_new.flatten() + if weights is not None: + weights = np.array(weights).flatten() + # Make a 'dummy' x array for the fit function + x_for_fit = np.array(range(y_new.size)) + return x_for_fit, x_new, y_new, weights, y_shape + + +def reshape_datasets( + x: list[np.ndarray], + y: list[np.ndarray], + weights: list[np.ndarray] | None, + vectorized: bool, +) -> tuple[np.ndarray, list[np.ndarray], np.ndarray, np.ndarray | None, list[tuple[int, ...]]]: + """ + Convert a list of X's and Y's to an acceptable shape for fitting. + + Parameters + ---------- + x : list[np.ndarray] + List of independent variables. + y : list[np.ndarray] + List of dependent variables. + weights : list[np.ndarray] | None + Optional weights for each dataset. + vectorized : bool + When ``True``, each x array may be multi-dimensional (e.g. an + ``(N, M, 2)`` grid for a 2D model) and is left as-is. When + ``False`` (default), each x array is expected to be 1-D. + + Returns + ------- + tuple[np.ndarray, list[np.ndarray], np.ndarray, np.ndarray | None, list[tuple[int, ...]]] + Dummy x index array for the engine, per-dataset reshaped x values, + concatenated y values, concatenated weights, and the per-dataset + dependent dimensions. + """ + if weights is None: + weights = [None] * len(x) + x_new, y_new, w_new, dims = [], [], [], [] + for _x, _y, _w in zip(x, y, weights): + _, _x_new, _y_new, _w_new, _dims = reshape_dataset(_x, _y, _w, vectorized) + x_new.append(_x_new) + y_new.append(_y_new) + w_new.append(_w_new) + dims.append(_dims) + y_new = np.hstack(y_new) + w_new = None if w_new[0] is None else np.hstack(w_new) + x_fit = np.linspace(0, y_new.size - 1, y_new.size) + return x_fit, x_new, y_new, w_new, dims + + +def inject_x( + fit_function: Callable, + real_x: np.ndarray | None = None, + flatten: bool = True, +) -> Callable: + """ + Wrap a fit function so it ignores the engine's dummy x and evaluates + on ``real_x`` instead, flattening the result if needed. + + Parameters + ---------- + fit_function : Callable + The user's fit function. + real_x : np.ndarray | None, default=None + Independent x values to be injected. By default, None. + flatten : bool, default=True + Should the result be a flat 1D array? By default, True. + + Returns + ------- + Callable + Wrapped optimizer function. + """ + + @functools.wraps(fit_function) + def wrapped_fit_function(x, **kwargs): + if real_x is not None: + x = real_x + dependent = fit_function(x, **kwargs) + if flatten: + dependent = dependent.flatten() + return dependent + + return wrapped_fit_function + + +def inject_x_multi( + fit_functions: list[Callable], + real_x: list[np.ndarray], + dims: list[tuple[int, ...]], + flatten: bool = True, +) -> Callable: + """ + Wrap one fit function per dataset into a single function whose output + is the concatenation of the per-dataset outputs. + + Parameters + ---------- + fit_functions : list[Callable] + One fit function per dataset. + real_x : list[np.ndarray] + One independent x array per dataset, injected into the matching + fit function. + dims : list[tuple[int, ...]] + Per-dataset dependent (``y``) shapes used to slice the combined + output, as returned by ``reshape_datasets``. + flatten : bool, default=True + Should each per-dataset result be flattened? By default, True. + + Returns + ------- + Callable + Wrapped optimizer function. + """ + wrapped_fns = [ + inject_x(this_fun, this_x, flatten=flatten) + for this_x, this_fun in zip(real_x, fit_functions) + ] + + def wrapped_fun(x, **kwargs): + # Generate an empty Y based on x + y = np.zeros_like(x) + i = 0 + # Iterate through wrapped functions, passing the WRONG x, the correct + # x was injected in the step above. + for wrapped, dim in zip(wrapped_fns, dims): + ep = i + np.prod(dim) + y[i:ep] = wrapped(x, **kwargs) + i = ep + return y + + return wrapped_fun diff --git a/src/easyscience/fitting/sampler.py b/src/easyscience/fitting/sampler.py index d3915115..298e36f7 100644 --- a/src/easyscience/fitting/sampler.py +++ b/src/easyscience/fitting/sampler.py @@ -15,7 +15,9 @@ from easyscience import global_object -from .minimizers.minimizer_base import MINIMIZER_PARAMETER_PREFIX +from .engine_base import PARAMETER_PREFIX +from .reshaping import inject_x_multi +from .reshaping import reshape_datasets if TYPE_CHECKING: # avoid import cycles; only needed for type hints from bumps.dream.state import MCMCDraw @@ -51,7 +53,6 @@ def _data_fingerprint( def _validate_dataset_arrays( name: str, data: np.ndarray | list | tuple, - allow_none_entries: bool = False, ) -> None: """Check that ``data`` (an array or list of arrays) holds numeric, at-least-1-D, non-empty arrays. @@ -67,9 +68,6 @@ def _validate_dataset_arrays( ``'weights'``). data : np.ndarray | list | tuple A single dataset array, or a list/tuple of dataset arrays. - allow_none_entries : bool, default=False - Accept ``None`` entries inside a list (used for per-dataset - optional weights). Raises ------ @@ -81,8 +79,6 @@ def _validate_dataset_arrays( is_multi = isinstance(data, (list, tuple)) for i, entry in enumerate(data if is_multi else [data]): label = f'{name}[{i}]' if is_multi else name - if entry is None and allow_none_entries: - continue try: arr = np.asarray(entry) except Exception as exc: @@ -108,8 +104,6 @@ def _copy_data(data): in-place mutation of the copies, so the chain and the ``save()`` fingerprint always describe the data actually sampled. """ - if data is None: - return None if isinstance(data, (list, tuple)): return [_copy_data(d) for d in data] arr = np.array(data, copy=True) @@ -173,7 +167,7 @@ def load_chain(path: str | os.PathLike, skip: int = 0) -> tuple[MCMCDraw, list[s """Reload a DREAM chain state saved by ``Sampler.save``. This is the standalone reader: unlike ``Sampler.load_state`` it needs no - fitter, model or data, so a saved chain can be inspected or post-processed + model or data, so a saved chain can be inspected or post-processed on a machine that does not have the model. Parameter names are restored from the sidecar when available (schema versions 1 and 2), falling back to the state's labels with the minimizer prefix stripped. @@ -218,9 +212,7 @@ def load_chain(path: str | os.PathLike, skip: int = 0) -> tuple[MCMCDraw, list[s # save_state/load_state does not preserve labels, so a reloaded state # typically carries default labels like ['P0', 'P1', ...]. param_names = [ - lbl[len(MINIMIZER_PARAMETER_PREFIX) :] - if lbl.startswith(MINIMIZER_PARAMETER_PREFIX) - else lbl + lbl[len(PARAMETER_PREFIX) :] if lbl.startswith(PARAMETER_PREFIX) else lbl for lbl in state.labels ] @@ -251,19 +243,9 @@ class SamplingResults: logp: np.ndarray state: MCMCDraw - def to_legacy_dict(self) -> dict: - """Return the legacy dict shape produced by the deprecated - ``mcmc_sample()`` APIs.""" - return { - 'draws': self.draws, - 'param_names': self.param_names, - 'internal_bumps_object': self.state, - 'logp': self.logp, - } - class Sampler: - """Bayesian MCMC sampler for one dataset, backed by a Fitter's BUMPS minimizer. + """Bayesian MCMC sampler for one dataset, backed by the BUMPS DREAM engine. One ``Sampler`` instance represents one chain over one ``(x, y, weights)`` dataset. The data is bound at construction; ``sample()`` and ``extend()`` @@ -274,32 +256,27 @@ class Sampler: effect on the sampler, and there are deliberately no setters: to sample different data, create a new ``Sampler``. - Construct directly with a configured ``Fitter`` (or ``MultiFitter``) whose - minimizer has been switched to ``AvailableMinimizers.Bumps``. **Running a - fit first is not required** β€” the ``Fitter`` supplies the model and the - minimizer, not a fit result, and sampling from the initial parameter values - works fine. - - It is often worth fitting first anyway. DREAM seeds its whole starting - population inside a tiny ball around the parameters' *current* values - (BUMPS' default ``init='eps'``), so sampling from fitted values starts the - chain in the right region and shortens the burn-in needed to reach the - typical set. From a poor initial guess, expect to burn for longer. - - The sampler is BUMPS/DREAM-specific for now: the BUMPS check in ``_run()`` - is the seam where another backend would plug in. + ``Sampler`` is a parallel entry point to ``Fitter``: it takes the same + ``(fit_object, fit_function)`` pair plus the data. Parameters ---------- - fitter : Fitter - A configured ``Fitter`` (or ``MultiFitter``) whose minimizer has been - switched to ``AvailableMinimizers.Bumps``. + fit_object : object + The EasyScience model object holding the ``Parameter`` instances to + sample. For multiple datasets this is one object exposing all the + parameters (for example an ``EasyList`` of models, or the + ``fit_object`` of a ``MultiFitter``). + fit_function : Callable | list[Callable] + The model function, or one per dataset when ``x``, ``y`` and + ``weights`` are lists of arrays. x : np.ndarray | list[np.ndarray] - Independent variable array (or list of arrays for ``MultiFitter``). + Independent variable array (or list of arrays, one per dataset). y : np.ndarray | list[np.ndarray] - Dependent variable array (or list of arrays for ``MultiFitter``). - weights : np.ndarray | list[np.ndarray | None] | None, default=None - Weight array (or list of arrays for ``MultiFitter``). + Dependent variable array (or list of arrays, one per dataset). + weights : np.ndarray | list[np.ndarray] + Weight array (or list of arrays, one per dataset). Required: + sampling has no default weighting, so a missing weight array is + rejected here rather than deep inside the sampling engine. vectorized : bool, default=False When ``True``, each x array may be multi-dimensional (e.g. an ``(N, M, 2)`` grid for a 2D model) and is left as-is. @@ -311,14 +288,14 @@ class Sampler: Raises ------ TypeError - If ``fitter`` is not Fitter-shaped (no ``minimizer``/``fit_function``), - if any dataset in ``x``/``y``/``weights`` is not a numeric array - (e.g. a string), or ``vectorized``/``sampler_kwargs`` have the wrong - type. + If ``fit_object`` has no ``get_free_parameters``, ``fit_function`` + is not callable, any dataset in ``x``/``y``/``weights`` is not a + numeric array (e.g. a string), or ``vectorized``/``sampler_kwargs`` + have the wrong type. ValueError - If ``x``, ``y`` and ``weights`` do not hold matching structures - (all arrays, or lists of the same length), or any dataset is a - scalar or empty array. + If ``fit_function``, ``x``, ``y`` and ``weights`` do not hold + matching structures (all single, or lists of the same length), or + any dataset is a scalar or empty array. Notes ----- @@ -351,7 +328,12 @@ class Sampler: the whole chain:: sampler = Sampler( - fitter, x, y, weights=w, sampler_kwargs={'trim': False} + parameter_container, + model_function, + x, + y, + weights=w, + sampler_kwargs={'trim': False}, ) Note also that trimming does not survive a ``save()``/``load_state()`` @@ -362,76 +344,149 @@ class Sampler: def __init__( self, - fitter: 'Fitter', + fit_object: object, + fit_function: Callable | list[Callable], x: np.ndarray | list[np.ndarray], y: np.ndarray | list[np.ndarray], - weights: np.ndarray | list[np.ndarray | None] | None = None, + weights: np.ndarray | list[np.ndarray], vectorized: bool = False, sampler_kwargs: dict | None = None, ): - if not (hasattr(fitter, 'minimizer') and hasattr(fitter, 'fit_function')): - raise TypeError( - f'fitter must be a configured Fitter or MultiFitter, got {type(fitter).__name__}.' - ) - x_is_multi = isinstance(x, (list, tuple)) - if x_is_multi != isinstance(y, (list, tuple)): + is_multi = isinstance(x, (list, tuple)) + if is_multi != isinstance(y, (list, tuple)): raise ValueError('x and y must either both be arrays or both be lists of arrays.') - if x_is_multi and len(x) != len(y): + if is_multi and len(x) != len(y): raise ValueError( f'x and y must hold the same number of datasets, got {len(x)} and {len(y)}.' ) - if weights is not None: - if isinstance(weights, (list, tuple)) != x_is_multi: - raise ValueError( - 'weights must match the structure of x and y (array or list of arrays).' - ) - if x_is_multi and len(weights) != len(x): - raise ValueError( - f'weights must hold the same number of datasets as x and y, ' - f'got {len(weights)} and {len(x)}.' - ) + if isinstance(weights, (list, tuple)) != is_multi: + raise ValueError( + 'weights must match the structure of x and y (array or list of arrays).' + ) + if is_multi and len(weights) != len(x): + raise ValueError( + f'weights must hold the same number of datasets as x and y, ' + f'got {len(weights)} and {len(x)}.' + ) _validate_dataset_arrays('x', x) _validate_dataset_arrays('y', y) - if weights is not None: - _validate_dataset_arrays('weights', weights, allow_none_entries=True) + _validate_dataset_arrays('weights', weights) + if isinstance(fit_function, (list, tuple)) != is_multi: + raise ValueError( + 'fit_function must be a list of callables when x, y and weights are ' + 'lists of arrays, and a single callable otherwise.' + ) + if is_multi and len(fit_function) != len(x): + raise ValueError( + f'fit_function must hold one callable per dataset, ' + f'got {len(fit_function)} for {len(x)} datasets.' + ) + fit_functions = list(fit_function) if is_multi else [fit_function] + if not all(callable(f) for f in fit_functions): + raise TypeError('fit_function must be callable (or a list of callables).') + if not hasattr(fit_object, 'get_free_parameters'): + raise TypeError( + f'fit_object must be an EasyScience model object exposing the parameters ' + f'to sample, got {type(fit_object).__name__}.' + ) if not isinstance(vectorized, bool): raise TypeError(f'vectorized must be a bool, got {type(vectorized).__name__}.') if sampler_kwargs is not None and not isinstance(sampler_kwargs, dict): raise TypeError( f'sampler_kwargs must be a dict or None, got {type(sampler_kwargs).__name__}.' ) - self._fitter = fitter - # Defensive copies, exposed read-only: mutating the caller's arrays - # (or the properties) cannot desynchronise the chain and the save() - # fingerprint from the data actually sampled. To sample different - # data, create a new Sampler. - self._x = _copy_data(x) - self._y = _copy_data(y) - self._weights = _copy_data(weights) + self._is_multi = is_multi + self._fit_object = fit_object + self._fit_functions = fit_functions + + self._x = _copy_data(x if is_multi else [x]) + self._y = _copy_data(y if is_multi else [y]) + self._weights = _copy_data(weights if is_multi else [weights]) self._vectorized = vectorized self._default_sampler_kwargs = dict(sampler_kwargs or {}) self._state: MCMCDraw | None = None # current chain state self._results: SamplingResults | None = None + @classmethod + def from_fitter( + cls, + fitter: Fitter, + x: np.ndarray | list[np.ndarray], + y: np.ndarray | list[np.ndarray], + weights: np.ndarray | list[np.ndarray], + vectorized: bool = False, + sampler_kwargs: dict | None = None, + ) -> Sampler: + """Build a ``Sampler`` from the model bound to an existing ``Fitter``. + + A convenience for the common fit-then-sample workflow: the sampler + takes the fitter's ``fit_object`` and fit function(s) and is + otherwise identical to one constructed directly. The fitter is not + retained or modified. A ``MultiFitter`` yields a multi-dataset + sampler, so ``x``, ``y`` and ``weights`` must then be lists of arrays. + + Parameters + ---------- + fitter : Fitter + A configured ``Fitter`` or ``MultiFitter``. It does not need to + have been fitted. + x : np.ndarray | list[np.ndarray] + Independent variable array (or list of arrays, one per dataset). + y : np.ndarray | list[np.ndarray] + Dependent variable array (or list of arrays, one per dataset). + weights : np.ndarray | list[np.ndarray] + Weight array (or list of arrays, one per dataset). + vectorized : bool, default=False + See ``Sampler``. + sampler_kwargs : dict | None, default=None + See ``Sampler``. + + Returns + ------- + Sampler + A sampler bound to the fitter's model and the given data. + """ + # A MultiFitter exposes one function per dataset as ``fit_functions``; + # a plain Fitter has a single ``fit_function``. + fit_function = getattr(fitter, 'fit_functions', None) or fitter.fit_function + return cls( + fitter.fit_object, + fit_function, + x, + y, + weights, + vectorized=vectorized, + sampler_kwargs=sampler_kwargs, + ) + + def _single_or_list(self, data: list): + """Return bound data the way it was passed in: one item or a list copy.""" + return list(data) if self._is_multi else data[0] + @property - def fitter(self) -> Fitter: - """The Fitter supplying the model and minimizer (read-only).""" - return self._fitter + def fit_object(self) -> object: + """The EasyScience model object holding the sampled parameters (read-only).""" + return self._fit_object + + @property + def fit_function(self) -> Callable | list[Callable]: + """The model function, or list of them for multiple datasets (read-only).""" + return self._single_or_list(self._fit_functions) @property def x(self) -> np.ndarray | list[np.ndarray]: """The bound independent variable data (read-only copy).""" - return list(self._x) if isinstance(self._x, list) else self._x + return self._single_or_list(self._x) @property def y(self) -> np.ndarray | list[np.ndarray]: """The bound dependent variable data (read-only copy).""" - return list(self._y) if isinstance(self._y, list) else self._y + return self._single_or_list(self._y) @property - def weights(self) -> np.ndarray | list[np.ndarray | None] | None: - """The bound weight data (read-only copy, or None).""" - return list(self._weights) if isinstance(self._weights, list) else self._weights + def weights(self) -> np.ndarray | list[np.ndarray]: + """The bound weight data (read-only copy).""" + return self._single_or_list(self._weights) @property def state(self) -> MCMCDraw | None: @@ -460,15 +515,7 @@ def logp(self) -> np.ndarray | None: def _fingerprint(self) -> str | None: """SHA-256 fingerprint of the bound (x, y, weights) data, or None.""" - x_list = list(self._x) if isinstance(self._x, (list, tuple)) else [self._x] - y_list = list(self._y) if isinstance(self._y, (list, tuple)) else [self._y] - if self._weights is None: - w_list = [] - elif isinstance(self._weights, (list, tuple)): - w_list = [w for w in self._weights if w is not None] - else: - w_list = [self._weights] - return _data_fingerprint(x_list, y_list, w_list) + return _data_fingerprint(self._x, self._y, self._weights) def _run( self, @@ -478,52 +525,46 @@ def _run( population: int | None, resume_state: MCMCDraw | None, sampler_kwargs: dict | None, - progress_callback: Callable[[dict], bool | None] | None, + progress_callback: Callable[[dict], None] | None, abort_test: Callable[[], bool] | None, ) -> SamplingResults: """Shared sampling engine for ``sample()`` and ``extend()``. Argument validation for ``samples``/``burn``/``thin`` lives in - ``Bumps.mcmc_sample`` (single source of truth). + ``DreamSampler.run``. """ - # Check the minimizer is BUMPS *before* mutating the fitter β€” a - # non-BUMPS fitter must not be needlessly rebuilt. - minimizer = self._fitter.minimizer - if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): + from .minimizers.minimizers import bumps_engine_available + + if not bumps_engine_available: raise RuntimeError( - 'Bayesian sampling requires a BUMPS minimizer. ' - 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' + 'Bayesian sampling requires the bumps package. ' + 'Install it with ``pip install bumps``.' ) + from .samplers.sampler_bumps import DreamSampler - x_fit, x_new, y_new, w_new, dims = self._fitter._precompute_reshaping( + x_fit, x_new, y_new, w_new, dims = reshape_datasets( self._x, self._y, self._weights, self._vectorized ) - self._fitter._dependent_dims = dims - wrapped = self._fitter._fit_function_wrapper(x_new, flatten=True) + wrapped = inject_x_multi(self._fit_functions, x_new, dims) merged_kwargs = {**self._default_sampler_kwargs, **(sampler_kwargs or {})} - original_fit_func = self._fitter.fit_function - # Assigning fit_function triggers _update_minimizer() and *rebuilds* - # the minimizer object β€” it must be re-fetched after this assignment. - self._fitter.fit_function = wrapped - try: - minimizer = self._fitter.minimizer - result = minimizer.mcmc_sample( - x=x_fit, - y=y_new, - weights=w_new, - samples=samples, - burn=burn, - thin=thin, - population=population, - resume_state=resume_state, - sampler_kwargs=merged_kwargs or None, - progress_callback=progress_callback, - abort_test=abort_test, - ) - finally: - self._fitter.fit_function = original_fit_func + # This is where a sampler factory would plug in once there + # is more than one backend. + engine = DreamSampler(obj=self._fit_object, fit_function=wrapped) + result = engine.run( + x=x_fit, + y=y_new, + weights=w_new, + samples=samples, + burn=burn, + thin=thin, + population=population, + resume_state=resume_state, + sampler_kwargs=merged_kwargs or None, + progress_callback=progress_callback, + abort_test=abort_test, + ) results = SamplingResults( draws=result['draws'], @@ -542,7 +583,7 @@ def sample( thin: int = 10, population: int | None = None, sampler_kwargs: dict | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, abort_test: Callable[[], bool] | None = None, ) -> SamplingResults: """Run fresh Bayesian MCMC sampling on the bound data. @@ -571,9 +612,10 @@ def sample( sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to the BUMPS DREAM sampler (merged over the instance defaults). - progress_callback : Callable[[dict], bool | None] | None, default=None + progress_callback : Callable[[dict], None] | None, default=None Optional callback invoked at each DREAM generation. The payload - dict includes ``iteration`` and ``sampling: True``. + dict includes ``iteration`` and ``sampling: True``. Any return + value is ignored. abort_test : Callable[[], bool] | None, default=None Optional callable that returns ``True`` to abort sampling early. @@ -591,7 +633,7 @@ def sample( Exceptions propagate from the sampling engine: ``ValueError`` if ``samples``, ``burn``, or ``thin`` are invalid, and ``RuntimeError`` - if the active minimizer is not a BUMPS instance. + if the ``bumps`` package is not installed. """ if self._state is not None: global_object.log.getLogger('fitting').warning( @@ -615,7 +657,7 @@ def extend( thin: int = 10, total_samples: int | None = None, sampler_kwargs: dict | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, + progress_callback: Callable[[dict], None] | None = None, abort_test: Callable[[], bool] | None = None, ) -> SamplingResults: """Continue the existing chain with additional samples. @@ -646,8 +688,9 @@ def extend( sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to the BUMPS DREAM sampler (merged over the instance defaults). - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback invoked at each DREAM generation. + progress_callback : Callable[[dict], None] | None, default=None + Optional callback invoked at each DREAM generation. Any return + value is ignored. abort_test : Callable[[], bool] | None, default=None Optional callable that returns ``True`` to abort sampling early. @@ -660,7 +703,8 @@ def extend( ------ RuntimeError If there is no chain to extend (call ``sample()`` or - ``load_state()`` first), or the minimizer is not BUMPS. + ``load_state()`` first), or the ``bumps`` package is not + installed. Notes ----- @@ -705,7 +749,7 @@ def save(self, path: str | os.PathLike) -> None: ``.params.json`` sidecar with the parameter names, the easyscience version, and a fingerprint of the bound data (verified with a warning on ``load_state()``). Use ``load_chain`` to read the - files back without a fitter. + files back without the model. Parameters ---------- @@ -748,7 +792,7 @@ def save(self, path: str | os.PathLike) -> None: def load_state(self, path: str | os.PathLike, skip: int = 0) -> SamplingResults: """Load a previously saved chain into this sampler. - The sampler must be constructed with the same fitter and data used to + The sampler must be constructed with the same model and data used to create the chain β€” ``extend()`` then continues the saved chain. If the sidecar carries a data fingerprint and it does not match this sampler's bound data, a warning is logged (extending a chain against diff --git a/tests/unit/fitting/calculators/__init__.py b/src/easyscience/fitting/samplers/__init__.py similarity index 60% rename from tests/unit/fitting/calculators/__init__.py rename to src/easyscience/fitting/samplers/__init__.py index b1990ec8..a617b861 100644 --- a/tests/unit/fitting/calculators/__init__.py +++ b/src/easyscience/fitting/samplers/__init__.py @@ -1,3 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from .sampler_bumps import DreamSampler +from .validation import validate_run_settings diff --git a/src/easyscience/fitting/samplers/sampler_bumps.py b/src/easyscience/fitting/samplers/sampler_bumps.py new file mode 100644 index 00000000..7190c552 --- /dev/null +++ b/src/easyscience/fitting/samplers/sampler_bumps.py @@ -0,0 +1,367 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""The BUMPS DREAM MCMC engine β€” ``DreamSampler``.""" + +from __future__ import annotations + +import copy +import math +from functools import partial +from typing import TYPE_CHECKING +from typing import Callable + +import numpy as np +from bumps.fitters import FitDriver + +from ..engine_base import PARAMETER_PREFIX +from ..engine_base import EngineBase +from ..minimizers.bumps_utils import BumpsProgressMonitor +from ..minimizers.bumps_utils import build_curve_problem +from ..minimizers.bumps_utils import parameter_names +from ..minimizers.bumps_utils import parameter_snapshot +from ..minimizers.utils import FitError +from .validation import validate_run_settings + +if TYPE_CHECKING: + from bumps.dream.state import MCMCDraw + from bumps.names import FitProblem + + +class DreamSampler(EngineBase): + """ + BUMPS DREAM MCMC engine. Runs and resumes chains for one + ``(obj, fit_function)`` binding. + + This is the minimizer-independent location of Bayesian sampling: it + builds its own BUMPS ``FitProblem`` via the shared ``bumps_utils`` + helpers, so sampling no longer requires the ``Fitter``'s active + minimizer to be BUMPS, only an installed ``bumps`` package. + + ``DreamSampler`` is internal. The public entry point is + :class:`easyscience.fitting.Sampler`. + """ + + package = 'bumps' + + def __init__( + self, + obj: object, #: ModelBase, + fit_function: Callable, + ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 + """ + Initialize the sampling engine. + + Parameters + ---------- + obj : object + Object containing the ``Parameter`` instances to sample. + fit_function : Callable + Callable returning model y values for the supplied x values. + """ + super().__init__(obj=obj, fit_function=fit_function) + + def run( + self, + x: np.ndarray, + y: np.ndarray, + weights: np.ndarray, + samples: int = 10000, + burn: int = 2000, + thin: int = 10, + population: int | None = None, + resume_state: MCMCDraw | None = None, + sampler_kwargs: dict | None = None, + progress_callback: Callable[[dict], None] | None = None, + abort_test: Callable[[], bool] | None = None, + ) -> dict: + """ + Run Bayesian MCMC sampling using the BUMPS DREAM sampler. + + Builds a BUMPS ``FitProblem`` from the bound object and fit + function and runs the DREAM sampler. This is the engine-level + entry point for Bayesian sampling; the public + :class:`~easyscience.fitting.Sampler` delegates to this method + after flattening its bound data. + + Parameters + ---------- + x : np.ndarray + Flattened independent variable array. + y : np.ndarray + Flattened dependent variable array. + weights : np.ndarray + Flattened weight array. Sampling has no default weighting, + so weights are required; ``Sampler`` enforces this at + construction. + samples : int, default=10000 + Number of raw samples to draw across all chains, before thinning. + A guaranteed minimum, not an exact count: DREAM advances in + blocks of 10 generations (one generation = one draw per chain) + and stops at the first block boundary at or past ``samples``. + burn : int, default=2000 + Burn-in generations to discard. BUMPS counts ``burn`` in + generations while ``samples`` counts raw draws, so ``burn=500`` + discards ``500 * n_chains`` raw samples. + thin : int, default=10 + Thinning interval: only every ``thin``-th generation is stored. + population : int | None, default=None + BUMPS DREAM population count per parameter (number of parallel + chains): BUMPS creates ``ceil(population * n_parameters)`` chains. + resume_state : MCMCDraw | None, default=None + A BUMPS ``MCMCDraw`` state object from a previous ``run()`` + call. When provided, DREAM **continues** the saved chain + instead of starting cold. The population, parameter count, + and parameter names must match the current model. Otherwise, a + ``ValueError`` is raised. + + ``samples`` must be the **total** number of raw samples, not an + increment: to extend an existing chain of ``N`` raw samples by + ``M``, pass ``samples=N + M`` (DREAM keeps only the last + ``samples`` draws in its buffer). The `Sampler.extend` helper + computes this for you. + + ``burn`` is forced to 0 on resume: a previously-converged chain is + never re-burned. + + The ``population`` and ``initializer`` parameters + have **no effect** when ``resume_state`` is provided. They + are determined by the saved state. + + Resuming against *different* data is undefined behaviour (the + chain's likelihood changes underneath it). + sampler_kwargs : dict | None, default=None + Additional keyword arguments forwarded to + ``bumps.fitters.fit``. + progress_callback : Callable[[dict], None] | None, default=None + Optional callback for progress updates during sampling. The + payload dict includes ``iteration`` (DREAM generation + number) and ``sampling: True``. Any return value is ignored. + abort_test : Callable[[], bool] | None, default=None + Optional callback that returns ``True`` to signal that + sampling should be aborted. Called periodically during the + DREAM sampling loop. + + Returns + ------- + dict + Dictionary with keys ``'draws'``, ``'param_names'``, + ``'internal_bumps_object'``, and ``'logp'``. + + Raises + ------ + ValueError + If the input shapes or weights are invalid, if + ``progress_callback`` is not callable, or if ``resume_state`` + is incompatible with the current model (parameter count, + names/order, or population mismatch). + FitError + If DREAM sampling was aborted by the user (via + ``abort_test``). + Exception + Re-raised from DREAM fitting if any unexpected error occurs + (parameter values are restored beforehand). + """ + from bumps.fitters import DreamFit + + x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) + + validate_run_settings(samples, burn, thin) + self.validate_arrays(x, y, weights) + + # Build the BUMPS Curve model around the engine's wrapped fit function + problem, _, _ = build_curve_problem(self, x, y, weights) + + pop = population + if resume_state is not None: + pop, burn = self._validate_resume_state(problem, resume_state, population, burn) + + # Build DREAM kwargs. Use the resolved ``pop``, not the raw + # ``population`` argument. On resume ``pop`` is the negative + # absolute chain count that reproduces the saved state's + # population, which BUMPS requires to match. + dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} + if pop is not None: + dream_kwargs['pop'] = pop + if sampler_kwargs: + dream_kwargs.update(sampler_kwargs) + + # Build monitors (same pattern as classical Bumps.fit()) + monitors = [] + if progress_callback is not None: + if not callable(progress_callback): + raise ValueError('progress_callback must be callable') + # Compute total DREAM steps for progress display (burn + sampling generations). + # BUMPS DREAM default population count is 10 when not specified by the user. + # A negative ``pop`` (resume) is an absolute chain count. + _dream_default_pop = 10 + pop_val = abs(pop) if pop is not None else _dream_default_pop + _total_steps = burn + (samples + pop_val - 1) // pop_val + monitors.append( + BumpsProgressMonitor( + problem, + progress_callback, + partial(self._build_sample_progress_payload, total_steps=_total_steps), + ) + ) + + driver = FitDriver( + fitclass=DreamFit, + problem=problem, + monitors=monitors, + abort_test=abort_test if abort_test is not None else (lambda: False), + **dream_kwargs, + ) + driver.clip() + + try: + fit_kwargs = {} + if resume_state is not None: + # Defensive copy: BUMPS mutates the state object in-place + # (via MCMCDraw.resize(): see bumps/dream/core.py allocate_state) + # during resume. Without a copy, the caller's original state + # object is silently altered, making it impossible to compare + # pre- and post-resume state (shape mismatch). + fit_kwargs['fit_state'] = copy.deepcopy(resume_state) + x_opt, fx = driver.fit(**fit_kwargs) + result_state = getattr(driver.fitter, 'state', None) + if result_state is None: + raise FitError('Sampling aborted by user') + except Exception: + self._restore_parameter_values() + raise + + _draw = result_state.draw() + + return { + 'draws': _draw.points, + 'param_names': parameter_names(problem), + 'internal_bumps_object': result_state, + 'logp': _draw.logp, + } + + def _validate_resume_state( + self, + problem: FitProblem, + resume_state: MCMCDraw, + population: int | None, + burn: int, + ) -> tuple[int, int]: + """Check that ``resume_state`` is compatible with ``problem`` and + resolve the population and burn values to use when resuming. + + Parameters + ---------- + problem : FitProblem + The freshly built BUMPS ``FitProblem`` for the current model. + resume_state : MCMCDraw + The saved chain state to resume from. + population : int | None + The caller-supplied population scale factor, or ``None``. + burn : int + The caller-supplied burn-in, ignored (with a warning) on resume. + + Returns + ------- + tuple[int, int] + ``(population, burn)`` to pass to DREAM. The population is + returned as a **negative** number, which BUMPS' + ``initpop.generate`` reads as an absolute chain count, exactly + reproducing the saved state's population. ``burn`` is always 0: + a previously converged chain is never re-burned. + + Raises + ------ + ValueError + If ``resume_state`` is incompatible with the current model + (parameter count, names/order, or population mismatch). + """ + from easyscience import global_object + + logger = global_object.log.getLogger('fitting.bumps') + + # Parameter count + n_params = len(problem._parameters) + if n_params != resume_state.Nvar: + raise ValueError( + f'resume_state has {resume_state.Nvar} parameters but the current ' + f'model has {n_params}. The model must have the same ' + f'number of fitted parameters as when the saved chain was created.' + ) + + prefix = PARAMETER_PREFIX + fresh_names = [(p.name or '')[len(prefix) :] for p in problem._parameters] + state_labels = list(resume_state.labels) + if state_labels and all(lbl.startswith(prefix) for lbl in state_labels): + state_names = [lbl[len(prefix) :] for lbl in state_labels] + if fresh_names != state_names: + raise ValueError( + f'Parameter names/order mismatch between the current model ' + f'and resume_state.\n' + f' Current model : {fresh_names}\n' + f' resume_state : {state_names}' + ) + else: + logger.warning( + 'resume_state does not carry parameter names (it was most ' + 'likely reloaded from disk, where BUMPS does not preserve ' + 'labels). Parameter-name validation is skipped; the saved ' + 'chain is matched to the current model by parameter order. ' + 'Ensure this is the same model, with parameters in the same ' + 'order, used to create the chain.' + ) + + # Population. BUMPS creates ``ceil(population * n_params)`` chains + # and requires the resumed state's chain count to match. + if population is not None: + expected_npop = math.ceil(population * n_params) + if expected_npop != resume_state.Npop: + raise ValueError( + f'Requested population ({population}) would produce ' + f'{expected_npop} chains but the saved state has ' + f'{resume_state.Npop} chains. The population cannot ' + f'be changed on resume.' + ) + if burn > 0: + logger.warning( + f'burn={burn} ignored on resume: a previously converged ' + f'chain is not re-burned. Forcing burn=0.' + ) + + # A negative ``pop`` is read by ``bumps.initpop.generate`` as an + # absolute chain count, exactly reproducing the saved population + # without having to recover the original scale factor. + return -int(resume_state.Npop), 0 + + def _build_sample_progress_payload( + self, + problem: FitProblem, + iteration: int, + point: np.ndarray | None, + nllf: float, + total_steps: int, + ) -> dict: + """ + Build a progress payload for Bayesian DREAM sampling steps. + + Called by :class:`BumpsProgressMonitor` at each DREAM + generation, with ``total_steps`` bound up front by the caller. + The payload includes ``sampling: True`` so downstream consumers + can distinguish sampling progress from classical fitting + progress; the remaining keys match the classical-fit payload + built by the minimizers. + """ + # Use the nllf already computed by the sampler to avoid a costly + # model re-evaluation, and let BUMPS apply its own chisq scaling. + chi2 = float(problem.chisq(nllf=nllf, norm=False)) + reduced_chi2 = float(problem.chisq(nllf=nllf, norm=True)) + + return { + 'iteration': iteration, + 'chi2': chi2, + 'reduced_chi2': reduced_chi2, + 'parameter_values': parameter_snapshot(problem, point), + 'refresh_plots': False, + 'finished': False, + 'sampling': True, + 'total_steps': total_steps, + } diff --git a/src/easyscience/fitting/samplers/validation.py b/src/easyscience/fitting/samplers/validation.py new file mode 100644 index 00000000..00fae214 --- /dev/null +++ b/src/easyscience/fitting/samplers/validation.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Run-settings validation for the DREAM sampler.""" + +from __future__ import annotations + + +def validate_run_settings(samples: int, burn: int, thin: int) -> None: + """Validate the DREAM run settings. + + Parameters + ---------- + samples : int + Number of raw samples to draw; must be a positive integer. + burn : int + Burn-in generations to discard; must be a non-negative integer. + thin : int + Thinning interval; must be a positive integer. + + Raises + ------ + ValueError + If any value is out of range or not an integer. + """ + # bool is a subclass of int, so ``samples=True`` would otherwise pass as + # ``samples=1``; these checks are strict (``10.0`` is rejected), so + # booleans must be rejected too. + if not isinstance(samples, int) or isinstance(samples, bool) or samples <= 0: + raise ValueError('samples must be a positive integer.') + if not isinstance(burn, int) or isinstance(burn, bool) or burn < 0: + raise ValueError('burn must be a non-negative integer.') + if not isinstance(thin, int) or isinstance(thin, bool) or thin < 1: + raise ValueError('thin must be a positive integer.') diff --git a/src/easyscience/global_object/__init__.py b/src/easyscience/global_object/__init__.py index c0934dde..53b783aa 100644 --- a/src/easyscience/global_object/__init__.py +++ b/src/easyscience/global_object/__init__.py @@ -2,7 +2,5 @@ # SPDX-License-Identifier: BSD-3-Clause from .global_object import GlobalObject # noqa: F401 -from .hugger.hugger import ScriptManager # noqa: F401 from .logger import Logger # noqa: F401 -from .map import Map # noqa: F401 -from .undo_redo import UndoStack # noqa: F401 +from .object_registry import ObjectRegistry # noqa: F401 diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index d128fe38..457cbfd1 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -2,9 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause from ..utils.classUtils import singleton -from .hugger.hugger import ScriptManager from .logger import Logger -from .map import Map +from .object_registry import ObjectRegistry @singleton @@ -17,8 +16,7 @@ class GlobalObject: """ __log = Logger() - __map = Map() - __stack = None + __registry = ObjectRegistry() __debug = False def __init__(self): @@ -26,22 +24,8 @@ def __init__(self): self.log: Logger = self.__log # Debug. Global debugging level self.debug: bool = self.__debug - # Stack. This is where the undo/redo operations are stored. - self.stack = self.__stack - # - self.script: ScriptManager = ScriptManager() - # Map. This is the conduit database between all global object species - self.map: Map = self.__map - - def instantiate_stack(self): - """ - The undo/redo stack references the collective. - - Hence it has to be imported after initialization. - """ - from easyscience.global_object.undo_redo import UndoStack - - self.stack = UndoStack() + # Registry. All live EasyScience objects, looked up by unique name + self.registry: ObjectRegistry = self.__registry def generate_unique_name(self, name_prefix: str) -> str: """ @@ -59,9 +43,7 @@ def generate_unique_name(self, name_prefix: str) -> str: str Generated unique name. """ - names_with_prefix = [ - name for name in self.map.vertices() if name.startswith(name_prefix + '_') - ] + names_with_prefix = [name for name in self.registry if name.startswith(name_prefix + '_')] if names_with_prefix: name_with_prefix_count = [0] for name in names_with_prefix: diff --git a/src/easyscience/global_object/hugger/hugger.py b/src/easyscience/global_object/hugger/hugger.py deleted file mode 100644 index f4f405a0..00000000 --- a/src/easyscience/global_object/hugger/hugger.py +++ /dev/null @@ -1,221 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import inspect -import sys -from abc import ABCMeta -from abc import abstractmethod -from typing import List -from typing import Tuple - -from ...utils.classUtils import singleton - - -@singleton -class Store: - __log = [] - __var_ident = 'var_' - __ret_ident = 'ret_' - - def __init__(self): - self.log = self.__log # TODO Async problem? - self.var_ident = self.__var_ident - self.ret_ident = self.__ret_ident - - @staticmethod - def get_defaults() -> dict: - return { - 'log': Store.__log, - 'create_list': Store.__create_list, - 'unique_args': Store.__unique_args, - 'unique_rets': Store.__unique_rets, - 'var_ident': Store.__var_ident, - 'ret_ident': Store.__ret_ident, - } - - def append_log(self, log_entry: str): - self.log.append(log_entry) - - -class ScriptManager: - def __init__(self, enabled=True): - self._store = Store() - self._enabled = enabled - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, value: bool): - self._enabled = value - - def history(self) -> List[str]: - return self._store.log - - def reset_history(self): - defaults = Store.get_defaults() - for key, item in defaults.items(): - setattr(self._store, key, item) - - def append_log(self, log_entry: str): - self._store.log.append(log_entry) - - -class Hugger(metaclass=ABCMeta): - def __init__(self): - self._store = Store() - - @property - def log(self) -> list: - return self._store.log - - @abstractmethod - def patch(self): - pass - - @abstractmethod - def restore(self): - pass - - -class PatcherFactory(Hugger, metaclass=ABCMeta): - def __init__(self): - super().__init__() - - @staticmethod - def is_mutable(arg) -> bool: - ret = True - if isinstance(arg, (int, float, complex, str, tuple, frozenset, bytes, property)): - ret = False - return ret - - @staticmethod - def _caller_name(skip: int = 2): - """ - Get a name of a caller in the format module.class.method - ``skip`` specifies how many levels of stack to skip while - getting caller name. - - skip=1 means "who calls me", skip=2 "who calls my caller" etc. - An empty string is returned if skipped levels exceed stack - height - https://gist.github.com/techtonik/2151727#gistcomment-2333747 - """ - - def stack_(frame): - framelist = [] - while frame: - framelist.append(frame) - frame = frame.f_back - return framelist - - stack = stack_(sys._getframe(1)) - start = 0 + skip - if len(stack) < start + 1: - return '' - parentframe = stack[start] - - name = [] - module = inspect.getmodule(parentframe) - # `modname` can be None when frame is executed directly in console - # TODO(techtonik): consider using __main__ - if module: - name.append(module.__name__) - # detect classname - if 'self' in parentframe.f_locals: - # I don't know any way to detect call from the object method - # XXX: there seems to be no way to detect static method call - it will - # be just a function call - name.append(parentframe.f_locals['self'].__class__.__name__) - codename = parentframe.f_code.co_name - if codename != '': # top level usually - name.append(codename) # function or a method - del parentframe - return '.'.join(name) - - def _append_args(self, *args, **kwargs): - - def check(res): - return ( - id(res) not in self._store.unique_rets - and id(res) not in self._store.create_list - and id(res) not in self._store.unique_args - ) - - for arg in args: - if self.is_mutable(arg) and check(arg): - self._store.unique_args.append(id(arg)) - for item in kwargs.values(): - if self.is_mutable(item) and check(item): - self._store.unique_args.append(id(item)) - - def _append_create(self, obj): - this_id = id(obj) - if this_id not in self._store.create_list: - self._store.create_list.append(this_id) - - def _append_result(self, result) -> int: - ret = 0 - - def check(res): - return ( - id(res) not in self._store.unique_rets - and id(res) not in self._store.create_list - and id(res) not in self._store.unique_args - ) - - if isinstance(result, type(None)): - return ret - elif isinstance(result, tuple): - for res in result: - # if self.is_mutable(res) and check(res): - if check(res): - self._store.unique_rets.append(id(res)) - ret = len(result) - else: - # if self.is_mutable(result) and check(result): - if check(result): - self._store.unique_rets.append(id(result)) - ret = 1 - return ret - - def _append_log(self, log_entry: str): - self._store.log.append(log_entry) - - def __options(self, item) -> Tuple[int, dict]: - this_id = id(item) - option = { - 'create_list': self._store.create_list, - 'return_list': self._store.unique_rets, - 'input_list': self._store.unique_args, - } - return this_id, option - - def _get_position(self, query: str, item) -> int: - this_id, option = self.__options(item) - in_list = self._in_list(query, item) - index = None - if in_list: - index = option.get(query).index(this_id) - return index - - def _in_list(self, query: str, item) -> bool: - this_id, option = self.__options(item) - return this_id in option.get(query, []) - - @staticmethod - def _get_class_that_defined_method(method_in) -> classmethod: - if inspect.ismethod(method_in): - for cls in inspect.getmro(method_in.__self__.__class__): - if cls.__dict__.get(method_in.__name__) is method_in: - return cls - method_in = method_in.__func__ # fallback to __qualname__ parsing - if inspect.isfunction(method_in): - class_name = method_in.__qualname__.split('.', 1)[0].rsplit('.', 1)[0] - try: - cls = getattr(inspect.getmodule(method_in), class_name) - except AttributeError: - cls = method_in.__globals__.get(class_name) - if isinstance(cls, type): - return cls diff --git a/src/easyscience/global_object/hugger/property.py b/src/easyscience/global_object/hugger/property.py deleted file mode 100644 index e36c52e5..00000000 --- a/src/easyscience/global_object/hugger/property.py +++ /dev/null @@ -1,281 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import logging -import sys -from functools import wraps -from typing import Any -from typing import Callable -from typing import List - -from easyscience import global_object - -from .hugger import PatcherFactory -from .hugger import Store - - -class LoggedProperty(property): - """ - Pump up python properties. - - In this case we can see who has called this property and then do - something if a criteria is met. In this case if the caller is not a - member of the ``ObjBase`` class. Note that all high level - ``EasyScience`` objects should be built from ``ObjBase``. - """ - - _global_object = global_object - - def __init__(self, *args, get_id=None, my_self=None, test_class=None, **kwargs): - super(LoggedProperty, self).__init__(*args, **kwargs) - self._get_id = get_id - self._my_self = my_self - self.test_class = test_class - - @staticmethod - def _caller_class(test_class, skip: int = 1): - - def stack_(frame): - frame_list = [] - while frame: - frame_list.append(frame) - frame = frame.f_back - return frame_list - - stack: List[Any] = stack_(sys._getframe(1)) - start = 0 + skip - if len(stack) < start + 1: - return '' - parent_frame = stack[start] - test = False - if 'self' in parent_frame.f_locals: - test = issubclass(parent_frame.f_locals['self'].__class__, test_class) - return test - - def __get__(self, instance, owner=None): - if not global_object.script.enabled: - return super(LoggedProperty, self).__get__(instance, owner) - test = self._caller_class(self.test_class) - res = super(LoggedProperty, self).__get__(instance, owner) - - def result_item(item_to_be_resulted): - if item_to_be_resulted is None: - return None - if global_object.map.is_known(item_to_be_resulted): - global_object.map.change_type(item_to_be_resulted, 'returned') - else: - global_object.map.add_vertex(item_to_be_resulted, obj_type='returned') - - if not test and self._get_id is not None and self._my_self is not None: - if not isinstance(res, list): - result_item(res) - else: - for item in res: - result_item(item) - Store().append_log(self.makeEntry('get', res)) - if global_object.debug: # noqa: S1006 - logging.getLogger('easyscience.global_object.hugger').debug( - "I'm %s and %s has been called from the outside!", self._my_self, self._get_id - ) - return res - - def __set__(self, instance, value): - if not global_object.script.enabled: - return super().__set__(instance, value) - test = self._caller_class(self.test_class) - if not test and self._get_id is not None and self._my_self is not None: - Store().append_log(self.makeEntry('set', value)) - if global_object.debug: # noqa: S1006 - logging.getLogger('easyscience.global_object.hugger').debug( - "I'm %s and %s has been set to %s from the outside!", - self._my_self, - self._get_id, - value, - ) - return super().__set__(instance, value) - - def makeEntry(self, log_type, returns, *args, **kwargs) -> str: - temp = '' - if returns is None: - returns = [] - if not isinstance(returns, list): - returns = [returns] - if log_type == 'get': - for var in returns: - if var.unique_name in global_object.map.returned_objs: - index = global_object.map.returned_objs.index(var.unique_name) - temp += f'{Store().var_ident}{index}, ' - if len(returns) > 0: - temp = temp[:-2] - temp += ' = ' - if self._my_self.unique_name in global_object.map.created_objs: - # for edge in route[::-1]: - index = global_object.map.created_objs.index(self._my_self.unique_name) - temp += f'{self._my_self.__class__.__name__.lower()}_{index}.{self._get_id}' - if self._my_self.unique_name in global_object.map.created_internal: - # We now have to trace.... - route = global_object.map.reverse_route(self._my_self) # noqa: F841 - index = global_object.map.created_internal.index(self._my_self.unique_name) - temp += f'{self._my_self.__class__.__name__.lower()}_{index}.{self._get_id}' - elif log_type == 'set': - if self._my_self.unique_name in global_object.map.created_objs: - index = global_object.map.created_objs.index(self._my_self.unique_name) - temp += f'{self._my_self.__class__.__name__.lower()}_{index}.{self._get_id} = ' - args = args[1:] - for var in args: - if var.unique_name in global_object.map.argument_objs: - index = global_object.map.argument_objs.index(var.unique_name) - temp += f'{Store().var_ident}{index}' - elif var.unique_name in global_object.map.returned_objs: - index = global_object.map.returned_objs.index(var.unique_name) - temp += f'{Store().var_ident}{index}' - elif var.unique_name in global_object.map.created_objs: - index = global_object.map.created_objs.index(var.unique_name) - temp += f'{self._my_self.__class__.__name__.lower()}_{index}' - else: - if isinstance(var, str): - var = '"' + var + '"' - temp += f'{var}' - else: - logging.getLogger('easyscience.global_object.hugger').debug( - '%s is not implemented yet. Sorry', log_type - ) - temp += '\n' - return temp - - -class PropertyHugger(PatcherFactory): - # Properties are immutable, so need to be set at the parent level. However unlike `FunctionHugger` we can't traverse - # the stack to get the parent. So, it and it's name has to be set at initialization. Boo! - - _global_object = global_object - - def __init__(self, klass, prop_name): - super().__init__() - self.klass = klass - if isinstance(prop_name, tuple): - self.prop_name = prop_name[0] - self.property = prop_name[1] - else: - self.prop_name = prop_name - self.property = klass.__dict__.get(prop_name) - self.__patch_ref = { - 'fget': {'old': self.property.fget, 'patcher': self.patch_get}, - 'fset': {'old': self.property.fset, 'patcher': self.patch_set}, - 'fdel': {'old': self.property.fdel, 'patcher': self.patch_del}, - } - - def patch(self): - option = {} - for key, item in self.__patch_ref.items(): - func = getattr(self.property, key) - if func is not None: - if global_object.debug: - logging.getLogger('easyscience.global_object.hugger').debug( - 'Patching property %s.%s', self.klass.__name__, self.prop_name - ) - patch_function: Callable = item.get('patcher') - new_func = patch_function(func) - option[key] = new_func - setattr(self.klass, self.prop_name, property(**option)) - - def restore(self): - if global_object.debug: - logging.getLogger('easyscience.global_object.hugger').debug( - 'Restoring property %s.%s', self.klass.__name__, self.prop_name - ) - setattr(self.klass, self.prop_name, self.property) - - def patch_get(self, func: Callable) -> Callable: - - @wraps(func) - def inner(*args, **kwargs): - if global_object.debug: - logging.getLogger('easyscience.global_object.hugger').debug( - '%s.%s has been called with %s, %s', - self.klass.__name__, - self.prop_name, - args[1:], - kwargs, - ) - res = func(*args, **kwargs) - self._append_args(*args, **kwargs) - self._append_result(res) - self._append_log(self.makeEntry('get', res, *args, **kwargs)) - return res - - return inner - - def patch_set(self, func: Callable) -> Callable: - - @wraps(func) - def inner(*args, **kwargs): - if global_object.debug: - logging.getLogger('easyscience.global_object.hugger').debug( - '%s.%s has been set with %s, %s', - self.klass.__name__, - self.prop_name, - args[1:], - kwargs, - ) - self._append_args(*args, **kwargs) - self._append_log(self.makeEntry('set', None, *args, **kwargs)) - return func(*args, **kwargs) - - return inner - - def patch_del(self, func: Callable) -> Callable: - - @wraps(func) - def inner(*args, **kwargs): - if global_object.debug: - logging.getLogger('easyscience.global_object.hugger').debug( - '%s.%s has been deleted.', self.klass.__name__, self.prop_name - ) - self._append_log(self.makeEntry('del', None, *args, **kwargs)) - return func(*args, **kwargs) - - return inner - - def makeEntry(self, log_type, returns, *args, **kwargs) -> str: - temp = '' - if returns is None: - returns = [] - if not isinstance(returns, list): - returns = [returns] - if log_type == 'get': - for var in returns: - if self._in_list('return_list', var): - index = self._get_position('return_list', var) - temp += f'{self._store.var_ident}{index}, ' - if len(returns) > 0: - temp = temp[:-2] - temp += ' = ' - if self._in_list('create_list', args[0]): - index = self._get_position('create_list', args[0]) - temp += f'{self.klass.__name__.lower()}_{index}.{self.prop_name}' - elif log_type == 'set': - if self._in_list('create_list', args[0]): - index = self._get_position('create_list', args[0]) - temp += f'{self.klass.__name__.lower()}_{index}.{self.prop_name} = ' - args = args[1:] - for var in args: - if self._in_list('input_list', var): - index = self._get_position('input_list', var) - temp += f'{self._store.var_ident}{index}' - elif self._in_list('return_list', var): - index = self._get_position('return_list', var) - temp += f'{self._store.var_ident}{index}' - elif self._in_list('create_list', var): - index = self._get_position('create_list', var) - temp += f'{self.klass.__name__.lower()}_{index}' - else: - if isinstance(var, str): - var = '"' + var + '"' - temp += f'{var}' - else: - logging.getLogger('easyscience.global_object.hugger').debug( - '%s is not implemented yet. Sorry', log_type - ) - temp += '\n' - return temp diff --git a/src/easyscience/global_object/map.py b/src/easyscience/global_object/map.py deleted file mode 100644 index c79fb9ac..00000000 --- a/src/easyscience/global_object/map.py +++ /dev/null @@ -1,337 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import gc -import sys -import weakref -from typing import List -from typing import Optional - - -class _EntryList(list): - def __init__(self, *args, my_type=None, **kwargs): - super(_EntryList, self).__init__(*args, **kwargs) - self.__known_types = {'argument', 'created', 'created_internal', 'returned'} - self.finalizer = None - self._type = [] - if my_type in self.__known_types: - self._type.append(my_type) - - def __repr__(self) -> str: - s = 'Map entry of type: ' - if self._type: - s += ', '.join(self._type) - else: - s += 'Undefined' - if self.finalizer is None: - s += '. Without a finalizer.' - else: - s += '. With a finalizer.' - return s - - def __delitem__(self, key): - super(_EntryList, self).__delitem__(key) - - def remove_type(self, old_type: str): - if old_type in self.__known_types and old_type in self._type: - self._type.remove(old_type) - - def reset_type(self, default_type: str = None): - self._type = [] - self.type = default_type - - @property - def type(self) -> List[str]: - return self._type - - @type.setter - def type(self, value: str): - if value in self.__known_types and value not in self._type: - self._type.append(value) - - @property - def is_argument(self) -> bool: - return 'argument' in self._type - - @property - def is_created(self) -> bool: - return 'created' in self._type - - @property - def is_created_internal(self) -> bool: - return 'created_internal' in self._type - - @property - def is_returned(self) -> bool: - return 'returned' in self._type - - -class Map: - def __init__(self): - # A dictionary of object names and their corresponding objects - self._store = weakref.WeakValueDictionary() - # A dict with object names as keys and a list of their object types as values, with weak references - self.__type_dict = {} - - def _snapshot_items(self): - """ - Return a stable snapshot of __type_dict items. - - Some callers iterate over __type_dict while other threads or - weakref finalizers may modify it. Creating a list snapshot (with - a retry loop) prevents RuntimeError: dictionary changed size - during iteration. - """ - while True: - try: - return list(self.__type_dict.items()) - except RuntimeError: - # Dict changed during snapshot creation, retry - continue - - def vertices(self) -> List[str]: - """ - Returns the vertices of a map. - - Uses a retry loop to handle RuntimeError that can occur when the - WeakValueDictionary is modified during iteration (e.g., by - garbage collection). - """ - while True: - try: - return list(self._store) - except RuntimeError: - # Dictionary changed size during iteration, retry - continue - - def edges(self): - """Returns the edges of a map.""" - return self.__generate_edges() - - @property - def argument_objs(self) -> List[str]: - return self._nested_get('argument') - - @property - def created_objs(self) -> List[str]: - return self._nested_get('created') - - @property - def created_internal(self) -> List[str]: - return self._nested_get('created_internal') - - @property - def returned_objs(self) -> List[str]: - return self._nested_get('returned') - - def _nested_get(self, obj_type: str) -> List[str]: - """Access a nested object in root by key sequence.""" - # Create a stable snapshot of the dict items to avoid RuntimeError - # when the dict is modified during iteration (e.g., by finalizers). - while True: - try: - items = self._snapshot_items() - return [key for key, item in items if obj_type in item.type] - except RuntimeError: - # In case the snapshot itself raises (very rare), retry - continue - - def get_item_by_key(self, item_id: str) -> object: - if item_id in self._store: - return self._store[item_id] - raise ValueError('Item not in map.') - - def is_known(self, vertex: object) -> bool: - """ - Check if a vertex is known in the map. - - All objects should have a 'unique_name' attribute. - """ - return vertex.unique_name in self._store - - def find_type(self, vertex: object) -> List[str]: - if self.is_known(vertex): - return self.__type_dict[vertex.unique_name].type - - def reset_type(self, obj, default_type: str): - if obj.unique_name in self.__type_dict: - self.__type_dict[obj.unique_name].reset_type(default_type) - - def change_type(self, obj, new_type: str): - if obj.unique_name in self.__type_dict: - self.__type_dict[obj.unique_name].type = new_type - - def add_vertex(self, obj: object, obj_type: str = None): - name = obj.unique_name - if name in self._store: - raise ValueError(f'Object name {name} already exists in the graph.') - # Clean up stale entry in __type_dict if the weak reference was collected - # but the finalizer hasn't run yet - if name in self.__type_dict: - del self.__type_dict[name] - - self._store[name] = obj - - entry_list = _EntryList() - entry_list.finalizer = weakref.finalize(obj, self.prune_type_dict, name) - entry_list.type = obj_type - self.__type_dict[name] = entry_list # Add objects type to the list of types - - def add_edge(self, start_obj: object, end_obj: object): - if start_obj.unique_name in self.__type_dict: - self.__type_dict[start_obj.unique_name].append(end_obj.unique_name) - else: - raise AttributeError('Start object not in map.') - - def get_edges(self, start_obj) -> List[str]: - if start_obj.unique_name in self.__type_dict: - return list(self.__type_dict[start_obj.unique_name]) - else: - raise AttributeError - - def __generate_edges(self) -> list: - """ - A static method generating the edges of the map. - - Edges are represented as sets with one (a loop back to the - vertex) or two vertices - """ - edges = [] - # Iterate over a snapshot of items and snapshot neighbour lists to - # avoid concurrent modification issues. - for vertex, neighbours in self._snapshot_items(): - neighbours_snapshot = list(neighbours) - for neighbour in neighbours_snapshot: - if {neighbour, vertex} not in edges: - edges.append({vertex, neighbour}) - return edges - - def prune_vertex_from_edge(self, parent_obj, child_obj): - vertex1 = parent_obj.unique_name - if child_obj is None: - return - vertex2 = child_obj.unique_name - - if vertex1 in self.__type_dict and vertex2 in self.__type_dict[vertex1]: - del self.__type_dict[vertex1][self.__type_dict[vertex1].index(vertex2)] - - def prune_type_dict(self, key: str): - if key in self.__type_dict: - del self.__type_dict[key] - - def prune(self, key: str): - if key in self.__type_dict: - del self.__type_dict[key] - if key in self._store: - del self._store[key] - - def find_isolated_vertices(self) -> list: - """Returns a list of isolated vertices.""" - isolated = [] - for vertex, neighbours in self._snapshot_items(): - if not list(neighbours): - isolated.append(vertex) - return isolated - - def find_path(self, start_vertex: str, end_vertex: str, path=[]) -> list: - """Find a path from start_vertex to end_vertex in map.""" - - graph = self.__type_dict - path = path + [start_vertex] - if start_vertex == end_vertex: - return path - if start_vertex not in graph: - return [] - for vertex in graph[start_vertex]: - if vertex not in path: - extended_path = self.find_path(vertex, end_vertex, path) - if extended_path: - return extended_path - return [] - - def find_all_paths(self, start_vertex: str, end_vertex: str, path=[]) -> list: - """Find all paths from start_vertex to end_vertex in map.""" - - graph = self.__type_dict - path = path + [start_vertex] - if start_vertex == end_vertex: - return [path] - if start_vertex not in graph: - return [] - paths = [] - for vertex in graph[start_vertex]: - if vertex not in path: - extended_paths = self.find_all_paths(vertex, end_vertex, path) - for p in extended_paths: - paths.append(p) - return paths - - def reverse_route(self, end_vertex: str, start_vertex: Optional[str] = None) -> List: - """ - In this case we have an object and want to know the connections - to get to another in reverse. - - We might not know the start_object. In which case we follow the - shortest path to a base vertex. - - Parameters - ---------- - end_vertex : str - Final vertex in the path. - start_vertex : Optional[str], default=None - Starting vertex. If omitted, the shortest reverse route is - chosen automatically. - - Returns - ------- - List - Reverse path from ``end_vertex`` back to ``start_vertex``. - """ - path_length = sys.maxsize - optimum_path = [] - if start_vertex is None: - # We now have to find where to begin..... Iterate over a snapshot - for possible_start, vertices in self._snapshot_items(): - vertices_snapshot = list(vertices) - if end_vertex in vertices_snapshot: - temp_path = self.find_path(possible_start, end_vertex) - if len(temp_path) < path_length: - path_length = len(temp_path) - optimum_path = temp_path - else: - optimum_path = self.find_path(start_vertex, end_vertex) - optimum_path.reverse() - return optimum_path - - def is_connected(self, vertices_encountered=None, start_vertex=None) -> bool: - """Determines if the map is connected.""" - if vertices_encountered is None: - vertices_encountered = set() - graph = self.__type_dict - vertices = list(graph) - if not start_vertex: - # chose a vertex from graph as a starting point - start_vertex = vertices[0] - vertices_encountered.add(start_vertex) - if len(vertices_encountered) != len(vertices): - for vertex in list(graph[start_vertex]): - if vertex not in vertices_encountered and self.is_connected( - vertices_encountered, vertex - ): - return True - else: - return True - return False - - def _clear(self): - """ - Reset the map to an empty state. - - Only to be used for testing - """ - self._store.clear() - self.__type_dict.clear() - gc.collect() - - def __repr__(self) -> str: - return f'Map object of {len(self._store)} vertices.' diff --git a/src/easyscience/global_object/object_registry.py b/src/easyscience/global_object/object_registry.py new file mode 100644 index 00000000..1b94d3d3 --- /dev/null +++ b/src/easyscience/global_object/object_registry.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import weakref +from typing import Iterator + + +class ObjectRegistry: + """Global registry of EasyScience objects, indexed by ``unique_name``. + + Registered objects must have a ``unique_name`` attribute. Values are + held weakly, so an entry disappears once nothing else references the + object. + + Supports ``name in registry``, ``registry[name]``, ``len(registry)`` + and iteration over the registered names. + """ + + def __init__(self): + # A dict with object names as keys and a list of their object + # types as values, with weak references + self._store = weakref.WeakValueDictionary() + + def __getitem__(self, name: str) -> object: + return self._store[name] + + def __contains__(self, name: object) -> bool: + return name in self._store + + def __iter__(self) -> Iterator[str]: + # Iterate over a snapshot, so objects collected mid-loop are safe + return iter(list(self._store)) + + def __len__(self) -> int: + return len(self._store) + + def names(self) -> list[str]: + """All registered unique names.""" + return list(self._store) + + def objects(self) -> list[object]: + """All registered objects that are still alive. + + The list holds strong references, so it keeps those objects alive + until it is released. + """ + return list(self._store.values()) + + def add(self, obj: object) -> None: + """Register ``obj`` under its ``unique_name``. + + Parameters + ---------- + obj : object + The object to register. It must have a ``unique_name`` + attribute. + + Raises + ------ + ValueError + If the name is already registered. + """ + name = obj.unique_name + if name in self._store: + raise ValueError(f'Object name {name} already exists in the registry.') + self._store[name] = obj + + def discard(self, name: str) -> None: + """Remove ``name`` from the registry, if present.""" + self._store.pop(name, None) + + def clear(self) -> None: + """Empty the registry.""" + self._store.clear() + + def __repr__(self) -> str: + return f'ObjectRegistry object of {len(self._store)} entries.' diff --git a/src/easyscience/global_object/undo_redo.py b/src/easyscience/global_object/undo_redo.py deleted file mode 100644 index 141fc5e7..00000000 --- a/src/easyscience/global_object/undo_redo.py +++ /dev/null @@ -1,472 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import abc -import functools -import logging -from collections import UserDict -from collections import deque -from typing import Any -from typing import Callable -from typing import Iterable -from typing import NoReturn -from typing import TypeVar -from typing import Union - -import numpy as np - -# Removed: from easyscience import global_object # Avoid circular import - - -class UndoCommand(metaclass=abc.ABCMeta): - """The Command interface pattern.""" - - def __init__(self, obj) -> None: - self._obj = obj - self._text = None - - @abc.abstractmethod - def undo(self) -> NoReturn: - """Undo implementation which should be overwritten.""" - - @abc.abstractmethod - def redo(self) -> NoReturn: - """Redo implementation which should be overwritten.""" - - @property - def text(self) -> str: - return self._text - - @text.setter - def text(self, text: str) -> NoReturn: - self._text = text - - -T_ = TypeVar('T_', bound=UndoCommand) - - -def dict_stack_deco(func: Callable) -> Callable: - - def inner(obj, *args, **kwargs): - from easyscience import global_object # Local import to avoid circular dependency - - # Only do the work to a NotarizedDict. - if hasattr(obj, '_stack_enabled') and obj._stack_enabled: - if not kwargs: - global_object.stack.push(DictStack(obj, *args)) - else: - global_object.stack.push(DictStackReCreate(obj, **kwargs)) - else: - func(obj, *args, **kwargs) - - return inner - - -class NotarizedDict(UserDict): - """ - A simple dict drop in for EasyScience group classes. - - This is used as it wraps the get/set methods - """ - - def __init__(self, **kwargs): - from easyscience import global_object # Local import to avoid circular dependency - - super().__init__(**kwargs) - self._global_object = global_object - self._stack_enabled = False - - @classmethod - def _classname(cls): - # This method just returns the name of the class - return cls.__name__ - - @dict_stack_deco - def __setitem__(self, key, value): - super(NotarizedDict, self).__setitem__(key, value) - - @dict_stack_deco - def __delitem__(self, key): - super(NotarizedDict, self).__delitem__(key) - - def __repr__(self): - return f'{self._classname()}({self.data})' - - @dict_stack_deco - def reorder(self, **kwargs): - self.data = kwargs.copy() - - -class CommandHolder: - """ - A holder for one or more commands which are added to the stack. - """ - - def __init__(self, text: str = None): - self._commands = deque() - self._text = text - self.__index = 0 - - def append(self, command: T_): - self._commands.appendleft(command) - - def pop(self): - return self._commands.popleft() - - def __iter__(self) -> T_: - while self.__index < len(self): - index = self.__index - self.__index += 1 - yield self._commands[index] - self.__index = 0 - - def __len__(self) -> int: - return len(self._commands) - - @property - def is_macro(self) -> bool: - return len(self) > 1 - - @property - def current(self) -> T_: - return self._commands[0] - - @property - def text(self) -> str: - text = '' - if self._commands: - text = self._commands[-1].text - if self._text is not None: - text = self._text - return text - - @text.setter - def text(self, text: str): - self._text = text - - -class UndoStack: - """Implement a version of QUndoStack without the QT.""" - - def __init__(self, max_history: Union[int, type(None)] = None): - self._history = deque(maxlen=max_history) - self._future = deque(maxlen=max_history) - self._macro_running = False - self._command_running = False - self._max_history = max_history - self._enabled = False - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, state: bool): - if self.enabled and self._macro_running: - self.endMacro() - self._enabled = state - - def force_state(self, state: bool): - self._enabled = state - - @property - def history(self) -> deque: - return self._history - - @property - def future(self) -> deque: - return self._future - - def push(self, command: T_) -> NoReturn: - """Add a command to the history stack.""" - # If we're not enabled, then what are we doing! - if not self.enabled or self._command_running: - # Do the command and leave. - command.redo() - return - # If there's a macro add the command to the command holder - if self._macro_running: - self.history[0].append(command) - else: - # Else create the command holder and add it to the stack - com = CommandHolder() - com.append(command) - self.history.appendleft(com) - # Actually do the command - command.redo() - # Reset the future - self._future = deque(maxlen=self._max_history) - - def pop(self) -> T_: - """ - !! WARNING - TO BE USED WITH EXTREME CAUTION !! !! THIS IS - PROBABLY NOT THE FN YOU'RE LOOKING FOR, IT CAN BREAK A LOT OF - STUFF !! Sometimes you really don't want the last command. - Remove it from the stack - - Returns - ------- - T_ - None. - """ - pop_it = self._history.popleft() - popped = pop_it.pop() - if len(pop_it) > 0: - self.history.appendleft(pop_it) - return popped - - def clear(self) -> None: # NoReturn: - """Remove any commands on the stack and reset the state.""" - self._history = deque(maxlen=self._max_history) - self._future = deque(maxlen=self._max_history) - self._macro_running = False - - def undo(self) -> NoReturn: - """Undo the last change to the stack.""" - if self.canUndo(): - # Move the command from the past to the future - this_command_stack = self._history.popleft() - self._future.appendleft(this_command_stack) - - # Execute all undo commands - for command in this_command_stack: - try: - self._command_running = True - command.undo() - except Exception as e: - logging.getLogger('easyscience.global_object.undo_redo').warning(str(e)) - finally: - self._command_running = False - - def redo(self) -> NoReturn: - """Redo the last ``undo`` command on the stack.""" - if self.canRedo(): - # Move from the future to the past - this_command_stack = self._future.popleft() - self._history.appendleft(this_command_stack) - # Need to go from right to left - this_command_stack = list(this_command_stack) - this_command_stack.reverse() - for command in this_command_stack: - try: - self._command_running = True - command.redo() - except Exception as e: - logging.getLogger('easyscience.global_object.undo_redo').warning(str(e)) - finally: - self._command_running = False - - def beginMacro(self, text: str) -> NoReturn: - """ - Start a bulk update i.e. multiple commands under one undo/redo - command. - """ - if self._macro_running: - raise AssertionError('Cannot start a macro when one is already running') - com = CommandHolder(text) - self.history.appendleft(com) - self._macro_running = True - - def endMacro(self) -> NoReturn: - """ - End a bulk update i.e. multiple commands under one undo/redo - command. - """ - if not self._macro_running: - raise AssertionError('Cannot end a macro when one is not running') - self._macro_running = False - - def canUndo(self) -> bool: - """Can the last command be undone?""" - return len(self._history) > 0 and not self._macro_running - - def canRedo(self) -> bool: - """Can we redo a command?""" - return len(self._future) > 0 and not self._macro_running - - def redoText(self) -> str: - """Text associated with a redo item.""" - text = '' - if self.canRedo(): - text = self.future[0].text - return text - - def undoText(self) -> str: - """Text associated with a undo item.""" - text = '' - if self.canUndo(): - text = self.history[0].text - return text - - -class PropertyStack(UndoCommand): - """Stack operator for when a property setter is wrapped.""" - - def __init__(self, parent, func: Callable, old_value: Any, new_value: Any, text: str = None): - # self.setText("Setting {} to {}".format(func.__name__, new_value)) - super().__init__(self) - self._parent = parent - self._old_value = old_value - self._new_value = new_value - self._set_func = func - self.text = f'{parent} value changed from {old_value} to {new_value}' - if text is not None: - self.text = text - - def undo(self) -> NoReturn: - self._set_func(self._parent, self._old_value) - - def redo(self) -> NoReturn: - self._set_func(self._parent, self._new_value) - - -class FunctionStack(UndoCommand): - def __init__(self, parent, set_func: Callable, unset_func: Callable, text: str = None): - super().__init__(self) - self._parent = parent - self._old_fn = set_func - self._new_fn = unset_func - self.text = f'{parent} called {set_func}' - if text is not None: - self.text = text - - def undo(self): - self._new_fn() - - def redo(self): - self._old_fn() - - -class DictStack(UndoCommand): - def __init__(self, in_dict: NotarizedDict, *args): - super().__init__(self) - self._parent = in_dict - - self._deletion = False - self._creation = False - - self._key = None - self._index = None - self._old_value = None - self._new_value = None - self.text = '' - - if len(args) == 1: - # We are deleting - self._deletion = True - self._index = list(self._parent.keys()).index(args[0]) - self._old_value = self._parent[args[0]] - self._key = args[0] - self.text = f'Deleting {args[0]} from {self._parent}' - elif len(args) == 2: - # We are either creating or setting - self._key = args[0] - self._new_value = args[1] - if self._key in self._parent.keys(): - # We are modifying - self._old_value = self._parent[self._key] - self.text = f'Setting {self._parent}[{self._key}] from {self._old_value} to {self._new_value}' - else: - self._creation = True - self.text = f'Creating {self._parent}[{self._key}] with value {self._new_value}' - else: - raise ValueError - - def undo(self) -> NoReturn: - if self._creation: - # Now we delete - self._parent.data.__delitem__(self._key) - else: - # Now we create/change value - if self._index is None: - self._parent.data.__setitem__(self._key, self._old_value) - else: - # This deals with placing an item in a place - keys = list(self._parent.keys()) - values = list(self._parent.values()) - keys.insert(self._index, self._key) - values.insert(self._index, self._old_value) - self._parent.reorder(**{k: v for k, v in zip(keys, values)}) - - def redo(self) -> NoReturn: - if self._deletion: - # Now we delete - self._parent.data.__delitem__(self._key) - else: - self._parent.data.__setitem__(self._key, self._new_value) - - -class DictStackReCreate(UndoCommand): - def __init__(self, in_dict: NotarizedDict, **kwargs): - super().__init__(self) - self._parent = in_dict - self._old_value = in_dict.data.copy() - self._new_value = kwargs - self.text = 'Updating dictionary' - - def undo(self) -> NoReturn: - self._parent.data = self._old_value - - def redo(self) -> NoReturn: - self._parent.data = self._new_value - - -def property_stack(arg: Union[str, Callable], begin_macro: bool = False) -> Callable: - """ - Decorate a ``property`` setter with undo/redo functionality This - decorator can be used as: - - @property_stack def func() .... - - or - - @property_stack("This is the undo/redo text) def func() .... - - In the latter case the argument is a string which might be - evaluated. The possible markups for this string are; - - ``obj`` - The thing being operated on ``func`` - The function being - called ``name`` - The name of the function being called. - ``old_value`` - The pre-set value ``new_value`` - The post-set value - - An example would be ``Function {name}: Set from {old_value} to - {new_value}`` - """ - - def make_wrapper(func: Callable, name: str, **kwargs) -> Callable: - def wrapper(obj, *args) -> NoReturn: - from easyscience import global_object # Local import to avoid circular dependency - - old_value = getattr(obj, name) - new_value = args[0] - if issubclass(type(old_value), Iterable) or issubclass(type(new_value), Iterable): - ret = np.all(old_value == new_value) - else: - ret = old_value == new_value - if ret: - return - - if global_object.debug: - logging.getLogger('easyscience.global_object.undo_redo').debug( - "I'm %s and have been set from %s to %s!", obj, old_value, new_value - ) - - global_object.stack.push(PropertyStack(obj, func, old_value, new_value, **kwargs)) - - return functools.update_wrapper(wrapper, func) - - if isinstance(arg, Callable): - func = arg - name = func.__name__ - wrapper = make_wrapper(func, name) - setattr(wrapper, 'func', func) - else: - txt = arg - - def wrapper(func: Callable) -> Callable: - name = func.__name__ - inner_wrapper = make_wrapper(func, name, text=txt.format(**locals())) - setattr(inner_wrapper, 'func', func) - return inner_wrapper - - return wrapper diff --git a/src/easyscience/io/__init__.py b/src/easyscience/io/__init__.py deleted file mode 100644 index 6a53db96..00000000 --- a/src/easyscience/io/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-FileCopyrightText: 2025 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from .serializer_base import SerializerBase -from .serializer_component import SerializerComponent -from .serializer_dict import SerializerDict - -__all__ = [ - SerializerBase, - SerializerComponent, - SerializerDict, -] diff --git a/src/easyscience/io/serializer_component.py b/src/easyscience/io/serializer_component.py deleted file mode 100644 index 10c5f7aa..00000000 --- a/src/easyscience/io/serializer_component.py +++ /dev/null @@ -1,123 +0,0 @@ -# SPDX-FileCopyrightText: 2025 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import List -from typing import Optional - -from .serializer_dict import SerializerDict - -if TYPE_CHECKING: - from .serializer_base import SerializerBase - - -class SerializerComponent: - """ - This base class adds the capability of saving and loading - (encoding/decoding, serializing/deserializing) easyscience objects - via the ``encode`` and ``decode`` methods. The default encoder is - ``SerializerDict``, which converts the object to a dictionary. - - Shortcuts for dictionary and encoding is also present. - """ - - def __deepcopy__(self, memo): - return self.from_dict(self.as_dict()) - - def encode( - self, skip: Optional[List[str]] = None, encoder: Optional[SerializerBase] = None, **kwargs - ) -> Any: - """ - Use an encoder to covert an EasyScience object into another - format. Default is to a dictionary using ``SerializerDict``. - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - encoded object. By default, None. - encoder : Optional[SerializerBase], default=None - The encoder to be used for encoding the data. By default, - None. - **kwargs : - Any additional key word arguments to be passed to the - encoder. - - Returns - ------- - Any - Encoded object containing all information to reform an - EasyScience object. - """ - if encoder is None: - encoder = SerializerDict - encoder_obj = encoder() - return encoder_obj.encode(self, skip=skip, **kwargs) - - @classmethod - def decode(cls, obj: Any, decoder: Optional[SerializerBase] = None) -> Any: - """ - Re-create an EasyScience object from the output of an encoder. - The default decoder is ``SerializerDict``. - - Parameters - ---------- - obj : Any - Encoded EasyScience object. - decoder : Optional[SerializerBase], default=None - Decoder to be used to reform the EasyScience object. By - default, None. - - Returns - ------- - Any - Reformed EasyScience object. - """ - - if decoder is None: - decoder = SerializerDict - return decoder.decode(obj) - - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Convert an EasyScience object into a full dictionary using - ``SerializerDict``. This is a shortcut for - ```obj.encode(encoder=SerializerDict)``` - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming - the dictionary. By default, None. - - Returns - ------- - Dict[str, Any] - Encoded object containing all information to reform an - EasyScience object. - """ - - return self.encode(skip=skip, encoder=SerializerDict) - - @classmethod - def from_dict(cls, obj_dict: Dict[str, Any]) -> Any: - """ - Re-create an EasyScience object from a full encoded dictionary. - - Parameters - ---------- - obj_dict : Dict[str, Any] - Dictionary containing the serialized contents (from - ``SerializerDict``) of an EasyScience object. - - Returns - ------- - Any - Reformed EasyScience object. - """ - - return cls.decode(obj_dict, decoder=SerializerDict) diff --git a/src/easyscience/io/serializer_dict.py b/src/easyscience/io/serializer_dict.py deleted file mode 100644 index af6c1e9d..00000000 --- a/src/easyscience/io/serializer_dict.py +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-FileCopyrightText: 2025 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -__author__ = 'https://github.com/materialsvirtuallab/monty/blob/master/monty/json.py' -__version__ = '3.0.0' - - -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import List -from typing import Optional - -from .serializer_base import SerializerBase - -if TYPE_CHECKING: - from .serializer_component import SerializerComponent - - -class SerializerDict(SerializerBase): - """ - This is a serializer that can encode and decode EasyScience objects - to and from a dictionary. - """ - - def encode( - self, - obj: SerializerComponent, - skip: Optional[List[str]] = None, - full_encode: bool = False, - **kwargs: Any, - ) -> Dict[str, Any]: - """ - Convert an EasyScience object to a dictionary. - - Parameters - ---------- - obj : SerializerComponent - Object to be encoded. - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - encoded object. By default, None. - full_encode : bool, default=False - Should the data also be encoded (default False). By default, - False. - **kwargs : Any - Any additional key word arguments to be passed to the - encoder. - - Returns - ------- - Dict[str, Any] - Object encoded to dictionary containing all information to - reform an EasyScience object. - """ - - return self._convert_to_dict(obj, skip=skip, full_encode=full_encode, **kwargs) - - @classmethod - def decode(cls, d: Dict[str, Any]) -> SerializerComponent: - """ - Re-create an EasyScience object from the dictionary - representation. - - Parameters - ---------- - d : Dict[str, Any] - Dict representation of an EasyScience object. - - Returns - ------- - SerializerComponent - EasyScience object. - """ - - return SerializerBase._convert_from_dict(d) diff --git a/src/easyscience/job/analysis.py b/src/easyscience/job/analysis.py index 03686714..06defcf2 100644 --- a/src/easyscience/job/analysis.py +++ b/src/easyscience/job/analysis.py @@ -3,26 +3,29 @@ from abc import ABCMeta from abc import abstractmethod +from typing import Any +from typing import Optional import numpy as np +from ..base_classes.model_base import ModelBase from ..fitting.minimizers import MinimizerBase -from ..legacy.obj_base import ObjBase -class AnalysisBase(ObjBase, metaclass=ABCMeta): +class AnalysisBase(ModelBase, metaclass=ABCMeta): """ This virtual class allows for the creation of technique-specific Analysis objects. """ - def __init__(self, name: str, interface=None, *args, **kwargs): - super(AnalysisBase, self).__init__(name, *args, **kwargs) - self.name = name - self._calculator = None + def __init__( + self, + *, + display_name: Optional[str] = None, + unique_name: Optional[str] = None, + ): + super(AnalysisBase, self).__init__(unique_name=unique_name, display_name=display_name) self._minimizer = None - self._fitter = None - self.interface = interface @abstractmethod def calculate_theory(self, x: np.ndarray, **kwargs) -> np.ndarray: @@ -32,17 +35,6 @@ def calculate_theory(self, x: np.ndarray, **kwargs) -> np.ndarray: def fit(self, x: np.ndarray, y: np.ndarray, e: np.ndarray, **kwargs) -> None: raise NotImplementedError('fit not implemented') - @property - def calculator(self) -> str: - if self._calculator is None: - self._calculator = self.interface.current_interface_name - return self._calculator - - @calculator.setter - def calculator(self, value) -> None: - # TODO: check if the calculator is available for the given JobType - self.interface.switch(value, fitter=self._fitter) - @property def minimizer(self) -> MinimizerBase: return self._minimizer @@ -53,4 +45,4 @@ def minimizer(self, minimizer: MinimizerBase) -> None: # required dunder methods def __str__(self): - return f'Analysis: {self.name}' + return f'Analysis: {self.display_name}' diff --git a/src/easyscience/job/experiment.py b/src/easyscience/job/experiment.py index b3e4daa9..7fc85dd6 100644 --- a/src/easyscience/job/experiment.py +++ b/src/easyscience/job/experiment.py @@ -1,19 +1,20 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from ..legacy.obj_base import ObjBase +from typing import Optional +from ..base_classes.model_base import ModelBase -class ExperimentBase(ObjBase): + +class ExperimentBase(ModelBase): """ This virtual class allows for the creation of technique-specific Experiment objects. """ - def __init__(self, name: str, *args, **kwargs): - super(ExperimentBase, self).__init__(name, *args, **kwargs) - self._name = name + def __init__(self, display_name: Optional[str] = None, unique_name: Optional[str] = None): + super(ExperimentBase, self).__init__(unique_name=unique_name, display_name=display_name) # required dunder methods def __str__(self): - return f'Experiment: {self._name}' + return f'Experiment: {self.display_name}' diff --git a/src/easyscience/job/job.py b/src/easyscience/job/job.py index bfed8bec..dee825fd 100644 --- a/src/easyscience/job/job.py +++ b/src/easyscience/job/job.py @@ -3,22 +3,22 @@ from abc import ABCMeta from abc import abstractmethod +from typing import Optional -from ..legacy.obj_base import ObjBase +from ..base_classes.model_base import ModelBase from .analysis import AnalysisBase from .experiment import ExperimentBase from .theoreticalmodel import TheoreticalModelBase -class JobBase(ObjBase, metaclass=ABCMeta): +class JobBase(ModelBase, metaclass=ABCMeta): """ This virtual class allows for the creation of technique-specific Job objects. """ - def __init__(self, name: str, *args, **kwargs): - super(JobBase, self).__init__(name, *args, **kwargs) - self.name = name + def __init__(self, display_name: Optional[str] = None, unique_name: Optional[str] = None): + super(JobBase, self).__init__(unique_name=unique_name, display_name=display_name) self._theory = None self._experiment = None self._analysis = None diff --git a/src/easyscience/job/theoreticalmodel.py b/src/easyscience/job/theoreticalmodel.py index 3fa87e50..c255e5cd 100644 --- a/src/easyscience/job/theoreticalmodel.py +++ b/src/easyscience/job/theoreticalmodel.py @@ -1,23 +1,20 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from ..legacy.obj_base import ObjBase +from typing import Optional +from ..base_classes.model_base import ModelBase -class TheoreticalModelBase(ObjBase): + +class TheoreticalModelBase(ModelBase): """ This virtual class allows for the creation of technique-specific Theory objects. """ - def __init__(self, name: str, *args, **kwargs): - self._name = name - super().__init__(name, *args, **kwargs) + def __init__(self, display_name: Optional[str] = None, unique_name: Optional[str] = None): + super().__init__(unique_name=unique_name, display_name=display_name) # required dunder methods def __str__(self): raise NotImplementedError('Copy not implemented') - - def as_dict(self, skip: list = []) -> dict: - this_dict = super().as_dict(skip=skip) - return this_dict diff --git a/src/easyscience/legacy/__init__.py b/src/easyscience/legacy/__init__.py deleted file mode 100644 index 482abc26..00000000 --- a/src/easyscience/legacy/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from .collection_base import CollectionBase # noqa: F401 -from .obj_base import ObjBase # noqa: F401 - -__all__ = ['CollectionBase', 'ObjBase'] diff --git a/src/easyscience/legacy/collection_base.py b/src/easyscience/legacy/collection_base.py deleted file mode 100644 index 5be3386c..00000000 --- a/src/easyscience/legacy/collection_base.py +++ /dev/null @@ -1,338 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from collections.abc import MutableSequence -from numbers import Number -from typing import TYPE_CHECKING -from typing import Any -from typing import Callable -from typing import List -from typing import Optional -from typing import Tuple -from typing import Union - -from easyscience import global_object -from easyscience.base_classes.new_base import NewBase -from easyscience.global_object.undo_redo import NotarizedDict - -from ..base_classes.based_base import BasedBase -from ..variable.descriptor_base import DescriptorBase - -if TYPE_CHECKING: - from ..fitting.calculators import InterfaceFactoryTemplate - - -class CollectionBase(BasedBase, MutableSequence): - """ - This is the base class for which all higher level classes are built - off of. - - .. deprecated:: - ``CollectionBase`` is deprecated and will be removed in a future version. - Please migrate to ``ModelBase`` or ``EasyList`` instead. - - NOTE: This object is serializable only if parameters are supplied - as: ``ObjBase(a=value, b=value)``. For ``Parameter`` or - ``Descriptor`` objects we can cheat with - ``ObjBase(*[Descriptor(...), Parameter(...), ...])``. - """ - - def __init__( - self, - name: str, - *args: Union[BasedBase, DescriptorBase, NewBase], - interface: Optional[InterfaceFactoryTemplate] = None, - unique_name: Optional[str] = None, - **kwargs: Any, - ): - """ - Set up the base collection class. - - Parameters - ---------- - name : str - Name of this object. - *args : Union[BasedBase, DescriptorBase, NewBase] - Initial EasyScience objects to include in the collection. - interface : Optional[InterfaceFactoryTemplate], default=None - Interface attached to contained objects. By default, None. - unique_name : Optional[str], default=None - Unique identifier for this collection. By default, None. - **kwargs : Any - Fields which this class should contain. - - Raises - ------ - AttributeError - If a provided item is not an EasyScience object or if a - keyword collides with an internal attribute. - """ - global_object.log.getLogger('legacy').warning( - 'CollectionBase is deprecated and will be removed in a future version. ' - 'Please migrate to ModelBase or EasyList.', - stacklevel=2, - ) - BasedBase.__init__(self, name, unique_name=unique_name) - kwargs = {key: kwargs[key] for key in kwargs.keys() if kwargs[key] is not None} - _args = [] - for item in args: - if not isinstance(item, list): - _args.append(item) - else: - _args += item - _kwargs = {} - for key, item in kwargs.items(): - if isinstance(item, list) and len(item) > 0: - _args += item - else: - _kwargs[key] = item - kwargs = _kwargs - for item in list(kwargs.values()) + _args: - if not issubclass(type(item), (DescriptorBase, BasedBase, NewBase)): - raise AttributeError('A collection can only be formed from easyscience objects.') - args = _args - _kwargs = {} - for key, item in kwargs.items(): - _kwargs[key] = item - for arg in args: - kwargs[arg.unique_name] = arg - _kwargs[arg.unique_name] = arg - - # Set kwargs, also useful for serialization - self._kwargs = NotarizedDict(**_kwargs) - - for key in kwargs.keys(): - if key in self.__dict__.keys() or key in self.__slots__: - raise AttributeError( - f'Given kwarg: `{key}`, is an internal attribute. Please rename.' - ) - if kwargs[key]: # Might be None (empty tuple or list) - self._global_object.map.add_edge(self, kwargs[key]) - self._global_object.map.reset_type(kwargs[key], 'created_internal') - if interface is not None: - kwargs[key].interface = interface - # TODO wrap getter and setter in Logger - if interface is not None: - self.interface = interface - self._kwargs._stack_enabled = True - - def insert(self, index: int, value: Union[DescriptorBase, BasedBase, NewBase]) -> None: - """ - Insert an object into the collection at an index. - - Parameters - ---------- - index : int - Index for EasyScience object to be inserted. - value : Union[DescriptorBase, BasedBase, NewBase] - Object to be inserted. - - Returns - ------- - None - None. - - Raises - ------ - AttributeError - If ``value`` is not an EasyScience object. - """ - t_ = type(value) - if issubclass(t_, (BasedBase, DescriptorBase, NewBase)): - update_key = list(self._kwargs.keys()) - values = list(self._kwargs.values()) - # Update the internal dict - new_key = value.unique_name - update_key.insert(index, new_key) - values.insert(index, value) - self._kwargs.reorder(**{k: v for k, v in zip(update_key, values)}) - # ADD EDGE - self._global_object.map.add_edge(self, value) - self._global_object.map.reset_type(value, 'created_internal') - value.interface = self.interface - else: - raise AttributeError('Only EasyScience objects can be put into an EasyScience group') - - def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase, NewBase]: - """ - Get an item in the collection based on its index. - - Parameters - ---------- - idx : Union[int, slice] - Index or slice of the collection. - - Returns - ------- - Union[DescriptorBase, BasedBase, NewBase] - Object at index ``idx``. - - Raises - ------ - IndexError - If ``idx`` does not resolve to an element. - TypeError - If boolean indexing is attempted. - """ - if isinstance(idx, slice): - start, stop, step = idx.indices(len(self)) - return self.__class__( - getattr(self, 'name'), *[self[i] for i in range(start, stop, step)] - ) - if str(idx) in self._kwargs.keys(): - return self._kwargs[str(idx)] - if isinstance(idx, str): - idx = [index for index, item in enumerate(self) if item.name == idx] - noi = len(idx) - if noi == 0: - raise IndexError('Given index does not exist') - elif noi == 1: - idx = idx[0] - else: - return self.__class__(getattr(self, 'name'), *[self[i] for i in idx]) - elif not isinstance(idx, int) or isinstance(idx, bool): - if isinstance(idx, bool): - raise TypeError('Boolean indexing is not supported at the moment') - try: - if idx > len(self): - raise IndexError(f'Given index {idx} is out of bounds') - except TypeError: - raise IndexError('Index must be of type `int`/`slice` or an item name (`str`)') - keys = list(self._kwargs.keys()) - return self._kwargs[keys[idx]] - - def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase, NewBase]) -> None: - """ - Set an item via it's index. - - Parameters - ---------- - key : int - Index in self. - value : Union[BasedBase, DescriptorBase, NewBase] - Value which index key should be set to. - - Raises - ------ - NotImplementedError - If ``value`` is neither numeric nor an EasyScience object. - """ - if isinstance(value, Number): # noqa: S3827 - item = self.__getitem__(key) - item.value = value - elif issubclass(type(value), (BasedBase, DescriptorBase, NewBase)): - update_key = list(self._kwargs.keys()) - values = list(self._kwargs.values()) - old_item = values[key] - # Update the internal dict - update_dict = {update_key[key]: value} - self._kwargs.update(update_dict) - # ADD EDGE - self._global_object.map.add_edge(self, value) - self._global_object.map.reset_type(value, 'created_internal') - value.interface = self.interface - # REMOVE EDGE - self._global_object.map.prune_vertex_from_edge(self, old_item) - else: - raise NotImplementedError( - 'At the moment only numerical values or EasyScience objects can be set.' - ) - - def __delitem__(self, key: int) -> None: - """ - Try to delete an idem by key. - - Parameters - ---------- - key : int - - Returns - ------- - None - """ - keys = list(self._kwargs.keys()) - item = self._kwargs[keys[key]] - self._global_object.map.prune_vertex_from_edge(self, item) - del self._kwargs[keys[key]] - - def __len__(self) -> int: - """ - Get the number of items in this collection. - - Returns - ------- - int - Number of items in this collection. - """ - return len(self._kwargs.keys()) - - def _convert_to_dict( - self, in_dict: dict, encoder: Any, skip: List[str] = [], **kwargs: Any - ) -> dict: - """ - Convert ones self into a serialized form. - - Parameters - ---------- - in_dict : dict - Dictionary being populated during serialization. - encoder : Any - Serializer used to encode child objects. - skip : List[str], default=[] - Field names to skip during serialization. - **kwargs : Any - Additional keyword arguments passed to the serializer. - - Returns - ------- - dict - Dictionary of ones self. - """ - d = {} - if hasattr(self, '_modify_dict'): - # any extra keys defined on the inheriting class - d = self._modify_dict(skip=skip, **kwargs) - in_dict['data'] = [encoder._convert_to_dict(item, skip=skip, **kwargs) for item in self] - out_dict = {**in_dict, **d} - return out_dict - - @property - def data(self) -> Tuple: - """ - The data function returns a tuple of the keyword arguments - passed to the constructor. This is useful for when you need to - pass in a dictionary of data to other functions, such as with - matplotlib's plot function. - - Returns - ------- - Tuple - The values of the attributes in a tuple :doc-author: - Trelent. - """ - return tuple(self._kwargs.values()) - - def __repr__(self) -> str: - return f'{self.__class__.__name__} `{getattr(self, "name")}` of length {len(self)}' - - def sort( - self, - mapping: Callable[[Union[BasedBase, DescriptorBase, NewBase]], Any], - reverse: bool = False, - ) -> None: - """ - Sort the collection according to the given mapping. - - Parameters - ---------- - mapping : Callable[[Union[BasedBase, DescriptorBase, NewBase]], Any] - Mapping function to sort the collection. i.e. lambda - parameter: parameter.value. - reverse : bool, default=False - Reverse the sorting. By default, False. - """ - i = list(self._kwargs.items()) - i.sort(key=lambda x: mapping(x[1]), reverse=reverse) - self._kwargs.reorder(**{k[0]: k[1] for k in i}) diff --git a/src/easyscience/legacy/dict.py b/src/easyscience/legacy/dict.py deleted file mode 100644 index 9b36c9f8..00000000 --- a/src/easyscience/legacy/dict.py +++ /dev/null @@ -1,179 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -__author__ = 'https://github.com/materialsvirtuallab/monty/blob/master/monty/json.py' -__version__ = '3.0.0' - - -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import List -from typing import Optional - -from .template import BaseEncoderDecoder - -if TYPE_CHECKING: - from .component_serializer import ComponentSerializer - -_KNOWN_CORE_TYPES = ('Descriptor', 'Parameter') - - -class DictSerializer(BaseEncoderDecoder): - """ - This is a serializer that can encode and decode EasyScience objects - to a JSON encoded dictionary. - """ - - def encode( - self, - obj: ComponentSerializer, - skip: Optional[List[str]] = None, - full_encode: bool = False, - **kwargs: Any, - ) -> Dict[str, Any]: - """ - Convert an EasyScience object to a JSON encoded dictionary. - - Parameters - ---------- - obj : ComponentSerializer - Object to be encoded. - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - encoded object. By default, None. - full_encode : bool, default=False - Should the data also be JSON encoded (default False). By - default, False. - **kwargs : Any - Any additional key word arguments to be passed to the - encoder. - - Returns - ------- - Dict[str, Any] - Object encoded to dictionary containing all information to - reform an EasyScience object. - """ - - return self._convert_to_dict(obj, skip=skip, full_encode=full_encode, **kwargs) - - @classmethod - def decode(cls, d: Dict[str, Any]) -> ComponentSerializer: - """ - Decode function. - - Parameters - ---------- - d : Dict[str, Any] - Dict representation. - - Returns - ------- - ComponentSerializer - ComponentSerializer class. - """ - - return BaseEncoderDecoder._convert_from_dict(d) - - @classmethod - def from_dict(cls, d: Dict[str, Any]) -> ComponentSerializer: - """ - From dict. - - Parameters - ---------- - d : Dict[str, Any] - Dict representation. - - Returns - ------- - ComponentSerializer - ComponentSerializer class. - """ - return BaseEncoderDecoder._convert_from_dict(d) - - -class DataDictSerializer(DictSerializer): - """ - This is a serializer that can encode the data in an EasyScience - object to a JSON encoded dictionary. - """ - - def encode( - self, - obj: ComponentSerializer, - skip: Optional[List[str]] = None, - full_encode: bool = False, - **kwargs: Any, - ) -> Dict[str, Any]: - """ - Convert an EasyScience object to a JSON encoded data dictionary. - - Parameters - ---------- - obj : ComponentSerializer - Object to be encoded. - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - encoded object. By default, None. - full_encode : bool, default=False - Should the data also be JSON encoded (default False). By - default, False. - **kwargs : Any - Any additional key word arguments to be passed to the - encoder. - - Returns - ------- - Dict[str, Any] - Object encoded to data dictionary. - - Raises - ------ - ValueError - If ``skip`` is not a list of strings. - """ - - if skip is None: - skip = [] - elif isinstance(skip, str): - skip = [skip] - if not isinstance(skip, list): - raise ValueError('Skip must be a list of strings.') - encoded = super().encode(obj, skip=skip, full_encode=full_encode, **kwargs) - return self._parse_dict(encoded) - - @classmethod - def decode(cls, d: Dict[str, Any]) -> ComponentSerializer: - """ - This function is not implemented as a data dictionary does not - contain the necessary information to re-form an EasyScience - object. - """ - - raise NotImplementedError( - 'It is not possible to reconstitute objects from data only dictionary.' - ) - - @staticmethod - def _parse_dict(in_dict: Dict[str, Any]) -> Dict[str, Any]: - """Strip out any non-data from a dictionary.""" - - out_dict = dict() - for key in in_dict.keys(): - if key[0] == '@': - if key == '@class' and in_dict[key] not in _KNOWN_CORE_TYPES: - out_dict['name'] = in_dict[key] - continue - out_dict[key] = in_dict[key] - if isinstance(in_dict[key], dict): - out_dict[key] = DataDictSerializer._parse_dict(in_dict[key]) - elif isinstance(in_dict[key], list): - out_dict[key] = [ - DataDictSerializer._parse_dict(x) if isinstance(x, dict) else x - for x in in_dict[key] - ] - return out_dict diff --git a/src/easyscience/legacy/json.py b/src/easyscience/legacy/json.py deleted file mode 100644 index 25833fbc..00000000 --- a/src/easyscience/legacy/json.py +++ /dev/null @@ -1,139 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -__author__ = 'https://github.com/materialsvirtuallab/monty/blob/master/monty/json.py' -__version__ = '3.0.0' - - -import json -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import List - -from .template import BaseEncoderDecoder - -if TYPE_CHECKING: - from .component_serializer import ComponentSerializer - - -class JsonSerializer(BaseEncoderDecoder): - def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: - """ - Returns a json string representation of the ComponentSerializer - object. - """ - ENCODER = type( - JsonEncoderTemplate.__name__, - (JsonEncoderTemplate, BaseEncoderDecoder), - {'skip': skip}, - ) - return json.dumps(obj, cls=ENCODER) - - @classmethod - def decode(cls, data: str) -> ComponentSerializer: - return json.loads(data, cls=JsonDecoderTemplate) - - -class JsonDataSerializer(BaseEncoderDecoder): - def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: - """ - Returns a json string representation of the ComponentSerializer - object. - """ - from .dict import DataDictSerializer - - ENCODER = type( - JsonEncoderTemplate.__name__, - (JsonEncoderTemplate, BaseEncoderDecoder), - { - 'skip': skip, - '_converter': lambda *args, **kwargs: DataDictSerializer._parse_dict( - DataDictSerializer._convert_to_dict(*args, **kwargs) - ), - }, - ) - - return json.dumps(obj, cls=ENCODER) - - @classmethod - def decode(cls, data: str) -> ComponentSerializer: - raise NotImplementedError( - 'It is not possible to reconstitute objects from data only objects.' - ) - - -class JsonEncoderTemplate(json.JSONEncoder): - """ - A Json Encoder which supports the ComponentSerializer API, plus adds - support for numpy arrays, datetime objects, bson ObjectIds (requires - bson). - - Usage:: - - # Add it as a *cls* keyword when using json.dump - json.dumps(object, cls=MontyEncoder) - """ - - skip = [] - _converter = BaseEncoderDecoder._convert_to_dict - - def default(self, o: Any) -> Dict[str, Any]: # pylint: disable=E0202 - """ - Overriding default method for JSON encoding. - - This method does two things: (a) If an object has a to_dict - property, return the to_dict output. (b) If the @module and - @class keys are not in the to_dict, add them to the output - automatically. If the object has no to_dict property, the - default Python json encoder default method is called. - - Parameters - ---------- - o : Any - Python object. - - Returns - ------- - Dict[str, Any] - JSON-serializable dictionary representation of ``o``. - """ - return self._converter(o, self.skip, full_encode=True) - - -class JsonDecoderTemplate(json.JSONDecoder): - """ - A Json Decoder which supports the ComponentSerializer API. - - By default, the decoder attempts to find a module and name - associated with a dict. If found, the decoder will generate a - Pymatgen as a priority. If that fails, the original decoded - dictionary from the string is returned. Note that nested lists and - dicts containing pymatgen object will be decoded correctly as well. - - Usage: - - # Add it as a *cls* keyword when using json.load - json.loads(json_string, cls=MontyDecoder) - """ - - _converter = BaseEncoderDecoder._convert_from_dict - - def decode(self, s: str) -> Any: - """ - Overrides decode from JSONDecoder. - - Parameters - ---------- - s : str - JSON string representation. - - Returns - ------- - Any - Decoded object. - """ - d = json.JSONDecoder.decode(self, s) - return self.__class__._converter(d) diff --git a/src/easyscience/legacy/legacy_core.py b/src/easyscience/legacy/legacy_core.py deleted file mode 100644 index 40a5beac..00000000 --- a/src/easyscience/legacy/legacy_core.py +++ /dev/null @@ -1,220 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import json -from collections import OrderedDict -from hashlib import sha1 -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import List -from typing import Optional - -from ..io.dict import DataDictSerializer -from ..io.dict import DictSerializer -from ..io.json import jsanitize - -if TYPE_CHECKING: - from ..io.template import BaseEncoderDecoder - - -class ComponentSerializer: - """ - This is the base class for all EasyScience objects and deals with - the data conversion to other formats via the ``encode`` and - ``decode`` functions. - - Shortcuts for dictionary and data dictionary encoding is also - present. - """ - - _CORE = True - - def __deepcopy__(self, memo): - return self.from_dict(self.as_dict()) - - def encode( - self, - skip: Optional[List[str]] = None, - encoder: Optional[BaseEncoderDecoder] = None, - **kwargs, - ) -> Any: - """ - Use an encoder to covert an EasyScience object into another - format. Default is to a dictionary using ``DictSerializer``. - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - encoded object. By default, None. - encoder : Optional[BaseEncoderDecoder], default=None - The encoder to be used for encoding the data. By default, - None. - **kwargs : - Any additional key word arguments to be passed to the - encoder. - - Returns - ------- - Any - Encoded object containing all information to reform an - EasyScience object. - """ - if encoder is None: - encoder = DictSerializer - encoder_obj = encoder() - return encoder_obj.encode(self, skip=skip, **kwargs) - - @classmethod - def decode(cls, obj: Any, decoder: Optional[BaseEncoderDecoder] = None) -> Any: - """ - Re-create an EasyScience object from the output of an encoder. - The default decoder is ``DictSerializer``. - - Parameters - ---------- - obj : Any - Encoded EasyScience object. - decoder : Optional[BaseEncoderDecoder], default=None - Decoder to be used to reform the EasyScience object. By - default, None. - - Returns - ------- - Any - Reformed EasyScience object. - """ - - if decoder is None: - decoder = DictSerializer - return decoder.decode(obj) - - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Convert an EasyScience object into a full dictionary using - ``DictSerializer``. This is a shortcut for - ```obj.encode(encoder=DictSerializer)``` - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming - the dictionary. By default, None. - - Returns - ------- - Dict[str, Any] - Encoded object containing all information to reform an - EasyScience object. - """ - - return self.encode(skip=skip, encoder=DictSerializer) - - @classmethod - def from_dict(cls, obj_dict: Dict[str, Any]) -> Any: - """ - Re-create an EasyScience object from a full encoded dictionary. - - Parameters - ---------- - obj_dict : Dict[str, Any] - Dictionary containing the serialized contents (from - ``DictSerializer``) of an EasyScience object. - - Returns - ------- - Any - Reformed EasyScience object. - """ - - return cls.decode(obj_dict, decoder=DictSerializer) - - def encode_data( - self, - skip: Optional[List[str]] = None, - encoder: Optional[BaseEncoderDecoder] = None, - **kwargs, - ) -> Any: - """ - Returns just the data in an EasyScience object win the format - specified by an encoder. - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - dictionary. By default, None. - encoder : Optional[BaseEncoderDecoder], default=None - The encoder to be used for encoding the data. By default, - None. - **kwargs : - Any additional keywords to pass to the encoder when - encoding. - - Returns - ------- - Any - Encoded object containing just the data of an EasyScience - object. - """ - - if encoder is None: - encoder = DataDictSerializer - return self.encode(skip=skip, encoder=encoder, **kwargs) - - def as_data_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Returns a dictionary containing just the data of an EasyScience - object. - - Parameters - ---------- - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - dictionary. By default, None. - - Returns - ------- - Dict[str, Any] - Dictionary containing just the data of an EasyScience - object. - """ - - return self.encode(skip=skip, encoder=DataDictSerializer) - - def unsafe_hash(self) -> sha1: - """ - Returns an hash of the current object. - - This uses a generic but low performance method of converting the - object to a dictionary, flattening any nested keys, and then - performing a hash on the resulting object - """ - - def flatten(obj, seperator='.'): - # Flattens a dictionary - - flat_dict = {} - for key, value in obj.items(): - if isinstance(value, dict): - flat_dict.update({ - seperator.join([key, _key]): _value - for _key, _value in flatten(value).items() - }) - elif isinstance(value, list): - list_dict = { - '{}{}{}'.format(key, seperator, num): item - for num, item in enumerate(value) - } - flat_dict.update(flatten(list_dict)) - else: - flat_dict[key] = value - - return flat_dict - - ordered_keys = sorted(flatten(jsanitize(self.as_dict())).items(), key=lambda x: x[0]) - ordered_keys = [item for item in ordered_keys if '@' not in item[0]] - return sha1(json.dumps(OrderedDict(ordered_keys)).encode('utf-8')) # noqa: S324 diff --git a/src/easyscience/legacy/obj_base.py b/src/easyscience/legacy/obj_base.py deleted file mode 100644 index 4a70de25..00000000 --- a/src/easyscience/legacy/obj_base.py +++ /dev/null @@ -1,171 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Callable -from typing import Optional - -from easyscience import global_object - -from ..base_classes.based_base import BasedBase -from ..utils.classTools import addLoggedProp -from ..variable.descriptor_base import DescriptorBase - -if TYPE_CHECKING: - from ..io import SerializerComponent - - -class ObjBase(BasedBase): - """ - This is the base class for which all higher level classes are built - off of. - - .. deprecated:: - ``ObjBase`` is deprecated and will be removed in a future version. - Please migrate to ``ModelBase`` instead. - - NOTE: This object is serializable only if parameters are supplied - as: ``ObjBase(a=value, b=value)``. For ``Parameter`` or - ``Descriptor`` objects we can cheat with - ``ObjBase(*[Descriptor(...), Parameter(...), ...])``. - """ - - def __init__( - self, - name: str, - unique_name: Optional[str] = None, - *args: Optional[SerializerComponent], - **kwargs: Optional[SerializerComponent], - ): - """ - Set up the base class. - - Parameters - ---------- - name : str - Name of this object. - unique_name : Optional[str], default=None - Unique identifier for this object. By default, None. - *args : Optional[SerializerComponent] - Positional serializer components to attach to this object. - **kwargs : Optional[SerializerComponent] - Fields which this class should contain. - - Raises - ------ - AttributeError - If a keyword component name would overwrite an existing - class attribute. - """ - global_object.log.getLogger('legacy').warning( - 'ObjBase is deprecated and will be removed in a future version. ' - 'Please migrate to ModelBase.', - stacklevel=2, - ) - super(ObjBase, self).__init__(name=name, unique_name=unique_name) - # If Parameter or Descriptor is given as arguments... - for arg in args: - if issubclass(type(arg), (ObjBase, DescriptorBase)): - kwargs[getattr(arg, 'name')] = arg - # Set kwargs, also useful for serialization - known_keys = self.__dict__.keys() - self._kwargs = kwargs - for key in kwargs.keys(): - if key in known_keys: - raise AttributeError('Kwargs cannot overwrite class attributes in ObjBase.') - if issubclass(type(kwargs[key]), (BasedBase, DescriptorBase)) or 'CollectionBase' in [ - c.__name__ for c in type(kwargs[key]).__bases__ - ]: - self._global_object.map.add_edge(self, kwargs[key]) - self._global_object.map.reset_type(kwargs[key], 'created_internal') - addLoggedProp( - self, - key, - self.__getter(key), - self.__setter(key), - get_id=key, - my_self=self, - test_class=ObjBase, - ) - - def _add_component(self, key: str, component: SerializerComponent) -> None: - """Dynamically add a component to the class. This is an internal - method, though can be called remotely. The recommended - alternative is to use typing, i.e. - - .. code-block:: python - - class Foo(Bar): - def __init__(self, foo: Parameter, bar: Parameter): - '''Init function.''' - super(Foo, self).__init__(bar=bar) - self._add_component('foo', foo) - - :param key: Name of component to be added - :param component: Component to be added - :return: None - """ - self._kwargs[key] = component - self._global_object.map.add_edge(self, component) - self._global_object.map.reset_type(component, 'created_internal') - addLoggedProp( - self, - key, - self.__getter(key), - self.__setter(key), - get_id=key, - my_self=self, - test_class=ObjBase, - ) - - def __setattr__(self, key: str, value: SerializerComponent) -> None: - # Assume that the annotation is a ClassVar - old_obj = None - if ( - hasattr(self.__class__, '__annotations__') - and key in self.__class__.__annotations__ - and hasattr(self.__class__.__annotations__[key], '__args__') - and issubclass( - getattr(value, '__old_class__', value.__class__), - self.__class__.__annotations__[key].__args__, - ) - ): - if issubclass(type(getattr(self, key, None)), (BasedBase, DescriptorBase)): - old_obj = self.__getattribute__(key) - self._global_object.map.prune_vertex_from_edge(self, old_obj) - self._add_component(key, value) - else: - if hasattr(self, key) and issubclass(type(value), (BasedBase, DescriptorBase)): - old_obj = self.__getattribute__(key) - self._global_object.map.prune_vertex_from_edge(self, old_obj) - self._global_object.map.add_edge(self, value) - super(ObjBase, self).__setattr__(key, value) - # Update the interface bindings if something changed (BasedBase and Descriptor) - if old_obj is not None: - old_interface = getattr(self, 'interface', None) - if old_interface is not None: - self.generate_bindings() - - def __repr__(self) -> str: - return f'{self.__class__.__name__} `{getattr(self, "name")}`' - - @staticmethod - def __getter(key: str) -> Callable[[SerializerComponent], SerializerComponent]: - def getter(obj: SerializerComponent) -> SerializerComponent: - return obj._kwargs[key] - - return getter - - @staticmethod - def __setter(key: str) -> Callable[[SerializerComponent], None]: - def setter(obj: SerializerComponent, value: float) -> None: - if issubclass(obj._kwargs[key].__class__, (DescriptorBase)) and not issubclass( - value.__class__, (DescriptorBase) - ): - obj._kwargs[key].value = value - else: - obj._kwargs[key] = value - - return setter diff --git a/src/easyscience/legacy/xml.py b/src/easyscience/legacy/xml.py deleted file mode 100644 index da52ef65..00000000 --- a/src/easyscience/legacy/xml.py +++ /dev/null @@ -1,197 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import sys -import xml.etree.ElementTree as ET -from numbers import Number -from typing import TYPE_CHECKING -from typing import Any -from typing import List -from typing import Optional - -import numpy as np - -from easyscience import global_object - -from ..io.dict import DataDictSerializer -from ..io.dict import DictSerializer -from ..io.template import BaseEncoderDecoder - -if TYPE_CHECKING: - from ..io.component_serializer import ComponentSerializer - - -can_intent = (sys.version_info.major > 2) & (sys.version_info.minor > 8) - - -class XMLSerializer(BaseEncoderDecoder): - """ - This is a serializer that can encode and decode EasyScience objects - to a basic xml format. - """ - - def encode( - self, - obj: ComponentSerializer, - skip: Optional[List[str]] = None, - data_only: bool = False, - fast: bool = False, - use_header: bool = False, - **kwargs, - ) -> str: - """ - Convert an EasyScience object to an XML encoded string. - - Note that for speed the ``fast`` setting can be changed to - ``True``. An XML document with initial block *data* is returned. - - Parameters - ---------- - obj : ComponentSerializer - Object to be encoded. - skip : Optional[List[str]], default=None - List of field names as strings to skip when forming the - encoded object. By default, None. - data_only : bool, default=False - Should only the object's data be encoded. By default, False. - fast : bool, default=False - Should the returned string be pretty? This can be turned off - for speed. By default, False. - use_header : bool, default=False - Should a header of ``'?xml version="1.0" - encoding="UTF-8"?'`` be included? By default, False. - **kwargs : - Any additional key-words to pass to the Dictionary - Serializer. - - Returns - ------- - str - String containing the XML encoded object. - """ - - if skip is None: - skip = [] - encoder = DictSerializer - if data_only: - encoder = DataDictSerializer - if isinstance(obj, dict): - obj_dict = obj - else: - obj_dict = encoder().encode(obj, skip=skip, full_encode=True, **kwargs) - block = ET.Element('data') - self._check_class(block, None, obj_dict, skip=skip) - header = '' - if use_header: - header = '?xml version="1.0" encoding="UTF-8"?' - if not fast and can_intent: - ET.indent(block) - if use_header: - header += '\n' - return header + ET.tostring(block, encoding='unicode') - - @classmethod - def decode(cls, data: str) -> ComponentSerializer: - """ - Decode an EasyScience object which has been encoded in XML - format. - - Parameters - ---------- - data : str - String containing XML encoded data. - - Returns - ------- - ComponentSerializer - Reformed EasyScience object. - """ - - data_xml = ET.XML(data) - out_dict = {} - for element in data_xml: - XMLSerializer._element_to_dict(element, out_dict) - return DictSerializer.decode(out_dict) - - @staticmethod - def _element_to_dict(element, out_dict): - """Convert an XML element to a dictionary recursively.""" - - label = element.tag - if label[0] == '_': - label = '@' + label[2:] - if len(element) == 0: - out_dict[label] = XMLSerializer.string_to_variable(element.text) - else: - this_dict = {} - for el in element: - XMLSerializer._element_to_dict(el, this_dict) - if label in out_dict.keys(): - # This object is a list. Create a list. - old_value = out_dict[label] - if not isinstance(old_value, list): - old_value = [old_value] - old_value.append(this_dict) - this_dict = old_value - out_dict[label] = this_dict - - @staticmethod - def string_to_variable(in_string: str): - """Convert an XML encoded string to JSON form.""" - if in_string is None: - return in_string - in_string = in_string.strip() - if "'" in in_string: - in_string = in_string.replace("'", '') - if '"' in in_string: - in_string = in_string.replace('"', '') - try: - value = float(in_string) - except ValueError: - if in_string == 'True': - value = True - elif in_string == 'False': - value = False - elif in_string == 'None': - value = None - else: - value = in_string - return value - - def _check_class(self, element, key: str, value: Any, skip: Optional[List[str]] = None): - """ - Add a value to an element or create a new element based on input - type. - """ - T_ = type(value) - if isinstance(value, dict): - for k, v in value.items(): - if k in skip: - continue - kk = k - if k[0] == '@': - kk = '__' + kk[1:] - if not isinstance(v, list): - s = ET.SubElement(element, kk) - self._check_class(s, kk, v, skip=skip) - else: - self._check_class(element, kk, v, skip=skip) - elif isinstance(value, bool): - element.text = str(value) - elif isinstance(value, str): - element.text = value - elif isinstance(value, list): - for i in value: - s = ET.SubElement(element, key) - self._check_class(s, None, i, skip=skip) - elif value is None: - element.text = 'None' - elif issubclass(T_, Number): - element.text = str(value) - elif issubclass(T_, np.ndarray): - element.text = str(value.tolist()) - else: - global_object.log.getLogger('legacy.xml').error('Cannot encode %s to XML', T_) - raise NotImplementedError diff --git a/src/easyscience/models/__init__.py b/src/easyscience/models/__init__.py deleted file mode 100644 index 3fc7f778..00000000 --- a/src/easyscience/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: 2024 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from .polynomial import Polynomial # noqa: F401 diff --git a/src/easyscience/models/polynomial.py b/src/easyscience/models/polynomial.py deleted file mode 100644 index 49b4757e..00000000 --- a/src/easyscience/models/polynomial.py +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-FileCopyrightText: 2024 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from typing import ClassVar -from typing import Iterable -from typing import Optional -from typing import Union - -import numpy as np - -from ..base_classes import CollectionBase -from ..base_classes import ObjBase -from ..variable import Parameter - - -class Polynomial(ObjBase): - """ - A polynomial model. - - Attributes - ---------- - coefficients : ClassVar[CollectionBase] - Coefficients of the polynomial. - - Parameters - ---------- - name : str, default='polynomial' - The name of the model. - coefficients : Optional[Union[Iterable[Union[float, Parameter]], CollectionBase]], default=None - Coefficients used to populate ``self.coefficients``. - - Raises - ------ - TypeError - If ``coefficients`` is neither a ``CollectionBase`` nor an - iterable of floats or ``Parameter`` instances. - """ - - coefficients: ClassVar[CollectionBase] - - def __init__( - self, - name: str = 'polynomial', - coefficients: Optional[Union[Iterable[Union[float, Parameter]], CollectionBase]] = None, - ): - super(Polynomial, self).__init__(name, coefficients=CollectionBase('coefficients')) - if coefficients is not None: - if issubclass(type(coefficients), CollectionBase): - self.coefficients = coefficients - elif isinstance(coefficients, Iterable): - for index, item in enumerate(coefficients): - if issubclass(type(item), Parameter): - self.coefficients.append(item) - elif isinstance(item, float): - self.coefficients.append(Parameter(name='c{}'.format(index), value=item)) - else: - raise TypeError('Coefficients must be floats or Parameters') - else: - raise TypeError('coefficients must be a list or a CollectionBase') - - def __call__(self, x: np.ndarray, *args, **kwargs) -> np.ndarray: - return np.polyval([c.value for c in self.coefficients], x) - - def __repr__(self): - s = [] - if len(self.coefficients) >= 1: - s += [f'{self.coefficients[0].value}'] - if len(self.coefficients) >= 2: - s += [f'{self.coefficients[1].value}x'] - if len(self.coefficients) >= 3: - s += [ - f'{c.value}x^{i + 2}' - for i, c in enumerate(self.coefficients[2:]) - if c.value != 0 - ] - s.reverse() - s = ' + '.join(s) - return 'Polynomial({}, {})'.format(self.name, s) diff --git a/src/easyscience/utils/classTools.py b/src/easyscience/utils/classTools.py deleted file mode 100644 index 151f36c1..00000000 --- a/src/easyscience/utils/classTools.py +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import List -from typing import Tuple - -from easyscience import global_object -from easyscience.global_object.hugger.property import LoggedProperty - -if TYPE_CHECKING: - from ..base_classes import BasedBase - from ..io import SerializerComponent - - -def addLoggedProp(inst: SerializerComponent, name: str, *args, **kwargs) -> None: - cls = type(inst) - annotations = getattr(cls, '__annotations__', False) - if not hasattr(cls, '__perinstance'): - cls = type(cls.__name__, (cls,), {'__module__': inst.__module__}) - cls.__perinstance = True - if annotations: - cls.__annotations__ = annotations - inst.__old_class__ = inst.__class__ - inst.__class__ = cls - setattr(cls, name, LoggedProperty(*args, **kwargs)) - - -def addProp(inst: SerializerComponent, name: str, *args, **kwargs) -> None: - cls = type(inst) - annotations = getattr(cls, '__annotations__', False) - if not hasattr(cls, '__perinstance'): - cls = type(cls.__name__, (cls,), {'__module__': __name__}) - cls.__perinstance = True - if annotations: - cls.__annotations__ = annotations - inst.__old_class__ = inst.__class__ - inst.__class__ = cls - - setattr(cls, name, property(*args, **kwargs)) - - -def removeProp(inst: SerializerComponent, name: str) -> None: - cls = type(inst) - if not hasattr(cls, '__perinstance'): - cls = type(cls.__name__, (cls,), {'__module__': __name__}) - cls.__perinstance = True - inst.__old_class__ = inst.__class__ - inst.__class__ = cls - delattr(cls, name) - - -def generatePath(model_obj: BasedBase, skip_first: bool = False) -> Tuple[List[int], List[str]]: - pars = model_obj.get_parameters() - start_idx = 0 + int(skip_first) - unique_names = [] - names = [] - for par in pars: - route = global_object.map.reverse_route(par.unique_name, model_obj.unique_name) - objs = [getattr(global_object.map.get_item_by_key(r), 'name') for r in route] - objs.reverse() - names.append('.'.join(objs[start_idx:])) - unique_names.append(par.unique_name) - return unique_names, names diff --git a/src/easyscience/variable/__init__.py b/src/easyscience/variable/__init__.py index 121d5cf9..5b7749eb 100644 --- a/src/easyscience/variable/__init__.py +++ b/src/easyscience/variable/__init__.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from .descriptor_any_type import DescriptorAnyType from .descriptor_array import DescriptorArray from .descriptor_base import DescriptorBase from .descriptor_bool import DescriptorBool @@ -10,11 +9,10 @@ from .parameter import Parameter __all__ = [ - DescriptorAnyType, - DescriptorArray, - DescriptorBase, - DescriptorBool, - DescriptorNumber, - DescriptorStr, - Parameter, + 'DescriptorArray', + 'DescriptorBase', + 'DescriptorBool', + 'DescriptorNumber', + 'DescriptorStr', + 'Parameter', ] diff --git a/src/easyscience/variable/descriptor_any_type.py b/src/easyscience/variable/descriptor_any_type.py deleted file mode 100644 index 157117d8..00000000 --- a/src/easyscience/variable/descriptor_any_type.py +++ /dev/null @@ -1,105 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import numbers -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Union - -import numpy as np - -from easyscience.global_object.undo_redo import property_stack - -from .descriptor_base import DescriptorBase - - -class DescriptorAnyType(DescriptorBase): - """ - A ``Descriptor`` for any type that does not fit the other - Descriptors. - - Should be avoided when possible. It was created to hold the symmetry - operations used in the SpaceGroup class of EasyCrystallography. - """ - - def __init__( - self, - name: str, - value: Any, - unique_name: Optional[str] = None, - description: Optional[str] = None, - url: Optional[str] = None, - display_name: Optional[str] = None, - parent: Optional[Any] = None, - ): - """ - Constructor for the DescriptorAnyType class. - - param name: Name of the descriptor param value: Value of the - descriptor param description: Description of the descriptor - param url: URL of the descriptor param display_name: Display - name of the descriptor param parent: Parent of the descriptor .. - note:: Undo/Redo functionality is implemented for the attributes - ``variance``, ``error``, ``unit`` and ``value``. - """ - - self._value = value - - super().__init__( - name=name, - unique_name=unique_name, - description=description, - url=url, - display_name=display_name, - parent=parent, - ) - - @property - def value(self) -> numbers.Number: - """ - Get the value. - - Returns - ------- - numbers.Number - Value of self. - """ - return self._value - - @value.setter - @property_stack - def value(self, value: Union[list, np.ndarray]) -> None: - """ - Set the value of self. - - Parameters - ---------- - value : Union[list, np.ndarray] - New value for the DescriptorAnyType. - """ - self._value = value - - def __copy__(self) -> DescriptorAnyType: - return super().__copy__() - - def __repr__(self) -> str: - """ - Return a string representation of the DescriptorAnyType, showing - its name and value. - """ - - if hasattr(self._value, '__repr__'): - value_repr = repr(self._value) - else: - value_repr = type(self._value) - - return f"<{self.__class__.__name__} '{self._name}': {value_repr}>" - - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - raw_dict = super().as_dict(skip=skip) - raw_dict['value'] = self._value - return raw_dict diff --git a/src/easyscience/variable/descriptor_array.py b/src/easyscience/variable/descriptor_array.py index 35f1c022..c8affc75 100644 --- a/src/easyscience/variable/descriptor_array.py +++ b/src/easyscience/variable/descriptor_array.py @@ -18,9 +18,6 @@ from scipp import UnitError from scipp import Variable -from easyscience.global_object.undo_redo import PropertyStack -from easyscience.global_object.undo_redo import property_stack - from .descriptor_base import DescriptorBase from .descriptor_number import DescriptorNumber @@ -34,29 +31,56 @@ class DescriptorArray(DescriptorBase): def __init__( self, - name: str, value: Union[list, np.ndarray], + *, unit: Optional[Union[str, sc.Unit]] = '', variance: Optional[Union[list, np.ndarray]] = None, unique_name: Optional[str] = None, description: Optional[str] = None, url: Optional[str] = None, display_name: Optional[str] = None, - parent: Optional[Any] = None, dimensions: Optional[list] = None, ): """ Constructor for the DescriptorArray class. - param name: Name of the descriptor param value: List containing - the values of the descriptor param unit: Unit of the descriptor - param variance: Variances of the descriptor param description: - Description of the descriptor param url: URL of the descriptor - param display_name: Display name of the descriptor param parent: - Parent of the descriptor param dimensions: List of dimensions to - pass to scipp. Will be autogenerated if not supplied. .. note:: - Undo/Redo functionality is implemented for the attributes - ``variance``, ``error``, ``unit`` and ``value``. + All arguments after ``value`` are keyword-only. + + Parameters + ---------- + value : Union[list, np.ndarray] + Values of this object. + unit : Optional[Union[str, sc.Unit]], default='' + This object can have a physical unit associated with it. By + default, ''. + variance : Optional[Union[list, np.ndarray]], default=None + The variances of the values, with the same shape as + ``value``. By default, None. + unique_name : Optional[str], default=None + Unique identifier for this object. By default, None. + description : Optional[str], default=None + A brief summary of what this object is. By default, None. + url : Optional[str], default=None + Lookup url for documentation/information. By default, None. + display_name : Optional[str], default=None + A pretty name for the object. Falls back to ``unique_name`` + when not given. By default, None. + dimensions : Optional[list], default=None + List of dimensions to pass to scipp. Autogenerated if not + supplied. By default, None. + + Raises + ------ + TypeError + If ``value`` or ``variance`` is not a list or NumPy array, or + if ``unit`` is not a string or scipp unit. + UnitError + If ``unit`` is not a valid scipp unit. + ValueError + If ``variance`` does not match the shape of ``value``, if it + contains negative entries, or if ``dimensions`` does not + match the shape of ``value``. + """ if not isinstance(value, (list, np.ndarray)): @@ -99,12 +123,10 @@ def __init__( # TODO: handle 1xn and nx1 arrays super().__init__( - name=name, unique_name=unique_name, description=description, url=url, display_name=display_name, - parent=parent, ) # Call convert_unit during initialization to ensure that the unit has no numbers in it, and to ensure unit consistency. @@ -112,14 +134,12 @@ def __init__( self.convert_unit(self._base_unit()) @classmethod - def from_scipp(cls, name: str, full_value: Variable, **kwargs: Any) -> DescriptorArray: + def from_scipp(cls, full_value: Variable, **kwargs: Any) -> DescriptorArray: """ Create a DescriptorArray from a scipp array. Parameters ---------- - name : str - Name of the descriptor. full_value : Variable Value of the descriptor as a scipp variable. **kwargs : Any @@ -138,7 +158,6 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs: Any) -> Descripto if not isinstance(full_value, Variable): raise TypeError(f'{full_value=} must be a scipp array') return cls( - name=name, value=full_value.values, unit=full_value.unit, variance=full_value.variances, @@ -181,7 +200,6 @@ def value(self) -> numbers.Number: return self._array.values @value.setter - @property_stack def value(self, value: Union[list, np.ndarray]) -> None: """ Set the value of self. @@ -295,7 +313,6 @@ def variance(self) -> np.ndarray: return self._array.variances @variance.setter - @property_stack def variance(self, variance: Union[list, np.ndarray]) -> None: """ Set the variance of self. @@ -331,8 +348,9 @@ def variance(self, variance: Union[list, np.ndarray]) -> None: if not np.all(variance >= 0): raise ValueError(f'{variance=} must only contain non-negative values.') - # Values must be floats for optimization - self._array.variances = variance.astype('float') + # Values must be floats for optimization + variance = variance.astype('float') + self._array.variances = variance @property def error(self) -> Optional[np.ndarray]: @@ -351,7 +369,6 @@ def error(self) -> Optional[np.ndarray]: return np.sqrt(self._array.variances) @error.setter - @property_stack def error(self, error: Union[list, np.ndarray]) -> None: """ Set the standard deviation for the parameter, which updates the @@ -407,26 +424,12 @@ def convert_unit(self, unit_str: str) -> None: raise TypeError(f'{unit_str=} must be a string representing a valid scipp unit') new_unit = sc.Unit(unit_str) - # Save the current state for undo/redo - old_array = self._array - # Perform the unit conversion try: new_array = self._array.to(unit=new_unit) except Exception as e: raise UnitError(f'Failed to convert unit: {e}') from e - # Define the setter function for the undo stack - def set_array(obj, scalar): - obj._array = scalar - - # Push to undo stack - self._global_object.stack.push( - PropertyStack( - self, set_array, old_array, new_array, text=f'Convert unit to {unit_str}' - ) - ) - # Update the array self._array = new_array @@ -437,12 +440,12 @@ def __copy__(self) -> DescriptorArray: def __repr__(self) -> str: """ Return a string representation of the DescriptorArray, showing - its name, value, variance, and unit. + its display name, value, variance, and unit. Large arrays are summarized for brevity. """ - # Base string with name - string = f"<{self.__class__.__name__} '{self._name}': " + # Base string with display name + string = f"<{self.__class__.__name__} '{self.display_name}': " # Summarize array values values_summary = np.array2string( @@ -472,14 +475,14 @@ def __repr__(self) -> str: string = string.replace('\n', ',') return string - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: """ Dict representation of the current DescriptorArray. The dict contains the value, unit and variances, in addition to the properties of DescriptorBase. """ - raw_dict = super().as_dict(skip=skip) + raw_dict = super().to_dict(skip=skip) raw_dict['value'] = self._array.values raw_dict['unit'] = str(self._array.unit) raw_dict['variance'] = self._array.variances @@ -591,9 +594,7 @@ def _apply_operation( else: return NotImplemented - descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) - descriptor_array.name = descriptor_array.unique_name - return descriptor_array + return DescriptorArray.from_scipp(full_value=new_full_value) def _rapply_operation( self, @@ -887,9 +888,7 @@ def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorA raise message from None if np.any(np.isnan(new_value.values)): raise ValueError('The result of the exponentiation is not a number') - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorArray.from_scipp(full_value=new_value) def __rpow__(self, other: numbers.Number): """ @@ -903,9 +902,7 @@ def __rpow__(self, other: numbers.Number): def __neg__(self) -> DescriptorArray: """Negate all values in the DescriptorArray.""" new_value = -self.full_value - descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_array.name = descriptor_array.unique_name - return descriptor_array + return DescriptorArray.from_scipp(full_value=new_value) def __abs__(self) -> DescriptorArray: """ @@ -916,9 +913,7 @@ def __abs__(self) -> DescriptorArray: DescriptorArray. """ new_value = abs(self.full_value) - descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_array.name = descriptor_array.unique_name - return descriptor_array + return DescriptorArray.from_scipp(full_value=new_value) def __getitem__(self, a) -> DescriptorArray: """ @@ -926,11 +921,7 @@ def __getitem__(self, a) -> DescriptorArray: Defer slicing to scipp. """ - descriptor = DescriptorArray.from_scipp( - name=self.name, full_value=self.full_value.__getitem__(a) - ) - descriptor.name = descriptor.unique_name - return descriptor + return DescriptorArray.from_scipp(full_value=self.full_value.__getitem__(a)) def __delitem__(self, a): """ @@ -1027,9 +1018,7 @@ def trace( ) constructor = DescriptorArray.from_scipp - descriptor = constructor(name=self.name, full_value=trace) - descriptor.name = descriptor.unique_name - return descriptor + return constructor(full_value=trace) def sum( self, dim: Optional[Union[str, list]] = None @@ -1057,9 +1046,7 @@ def sum( else: constructor = DescriptorArray.from_scipp - descriptor = constructor(name=self.name, full_value=new_full_value) - descriptor.name = descriptor.unique_name - return descriptor + return constructor(full_value=new_full_value) # This is to be implemented at a later time # def __matmul__(self, other: [DescriptorArray, list]) -> DescriptorArray: diff --git a/src/easyscience/variable/descriptor_base.py b/src/easyscience/variable/descriptor_base.py index ba7198bc..ab411763 100644 --- a/src/easyscience/variable/descriptor_base.py +++ b/src/easyscience/variable/descriptor_base.py @@ -5,22 +5,21 @@ import abc from typing import Any +from typing import Dict +from typing import List from typing import Optional from easyscience import global_object -from easyscience.global_object.undo_redo import property_stack -from easyscience.io import SerializerComponent +from easyscience.base_classes.easy_base import EasyBase -class DescriptorBase(SerializerComponent, metaclass=abc.ABCMeta): +class DescriptorBase(EasyBase, metaclass=abc.ABCMeta): """ This is the base of all variable descriptions for models. It contains all information to describe a single unique property of - an object. This description includes a name and value as well as - optionally a unit, description and url (for reference material). - Also implemented is a callback so that the value can be read/set - from a linked library object. + an object. This description includes a value as well as optionally a + unit, description and url (for reference material). A ``Descriptor`` is typically something which describes part of a model and is non-fittable and generally changes the state of an @@ -28,24 +27,21 @@ class DescriptorBase(SerializerComponent, metaclass=abc.ABCMeta): """ _global_object = global_object - # Used by serializer - _REDIRECT = {'parent': None} def __init__( self, - name: str, + *, unique_name: Optional[str] = None, description: Optional[str] = None, url: Optional[str] = None, display_name: Optional[str] = None, - parent: Optional[Any] = None, ): """ This is the base of variables for models. It contains all information to describe a single unique property - of an object. This description includes a name, description and - url (for reference material). + of an object. This description includes a description and url + (for reference material). A ``Descriptor`` is typically something which describes part of a model and is non-fittable and generally changes the state of @@ -53,8 +49,6 @@ def __init__( Parameters ---------- - name : str - Name of this object. unique_name : Optional[str], default=None Unique identifier for this object. By default, None. description : Optional[str], default=None @@ -62,29 +56,16 @@ def __init__( url : Optional[str], default=None Lookup url for documentation/information. By default, None. display_name : Optional[str], default=None - A pretty name for the object. By default, None. - parent : Optional[Any], default=None - The object which this descriptor is attached to. By default, - None. + A pretty name for the object. Falls back to ``unique_name`` + when not given. By default, None. Raises ------ TypeError - If ``name`` is not a string or if any optional string field - has an invalid type. + If any optional string field has an invalid type. """ - if unique_name is None: - unique_name = global_object.generate_unique_name(self.__class__.__name__) - self._unique_name = unique_name - - if not isinstance(name, str): - raise TypeError('Name must be a string') - self._name: str = name - - if display_name is not None and not isinstance(display_name, str): - raise TypeError('Display name must be a string or None') - self._display_name: str = display_name + super().__init__(unique_name=unique_name, display_name=display_name) if description is not None and not isinstance(description, str): raise TypeError('Description must be a string or None') @@ -98,80 +79,6 @@ def __init__( url = '' self._url: str = url - # Let the collective know we've been assimilated - self._parent = parent - global_object.map.add_vertex(self, obj_type='created') - # Make the connection between self and parent - if parent is not None: - global_object.map.add_edge(parent, self) - - @property - def name(self) -> str: - """ - Get the name of the object. - - Returns - ------- - str - Name of the object. - """ - return self._name - - @name.setter - @property_stack - def name(self, new_name: str) -> None: - """ - Set the name. - - Parameters - ---------- - new_name : str - Name of the object. - - Raises - ------ - TypeError - If ``new_name`` is not a string. - """ - if not isinstance(new_name, str): - raise TypeError('Name must be a string') - self._name = new_name - - @property - def display_name(self) -> str: - """ - Get a pretty display name. - - Returns - ------- - str - The pretty display name. - """ - display_name = self._display_name - if display_name is None: - display_name = self._name - return display_name - - @display_name.setter - @property_stack - def display_name(self, name: Optional[str]) -> None: - """ - Set the pretty display name. - - Parameters - ---------- - name : Optional[str] - Pretty display name of the object. - - Raises - ------ - TypeError - If ``name`` is neither a string nor ``None``. - """ - if name is not None and not isinstance(name, str): - raise TypeError('Display name must be a string or None') - self._display_name = name - @property def description(self) -> str: """ @@ -234,40 +141,6 @@ def url(self, url: Optional[str]) -> None: raise TypeError('url must be a string') self._url = url - @property - def unique_name(self) -> str: - """ - Get the unique name of this object. - - Returns - ------- - str - Unique name of this object. - """ - return self._unique_name - - @unique_name.setter - def unique_name(self, new_unique_name: str): - """ - Set a new unique name for the object. - - The old name is still kept in the map. - - Parameters - ---------- - new_unique_name : str - New unique name for the object. - - Raises - ------ - TypeError - If ``new_unique_name`` is not a string. - """ - if not isinstance(new_unique_name, str): - raise TypeError('Unique name has to be a string.') - self._unique_name = new_unique_name - global_object.map.add_vertex(self) - @property @abc.abstractmethod def value(self) -> Any: @@ -281,9 +154,3 @@ def value(self, value: Any) -> None: @abc.abstractmethod def __repr__(self) -> str: """Return printable representation of the object.""" - - def __copy__(self) -> DescriptorBase: - """Return a copy of the object.""" - temp = self.as_dict(skip=['unique_name']) - new_obj = self.__class__.from_dict(temp) - return new_obj diff --git a/src/easyscience/variable/descriptor_bool.py b/src/easyscience/variable/descriptor_bool.py index 05a09d9d..cad85a50 100644 --- a/src/easyscience/variable/descriptor_bool.py +++ b/src/easyscience/variable/descriptor_bool.py @@ -3,11 +3,8 @@ from __future__ import annotations -from typing import Any from typing import Optional -from easyscience.global_object.undo_redo import property_stack - from .descriptor_base import DescriptorBase @@ -16,26 +13,45 @@ class DescriptorBool(DescriptorBase): def __init__( self, - name: str, value: bool, + *, unique_name: Optional[str] = None, description: Optional[str] = None, url: Optional[str] = None, display_name: Optional[str] = None, - parent: Optional[Any] = None, ): + """ + Constructor for the DescriptorBool class. + + All arguments after ``value`` are keyword-only. + + Parameters + ---------- + value : bool + Value of this object. + unique_name : Optional[str], default=None + Unique identifier for this object. By default, None. + description : Optional[str], default=None + A brief summary of what this object is. By default, None. + url : Optional[str], default=None + Lookup url for documentation/information. By default, None. + display_name : Optional[str], default=None + A pretty name for the object. Falls back to ``unique_name`` + when not given. By default, None. + + Raises + ------ + ValueError + If ``value`` is not a boolean. + """ if not isinstance(value, bool): raise ValueError(f'{value=} must be type bool') super().__init__( - name=name, unique_name=unique_name, description=description, url=url, display_name=display_name, - parent=parent, ) - if not isinstance(value, bool): - raise TypeError(f'{value=} must be type bool') self._bool_value = value @property @@ -51,7 +67,6 @@ def value(self) -> bool: return self._bool_value @value.setter - @property_stack def value(self, value: bool) -> None: """ Set the value of self. @@ -78,7 +93,7 @@ def value(self, value: bool) -> None: def __repr__(self) -> str: """Return printable representation.""" class_name = self.__class__.__name__ - obj_name = self._name + obj_name = self.display_name obj_value = self._bool_value return f"<{class_name} '{obj_name}': {obj_value}>" diff --git a/src/easyscience/variable/descriptor_number.py b/src/easyscience/variable/descriptor_number.py index 5bcbcb0d..41fb78d3 100644 --- a/src/easyscience/variable/descriptor_number.py +++ b/src/easyscience/variable/descriptor_number.py @@ -17,9 +17,6 @@ from scipp import UnitError from scipp import Variable -from easyscience.global_object.undo_redo import PropertyStack -from easyscience.global_object.undo_redo import property_stack - from .descriptor_base import DescriptorBase @@ -57,33 +54,64 @@ class DescriptorNumber(DescriptorBase): def __init__( self, - name: str, value: numbers.Number, + *, unit: Optional[Union[str, sc.Unit]] = '', variance: Optional[numbers.Number] = None, unique_name: Optional[str] = None, description: Optional[str] = None, url: Optional[str] = None, display_name: Optional[str] = None, - parent: Optional[Any] = None, **kwargs: Any, # Additional keyword arguments (used for (de)serialization) ): """ Constructor for the DescriptorNumber class. - param name: Name of the descriptor param value: Value of the - descriptor param unit: Unit of the descriptor param variance: - Variance of the descriptor param description: Description of the - descriptor param url: URL of the descriptor param display_name: - Display name of the descriptor param parent: Parent of the - descriptor .. note:: Undo/Redo functionality is implemented for - the attributes ``variance``, ``error``, ``unit`` and ``value``. + All arguments after ``value`` are keyword-only. + + Parameters + ---------- + value : numbers.Number + Value of this object. + unit : Optional[Union[str, sc.Unit]], default='' + This object can have a physical unit associated with it. By + default, ''. + variance : Optional[numbers.Number], default=None + The variance of the value. By default, None. + unique_name : Optional[str], default=None + Unique identifier for this object. By default, None. + description : Optional[str], default=None + A brief summary of what this object is. By default, None. + url : Optional[str], default=None + Lookup url for documentation/information. By default, None. + display_name : Optional[str], default=None + A pretty name for the object. Falls back to ``unique_name`` + when not given. By default, None. + **kwargs : Any + Additional keyword arguments used during (de)serialization. + + Raises + ------ + TypeError + If ``value`` or ``variance`` is not a number, if ``unit`` is + not a string or scipp unit, or if an unexpected keyword + argument is given. + UnitError + If ``unit`` is not a valid scipp unit. + ValueError + If ``variance`` is negative. + """ self._observers: List[DescriptorNumber] = [] # Extract serializer_id if provided during deserialization if '__serializer_id' in kwargs: self.__serializer_id = kwargs.pop('__serializer_id') + if kwargs: + raise TypeError( + f'{self.__class__.__name__}.__init__() got unexpected keyword ' + f'argument(s): {", ".join(sorted(kwargs))}.' + ) if not isinstance(value, numbers.Number) or isinstance(value, bool): raise TypeError(f'{value=} must be a number') @@ -102,12 +130,10 @@ def __init__( except Exception as message: raise UnitError(message) super().__init__( - name=name, unique_name=unique_name, description=description, url=url, display_name=display_name, - parent=parent, ) # Call convert_unit during initialization to ensure that the unit has no numbers in it, and to ensure unit consistency. @@ -115,14 +141,12 @@ def __init__( self._convert_unit(self._base_unit()) @classmethod - def from_scipp(cls, name: str, full_value: Variable, **kwargs: Any) -> DescriptorNumber: + def from_scipp(cls, full_value: Variable, **kwargs: Any) -> DescriptorNumber: """ Create a DescriptorNumber from a scipp constant. Parameters ---------- - name : str - Name of the descriptor. full_value : Variable Value of the descriptor as a scipp scalar. **kwargs : Any @@ -143,7 +167,6 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs: Any) -> Descripto if len(full_value.dims) != 0: raise TypeError(f'{full_value=} must be a scipp scalar') return cls( - name=name, value=full_value.value, unit=full_value.unit, variance=full_value.variance, @@ -231,7 +254,6 @@ def value(self) -> numbers.Number: @value.setter @notify_observers - @property_stack def value(self, value: numbers.Number) -> None: """ Set the value of self. @@ -288,7 +310,6 @@ def variance(self) -> float: @variance.setter @notify_observers - @property_stack def variance(self, variance_float: float) -> None: """ Set the variance. @@ -329,7 +350,6 @@ def error(self) -> float: @error.setter @notify_observers - @property_stack def error(self, value: float) -> None: """ Set the standard deviation for the parameter. @@ -378,26 +398,12 @@ def _convert_unit(self, unit_str: str) -> None: raise TypeError(f'{unit_str=} must be a string representing a valid scipp unit') new_unit = sc.Unit(unit_str) - # Save the current state for undo/redo - old_scalar = self._scalar - # Perform the unit conversion try: new_scalar = self._scalar.to(unit=new_unit) except Exception as e: raise UnitError(f'Failed to convert unit: {e}') from e - # Define the setter function for the undo stack - def set_scalar(obj, scalar): - obj._scalar = scalar - - # Push to undo stack - self._global_object.stack.push( - PropertyStack( - self, set_scalar, old_scalar, new_scalar, text=f'Convert unit to {unit_str}' - ) - ) - # Update the scalar self._scalar = new_scalar @@ -422,7 +428,7 @@ def __repr__(self) -> str: """Return printable representation.""" string = '<' string += self.__class__.__name__ + ' ' - string += f"'{self._name}': " + string += f"'{self.display_name}': " if np.abs(self._scalar.value) > 1e4 or ( np.abs(self._scalar.value) < 1e-4 and self._scalar.value != 0 ): @@ -444,8 +450,8 @@ def __repr__(self) -> str: return string # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - raw_dict = super().as_dict(skip=skip) + def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + raw_dict = super().to_dict(skip=skip) raw_dict['value'] = self._scalar.value raw_dict['unit'] = str(self._scalar.unit) raw_dict['variance'] = self._scalar.variance @@ -470,9 +476,7 @@ def __add__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN other._convert_unit(original_unit) else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __radd__(self, other: numbers.Number) -> DescriptorNumber: if isinstance(other, numbers.Number): @@ -481,9 +485,7 @@ def __radd__(self, other: numbers.Number) -> DescriptorNumber: new_value = other + self.full_value else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __sub__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: if isinstance(other, numbers.Number): @@ -502,9 +504,7 @@ def __sub__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN other._convert_unit(original_unit) else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __rsub__(self, other: numbers.Number) -> DescriptorNumber: if isinstance(other, numbers.Number): @@ -513,9 +513,7 @@ def __rsub__(self, other: numbers.Number) -> DescriptorNumber: new_value = other - self.full_value else: return NotImplemented - descriptor = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor.name = descriptor.unique_name - return descriptor + return DescriptorNumber.from_scipp(full_value=new_value) def __mul__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: if isinstance(other, numbers.Number): @@ -524,9 +522,8 @@ def __mul__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN new_value = self.full_value * other.full_value else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number = DescriptorNumber.from_scipp(full_value=new_value) descriptor_number._convert_unit(descriptor_number._base_unit()) - descriptor_number.name = descriptor_number.unique_name return descriptor_number def __rmul__(self, other: numbers.Number) -> DescriptorNumber: @@ -534,9 +531,7 @@ def __rmul__(self, other: numbers.Number) -> DescriptorNumber: new_value = other * self.full_value else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __truediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: if isinstance(other, numbers.Number): @@ -549,9 +544,8 @@ def __truediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Descrip new_value = self.full_value / other.full_value else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number = DescriptorNumber.from_scipp(full_value=new_value) descriptor_number._convert_unit(descriptor_number._base_unit()) - descriptor_number.name = descriptor_number.unique_name return descriptor_number def __rtruediv__(self, other: numbers.Number) -> DescriptorNumber: @@ -561,9 +555,7 @@ def __rtruediv__(self, other: numbers.Number) -> DescriptorNumber: new_value = other / self.full_value else: return NotImplemented - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: if isinstance(other, numbers.Number): @@ -582,9 +574,7 @@ def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN raise message from None if np.isnan(new_value.value): raise ValueError('The result of the exponentiation is not a number') - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __rpow__(self, other: numbers.Number) -> numbers.Number: if isinstance(other, numbers.Number): @@ -599,15 +589,11 @@ def __rpow__(self, other: numbers.Number) -> numbers.Number: def __neg__(self) -> DescriptorNumber: new_value = -self.full_value - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def __abs__(self) -> DescriptorNumber: new_value = abs(self.full_value) - descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_value) def _base_unit(self) -> str: """ diff --git a/src/easyscience/variable/descriptor_str.py b/src/easyscience/variable/descriptor_str.py index 5e74e786..13c23258 100644 --- a/src/easyscience/variable/descriptor_str.py +++ b/src/easyscience/variable/descriptor_str.py @@ -3,11 +3,8 @@ from __future__ import annotations -from typing import Any from typing import Optional -from easyscience.global_object.undo_redo import property_stack - from .descriptor_base import DescriptorBase @@ -16,21 +13,42 @@ class DescriptorStr(DescriptorBase): def __init__( self, - name: str, value: str, + *, unique_name: Optional[str] = None, description: Optional[str] = None, url: Optional[str] = None, display_name: Optional[str] = None, - parent: Optional[Any] = None, ): + """ + Constructor for the DescriptorStr class. + + All arguments after ``value`` are keyword-only. + + Parameters + ---------- + value : str + Value of this object. + unique_name : Optional[str], default=None + Unique identifier for this object. By default, None. + description : Optional[str], default=None + A brief summary of what this object is. By default, None. + url : Optional[str], default=None + Lookup url for documentation/information. By default, None. + display_name : Optional[str], default=None + A pretty name for the object. Falls back to ``unique_name`` + when not given. By default, None. + + Raises + ------ + ValueError + If ``value`` is not a string. + """ super().__init__( - name=name, unique_name=unique_name, description=description, url=url, display_name=display_name, - parent=parent, ) if not isinstance(value, str): raise ValueError(f'{value=} must be type str') @@ -49,7 +67,6 @@ def value(self) -> str: return self._string @value.setter - @property_stack def value(self, value: str) -> None: """ Set the value of self. @@ -76,7 +93,7 @@ def value(self, value: str) -> None: def __repr__(self) -> str: """Return printable representation.""" class_name = self.__class__.__name__ - obj_name = self._name + obj_name = self.display_name obj_value = self._string return f"<{class_name} '{obj_name}': {obj_value}>" diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index 3ec654f7..81d6c2a7 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -3,10 +3,8 @@ from __future__ import annotations -import copy import numbers import re -import weakref from typing import Any from typing import Dict from typing import List @@ -20,7 +18,6 @@ from scipp import Variable from easyscience import global_object -from easyscience.global_object.undo_redo import property_stack from .descriptor_number import DescriptorNumber from .descriptor_number import notify_observers @@ -33,15 +30,10 @@ class Parameter(DescriptorNumber): It has additional fields to facilitate this. """ - # Used by serializer - # We copy the parent's _REDIRECT and modify it to avoid altering the parent's class dict - _REDIRECT = DescriptorNumber._REDIRECT.copy() - _REDIRECT['callback'] = None - def __init__( self, - name: str, value: numbers.Number, + *, unit: Optional[Union[str, sc.Unit]] = '', variance: Optional[numbers.Number] = 0.0, min: Optional[numbers.Number] = -np.inf, @@ -51,8 +43,6 @@ def __init__( description: Optional[str] = None, url: Optional[str] = None, display_name: Optional[str] = None, - callback: property = property(), - parent: Optional[Any] = None, **kwargs: Any, # Additional keyword arguments (used for (de)serialization) ): """ @@ -62,10 +52,10 @@ def __init__( for dynamic objects. A parameter has the ability to be used in fitting and has additional fields to facilitate this. + All arguments after ``value`` are keyword-only. + Parameters ---------- - name : str - Name of this object. value : numbers.Number Value of this object. unit : Optional[Union[str, sc.Unit]], default='' @@ -87,14 +77,8 @@ def __init__( url : Optional[str], default=None Lookup url for documentation/information. By default, None. display_name : Optional[str], default=None - The name of the object as it should be displayed. By - default, None. - callback : property, default=property() - Callback used to synchronize the parameter with an external - model. - parent : Optional[Any], default=None - The object which is the parent to this one. By default, - None. + The name of the object as it should be displayed. Falls back + to ``unique_name`` when not given. By default, None. **kwargs : Any Additional keyword arguments used during serialization. @@ -106,11 +90,6 @@ def __init__( If ``value`` falls outside the provided bounds or if the bounds are invalid. - Notes - ----- - Undo/Redo functionality is implemented for the attributes - ``value``, ``variance``, ``error``, ``min``, ``max``, - ``bounds``, ``fixed``, ``unit`` """ # Extract and ignore serialization-specific fields from kwargs kwargs.pop('_dependency_string', None) @@ -139,7 +118,6 @@ def __init__( self._max = sc.scalar(float(max), unit=unit) super().__init__( - name=name, value=value, unit=unit, variance=variance, @@ -147,21 +125,12 @@ def __init__( description=description, url=url, display_name=display_name, - parent=parent, **kwargs, # Additional keyword arguments (used for (de)serialization) ) - self._callback = callback # Callback is used by interface to link to model - if self._callback.fdel is not None: - weakref.finalize(self, self._callback.fdel) - - # Create additional fitting elements - self._initial_scalar = copy.deepcopy(self._scalar) - @classmethod def from_dependency( cls, - name: str, dependency_expression: str, dependency_map: Optional[dict] = None, desired_unit: str | sc.Unit | None = None, @@ -173,8 +142,6 @@ def from_dependency( Parameters ---------- - name : str - The name of the parameter. dependency_expression : str The dependency expression to evaluate. This should be a string which can be evaluated by the ASTEval interpreter. @@ -198,7 +165,7 @@ def from_dependency( default_kwargs = {'value': 0.0, 'variance': 0.0, 'min': -np.inf, 'max': np.inf} # Update with user-provided kwargs, to avoid errors. default_kwargs.update(kwargs) - parameter = cls(name=name, **default_kwargs) + parameter = cls(**default_kwargs) parameter.make_dependent_on( dependency_expression=dependency_expression, dependency_map=dependency_map, @@ -235,9 +202,6 @@ def _update(self) -> None: if self._desired_unit is not None: self._convert_unit(self._desired_unit) - if self._callback.fset is not None: - self._callback.fset(self._scalar.value) - self._notify_observers() else: global_object.log.getLogger('variable').warning( @@ -526,18 +490,6 @@ def dependency_map(self, new_map: Dict[str, DescriptorNumber]) -> None: 'Dependency map is read-only. Use `make_dependent_on` to change the dependency map.' ) - @property - def value_no_call_back(self) -> numbers.Number: - """ - Get the currently hold value of self suppressing call back. - - Returns - ------- - numbers.Number - Value of self without unit. - """ - return self._scalar.value - @property def full_value(self) -> Variable: """ @@ -570,14 +522,9 @@ def value(self) -> numbers.Number: numbers.Number Value of self without unit. """ - if self._callback.fget is not None: - existing_value = self._callback.fget() - if existing_value != self._scalar.value: - self._scalar.value = existing_value return self._scalar.value @value.setter - @property_stack def value(self, value: numbers.Number) -> None: """ Set the value of self. @@ -608,9 +555,6 @@ def value(self, value: numbers.Number) -> None: self._scalar.value = value - if self._callback.fset is not None: - self._callback.fset(self._scalar.value) - # Notify observers of the change self._notify_observers() else: @@ -745,13 +689,10 @@ def min(self) -> numbers.Number: return self._min.value @min.setter - @property_stack def min(self, min_value: numbers.Number) -> None: """ Set the minimum value for fitting. - - implements undo/redo functionality. - Parameters ---------- min_value : numbers.Number @@ -804,13 +745,10 @@ def max(self) -> numbers.Number: return self._max.value @max.setter - @property_stack def max(self, max_value: numbers.Number) -> None: """ Get the maximum value for fitting. - - implements undo/redo functionality. - Parameters ---------- max_value : numbers.Number @@ -863,13 +801,10 @@ def fixed(self) -> bool: return self._fixed @fixed.setter - @property_stack def fixed(self, fixed: bool) -> None: """ Change the parameter vary while fitting state. - - implements undo/redo functionality. - Parameters ---------- fixed : bool @@ -887,9 +822,6 @@ def fixed(self, fixed: bool) -> None: if self._independent: self._fixed = fixed else: - if self._global_object.stack.enabled: - # Remove the recorded change from the stack - global_object.stack.pop() raise AttributeError( 'This is a dependent parameter, dependent parameters cannot be fixed.' ) @@ -903,11 +835,11 @@ def free(self) -> bool: def free(self, value: bool) -> None: self.fixed = not value - def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: """ - Overwrite the as_dict method to handle dependency information. + Overwrite the to_dict method to handle dependency information. """ - raw_dict = super().as_dict(skip=skip) + raw_dict = super().to_dict(skip=skip) # Add dependency information for dependent parameters if not self._independent: @@ -970,15 +902,15 @@ def _process_dependency_unique_names(self, dependency_expression: str): inputted_unique_names += re.findall('(".+?")', dependency_expression) clean_dependency_string = dependency_expression - existing_unique_names = self._global_object.map.vertices() # Add the unique names of the parameters to the ASTEVAL interpreter for name in inputted_unique_names: stripped_name = name.strip('\'"') - if stripped_name not in existing_unique_names: + try: + dependent_parameter = self._global_object.registry[stripped_name] + except KeyError: raise ValueError( f'A Parameter with unique_name {stripped_name} does not exist. Please check your dependency expression.' - ) # noqa: E501 - dependent_parameter = self._global_object.map.get_item_by_key(stripped_name) + ) from None # noqa: E501 if isinstance(dependent_parameter, DescriptorNumber): self._dependency_map['__' + stripped_name + '__'] = dependent_parameter clean_dependency_string = clean_dependency_string.replace( @@ -1019,11 +951,6 @@ def from_dict(cls, obj_dict: dict) -> 'Parameter': return param - def __copy__(self) -> Parameter: - new_obj = super().__copy__() - new_obj._callback = property() - return new_obj - def __repr__(self) -> str: """Return printable representation of a Parameter object.""" super_str = super().__repr__() @@ -1031,6 +958,8 @@ def __repr__(self) -> str: s = [] if self.fixed: super_str += ' (fixed)' + elif not self.independent: + super_str += ' (dependent)' s.append(super_str) s.append('bounds=[%s:%s]' % (repr(float(self.min)), repr(float(self.max)))) return '%s>' % ', '.join(s) @@ -1066,11 +995,7 @@ def __add__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> other._convert_unit(other_unit) else: return NotImplemented - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def __radd__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: if isinstance(other, numbers.Number): @@ -1095,11 +1020,7 @@ def __radd__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: self._convert_unit(original_unit) else: return NotImplemented - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def __sub__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: if isinstance(other, numbers.Number): @@ -1128,11 +1049,7 @@ def __sub__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> other._convert_unit(other_unit) else: return NotImplemented - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def __rsub__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: if isinstance(other, numbers.Number): @@ -1157,21 +1074,13 @@ def __rsub__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: self._convert_unit(original_unit) else: return NotImplemented - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def __mul__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: if isinstance(other, numbers.Number): new_full_value = self.full_value * other if other == 0: - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) combinations = [self.min * other, self.max * other] elif isinstance( other, DescriptorNumber @@ -1180,11 +1089,7 @@ def __mul__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> if ( other.value == 0 and type(other) is DescriptorNumber ): # Only return DescriptorNumber if other is strictly 0, i.e. not a parameter # noqa: E501 - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) if isinstance(other, Parameter): combinations = [] for first, second in [ @@ -1205,43 +1110,29 @@ def __mul__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> return NotImplemented min_value = min(combinations) max_value = max(combinations) - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) + parameter = Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) parameter._convert_unit(parameter._base_unit()) - parameter.name = parameter.unique_name return parameter def __rmul__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: if isinstance(other, numbers.Number): new_full_value = other * self.full_value if other == 0: - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) combinations = [other * self.min, other * self.max] elif isinstance( other, DescriptorNumber ): # Parameter inherits from DescriptorNumber and is also handled here new_full_value = other.full_value * self.full_value if other.value == 0: - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) combinations = [self.min * other.value, self.max * other.value] else: return NotImplemented min_value = min(combinations) max_value = max(combinations) - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) + parameter = Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) parameter._convert_unit(parameter._base_unit()) - parameter.name = parameter.unique_name return parameter def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: @@ -1287,11 +1178,8 @@ def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) return NotImplemented min_value = min(combinations) max_value = max(combinations) - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) + parameter = Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) parameter._convert_unit(parameter._base_unit()) - parameter.name = parameter.unique_name return parameter def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: @@ -1302,22 +1190,14 @@ def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parame new_full_value = other / self.full_value other_value = other if other_value == 0: - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) elif isinstance( other, DescriptorNumber ): # Parameter inherits from DescriptorNumber and is also handled here new_full_value = other.full_value / self.full_value other_value = other.value if other_value == 0: - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) else: return NotImplemented if self.min < 0 and self.max > 0: @@ -1336,11 +1216,8 @@ def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parame combinations = [other_value / self.min, other_value / self.max] min_value = min(combinations) max_value = max(combinations) - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) + parameter = Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) parameter._convert_unit(parameter._base_unit()) - parameter.name = parameter.unique_name return parameter def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: @@ -1365,11 +1242,7 @@ def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: if np.isnan(new_full_value.value): raise ValueError('The result of the exponentiation is not a number') if exponent == 0: - descriptor_number = DescriptorNumber.from_scipp( - name=self.name, full_value=new_full_value - ) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + return DescriptorNumber.from_scipp(full_value=new_full_value) elif exponent < 0: if self.min < 0 and self.max > 0: combinations = [-np.inf, np.inf] @@ -1391,21 +1264,13 @@ def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: combinations = [combination for combination in combinations if combination >= 0] min_value = min(combinations) max_value = max(combinations) - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def __neg__(self) -> Parameter: new_full_value = -self.full_value min_value = -self.max max_value = -self.min - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def __abs__(self) -> Parameter: new_full_value = abs(self.full_value) @@ -1414,11 +1279,7 @@ def __abs__(self) -> Parameter: combinations.append(0.0) min_value = min(combinations) max_value = max(combinations) - parameter = Parameter.from_scipp( - name=self.name, full_value=new_full_value, min=min_value, max=max_value - ) - parameter.name = parameter.unique_name - return parameter + return Parameter.from_scipp(full_value=new_full_value, min=min_value, max=max_value) def resolve_pending_dependencies(self) -> None: """ @@ -1463,9 +1324,9 @@ def resolve_pending_dependencies(self) -> None: def _find_parameter_by_serializer_id(self, serializer_id: str) -> Optional['DescriptorNumber']: """ Find a parameter by its serializer_id from all parameters in the - global map. + object registry. """ - for obj in self._global_object.map._store.values(): + for obj in self._global_object.registry.objects(): if isinstance(obj, DescriptorNumber) and hasattr( obj, '_DescriptorNumber__serializer_id' ): diff --git a/src/easyscience/variable/parameter_dependency_resolver.py b/src/easyscience/variable/parameter_dependency_resolver.py index d231dcd2..fd91411b 100644 --- a/src/easyscience/variable/parameter_dependency_resolver.py +++ b/src/easyscience/variable/parameter_dependency_resolver.py @@ -82,8 +82,8 @@ def _collect_parameters(item: Any, parameters: List[Parameter]) -> None: error_count += 1 serializer_id = getattr(param, '_DescriptorNumber__serializer_id', 'unknown') errors.append( - f"Failed to resolve dependencies for parameter '{param.name}'" - f" (unique_name: '{param.unique_name}', serializer_id: '{serializer_id}'): {e}" + f"Failed to resolve dependencies for parameter '{param.unique_name}'" + f" (display_name: '{param.display_name}', serializer_id: '{serializer_id}'): {e}" ) # Report results diff --git a/tests/conftest.py b/tests/conftest.py index 2f9a6768..bdad1f01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,10 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] SRC_ROOT = PROJECT_ROOT / 'src' +# Shared test-only models, imported as ``from easy_test_models import ...`` +HELPERS_ROOT = Path(__file__).resolve().parent / 'helpers' -src_root_str = str(SRC_ROOT) -if src_root_str not in sys.path: - sys.path.insert(0, src_root_str) +for root in (SRC_ROOT, HELPERS_ROOT): + root_str = str(root) + if root_str not in sys.path: + sys.path.insert(0, root_str) diff --git a/tests/functional/test_smoke.py b/tests/functional/test_smoke.py deleted file mode 100644 index 23323fc9..00000000 --- a/tests/functional/test_smoke.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import numpy as np -import pytest - -from easyscience import global_object -from easyscience.models.polynomial import Polynomial - - -@pytest.mark.fast -def test_polynomial_smoke() -> None: - """Exercise a minimal user-facing model workflow.""" - global_object.map._clear() - - model = Polynomial(name='smoke', coefficients=[1.0, 2.0, 3.0]) - x = np.array([0.0, 1.0, 2.0]) - - assert [coefficient.name for coefficient in model.coefficients] == ['c0', 'c1', 'c2'] - assert np.allclose(model(x), np.polyval([1.0, 2.0, 3.0], x)) diff --git a/tests/helpers/easy_test_models.py b/tests/helpers/easy_test_models.py new file mode 100644 index 00000000..8b6a51c8 --- /dev/null +++ b/tests/helpers/easy_test_models.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Test-only models shared across the test suite. + +``tests/conftest.py`` puts this directory on ``sys.path``, so tests +import from here with ``from easy_test_models import ...``. The module +name starts with ``easy`` on purpose: the serializer only treats a dict +as an EasyScience object (and dispatches to ``from_dict``) when its +``@module`` starts with ``easy``, so this keeps serializer round trips +working for these models. +""" + +from __future__ import annotations + +from typing import Iterable + +import numpy as np + +from easyscience.base_classes import EasyList +from easyscience.base_classes import ModelBase +from easyscience.variable import Parameter + + +class CoefficientModel(ModelBase): + """ + A minimal model holding its parameters in an ``EasyList``. + + It exercises nested containers in serialization, copying, fitting + parameter discovery and external calculators. Plain numbers become + ``Parameter`` objects named ``c0``, ``c1``, ... The model evaluates + as a polynomial with the coefficients in ``numpy.polyval`` order. + + Parameters + ---------- + display_name : str | None, default=None + Display name of the model. + coefficients : Iterable[float | Parameter] | EasyList | None, default=None + Initial coefficients, either numbers/``Parameter`` objects or a + ready-made ``EasyList``. + unique_name : str | None, default=None + Unique name of the model. + """ + + def __init__( + self, + display_name: str | None = None, + coefficients: Iterable[float | Parameter] | EasyList | None = None, + unique_name: str | None = None, + ): + super().__init__(unique_name=unique_name, display_name=display_name) + if isinstance(coefficients, EasyList): + self._coefficients = coefficients + return + self._coefficients = EasyList(protected_types=Parameter) + for index, item in enumerate(coefficients or []): + if not isinstance(item, Parameter): + item = Parameter(value=float(item), display_name=f'c{index}') + self._coefficients.append(item) + + @property + def coefficients(self) -> EasyList: + """The coefficient parameters.""" + return self._coefficients + + def __call__(self, x: np.ndarray, *args, **kwargs) -> np.ndarray: + return np.polyval([c.value for c in self.coefficients], x) + + +class PairModel(ModelBase): + """ + Model holding two parameters as plain attributes. + + Used to check that a nested ``Parameter`` is serialized through its + own ``to_dict``, so dependency information survives a round trip. + + Following the ``ModelBase.from_dict`` convention, a plain number is + wrapped in a new ``Parameter`` while a ``Parameter`` is used as is. + + Parameters + ---------- + a : float | Parameter, default=0.0 + First parameter. + b : float | Parameter, default=0.0 + Second parameter, possibly dependent on ``a``. + unique_name : str | None, default=None + Unique name of the model. + display_name : str | None, default=None + Display name of the model. + """ + + def __init__( + self, + a: float | Parameter = 0.0, + b: float | Parameter = 0.0, + unique_name: str | None = None, + display_name: str | None = None, + ): + super().__init__(unique_name=unique_name, display_name=display_name) + self._a = a if isinstance(a, Parameter) else Parameter(value=float(a), display_name='a') + self._b = b if isinstance(b, Parameter) else Parameter(value=float(b), display_name='b') + + @property + def a(self) -> Parameter: + """First parameter.""" + return self._a + + @property + def b(self) -> Parameter: + """Second parameter.""" + return self._b diff --git a/tests/integration/fitting/test_fitter.py b/tests/integration/fitting/test_fitter.py index df17a65c..740c5033 100644 --- a/tests/integration/fitting/test_fitter.py +++ b/tests/integration/fitting/test_fitter.py @@ -4,37 +4,43 @@ import numpy as np import pytest -from easyscience import AvailableMinimizers from easyscience import Fitter -from easyscience import ObjBase +from easyscience import Minimizers from easyscience import Parameter from easyscience.base_classes import ModelBase from easyscience.fitting.minimizers import FitError # Model and container of parameters for tests -class AbsSin(ObjBase): - phase: Parameter - offset: Parameter - +class _PhaseOffset(ModelBase): def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin', offset=offset, phase=phase) + super().__init__() + self._offset = Parameter(offset_val, display_name='offset') + self._phase = Parameter(phase_val, display_name='phase') - def __call__(self, x): - return np.abs(np.sin(self.phase.value * x + self.offset.value)) + @property + def offset(self) -> Parameter: + return self._offset + @offset.setter + def offset(self, value: float) -> None: + self._offset.value = value -class AbsSin2D(ObjBase): - phase: Parameter - offset: Parameter + @property + def phase(self) -> Parameter: + return self._phase - def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin2D', offset=offset, phase=phase) + @phase.setter + def phase(self, value: float) -> None: + self._phase.value = value + + +class AbsSin(_PhaseOffset): + def __call__(self, x): + return np.abs(np.sin(self.phase.value * x + self.offset.value)) + +class AbsSin2D(_PhaseOffset): def __call__(self, x): X = x[:, :, 0] # x is a 2D array Y = x[:, :, 1] @@ -55,8 +61,8 @@ def __call__(self, x): class StraightLine(ModelBase): def __init__(self, slope: float, intercept: float): super().__init__() - self._slope = Parameter('slope', slope) - self._intercept = Parameter('intercept', intercept) + self._slope = Parameter(slope, display_name='slope') + self._intercept = Parameter(intercept, display_name='intercept') @property def slope(self) -> Parameter: @@ -78,8 +84,8 @@ def __call__(self, x: np.ndarray) -> np.ndarray: return self.slope.value * x + self.intercept.value -def check_fit_results(result, sp_sin, ref_sin, x, **kwargs): - assert result.n_pars == len(sp_sin.get_fit_parameters()) +def check_fit_results(result, sp_sin, ref_sin, x, expect_error=True, **kwargs): + assert result.n_pars == len(sp_sin.get_free_parameters()) assert result.chi2 == pytest.approx(0, abs=1.5e-3 * (len(result.x) - result.n_pars)) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) assert result.success @@ -90,9 +96,14 @@ def check_fit_results(result, sp_sin, ref_sin, x, **kwargs): assert key in result.p0.keys() assert result.p0[key] == pytest.approx(value) # Bumps does something strange here assert np.all(result.x == x) - for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()): - # assert item.error > 0 % This does not work as some methods don't calculate error - assert item1.error == pytest.approx(0, abs=2.1e-1) + for item1, item2 in zip(sp_sin.get_all_parameters(), ref_sin.get_all_parameters()): + # Gradient-free methods (e.g. lmfit's powell/cobyla) have no covariance + # matrix, so they report error as None rather than a fake 0.0. Every + # other method must still produce a real uncertainty. + if expect_error: + assert item1.error == pytest.approx(0, abs=2.1e-1) + else: + assert item1.error is None assert item1.value == pytest.approx(item2.value, abs=5e-3) y_calc_ref = ref_sin(x) assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) @@ -104,12 +115,12 @@ def check_fit_results(result, sp_sin, ref_sin, x, **kwargs): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) -def test_basic_fit(fit_engine: AvailableMinimizers): +def test_basic_fit(fit_engine: Minimizers): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -122,10 +133,7 @@ def test_basic_fit(fit_engine: AvailableMinimizers): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x=x, y=y, weights=weights) @@ -142,9 +150,9 @@ def test_basic_fit(fit_engine: AvailableMinimizers): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_fit_result(fit_engine): @@ -160,20 +168,17 @@ def test_fit_result(fit_engine): sp_ref1 = { f'p{item1.unique_name}': item1.value - for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) + for item1, item2 in zip(sp_sin.get_all_parameters(), ref_sin.get_all_parameters()) } sp_ref2 = { f'p{item1.unique_name}': item2.value - for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) + for item1, item2 in zip(sp_sin.get_all_parameters(), ref_sin.get_all_parameters()) } f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x, y, weights=weights) check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2) @@ -184,9 +189,9 @@ def test_fit_result(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_basic_max_evaluations(fit_engine): @@ -202,10 +207,7 @@ def test_basic_max_evaluations(fit_engine): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) # Result should not be the same as the reference @@ -218,9 +220,9 @@ def test_basic_max_evaluations(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_max_evaluations_populates_fit_result_fields(fit_engine): @@ -237,10 +239,7 @@ def test_max_evaluations_populates_fit_result_fields(fit_engine): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) @@ -265,10 +264,7 @@ def test_bumps_max_evaluations_counts_objective_calls() -> None: sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - try: - f.switch_minimizer(AvailableMinimizers.Bumps) - except AttributeError: - pytest.skip(reason=f'{AvailableMinimizers.Bumps} is not installed') + f.switch_minimizer(Minimizers.Bumps) f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) @@ -285,9 +281,9 @@ def test_bumps_max_evaluations_counts_objective_calls() -> None: 'fit_engine,tolerance', [ (None, 10), - (AvailableMinimizers.LMFit, 10), - (AvailableMinimizers.Bumps, 10), - (AvailableMinimizers.DFO, 0.1), + (Minimizers.LMFit, 10), + (Minimizers.Bumps, 10), + (Minimizers.DFO, 0.1), ], ) def test_basic_tolerance(fit_engine, tolerance): @@ -303,10 +299,7 @@ def test_basic_tolerance(fit_engine, tolerance): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.tolerance = tolerance result = f.fit(x=x, y=y, weights=weights) # Result should not be the same as the reference @@ -330,7 +323,9 @@ def test_lmfit_methods(fit_method): f = Fitter(sp_sin, sp_sin) assert fit_method in f._minimizer.supported_methods() result = f.fit(x, y, weights=weights, method=fit_method) - check_fit_results(result, sp_sin, ref_sin, x) + check_fit_results( + result, sp_sin, ref_sin, x, expect_error=fit_method not in ('powell', 'cobyla') + ) # @pytest.mark.xfail(reason="known bumps issue") @@ -348,16 +343,45 @@ def test_bumps_methods(fit_method): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - f.switch_minimizer('Bumps') + f.switch_minimizer(Minimizers.Bumps) assert fit_method in f._minimizer.supported_methods() result = f.fit(x, y, weights=weights, method=fit_method) check_fit_results(result, sp_sin, ref_sin, x) +@pytest.mark.fast +def test_bumps_fit_emits_no_fitness_deprecation_warning(): + """Regression (CR-1): the classical BUMPS fit path must not read the + deprecated ``FitProblem.fitness`` property, which emits a ``UserWarning`` + on bumps >= 1.0.4 β€” the ``Curve`` comes back from ``build_curve_problem`` + directly.""" + import warnings + + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + weights = np.ones_like(x) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + f = Fitter(sp_sin, sp_sin) + f.switch_minimizer(Minimizers.Bumps) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + f.fit(x, y, weights=weights) + + fitness_warnings = [w for w in caught if 'fitness' in str(w.message)] + assert fitness_warnings == [] + + @pytest.mark.fast @pytest.mark.parametrize( 'fit_engine', - [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO], + [Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], ) def test_dependent_parameter(fit_engine): ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) @@ -374,13 +398,13 @@ def test_dependent_parameter(fit_engine): ) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x, y, weights=weights) - check_fit_results(result, sp_sin, ref_sin, x) + # With `offset` dependent on `phase` only one parameter varies against + # noiseless data, so lmfit's covariance is exactly [[0.]] and it reports + # `errorbars=False` -> no uncertainties. + check_fit_results(result, sp_sin, ref_sin, x, expect_error=fit_engine is not Minimizers.LMFit) @pytest.mark.fast @@ -388,9 +412,9 @@ def test_dependent_parameter(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_2D_vectorized(fit_engine): @@ -402,10 +426,7 @@ def test_2D_vectorized(fit_engine): weights = np.ones_like(mm(XY)) ff = Fitter(m2, m2) if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + ff.switch_minimizer(fit_engine) try: result = ff.fit(x=XY, y=mm(XY), weights=weights, vectorized=True) except FitError as e: @@ -413,7 +434,7 @@ def test_2D_vectorized(fit_engine): pytest.skip(reason='MemoryError - Matrix too large') else: raise e - assert result.n_pars == len(m2.get_fit_parameters()) + assert result.n_pars == len(m2.get_free_parameters()) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) assert result.success assert np.all(result.x == XY) @@ -427,9 +448,9 @@ def test_2D_vectorized(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_2D_non_vectorized(fit_engine): @@ -441,10 +462,7 @@ def test_2D_non_vectorized(fit_engine): weights = np.ones_like(mm(XY.reshape(-1, 2))) ff = Fitter(m2, m2) if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + ff.switch_minimizer(fit_engine) try: result = ff.fit(x=XY, y=mm(XY.reshape(-1, 2)), weights=weights, vectorized=False) except FitError as e: @@ -452,7 +470,7 @@ def test_2D_non_vectorized(fit_engine): pytest.skip(reason='MemoryError - Matrix too large') else: raise e - assert result.n_pars == len(m2.get_fit_parameters()) + assert result.n_pars == len(m2.get_free_parameters()) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) assert result.success assert np.all(result.x == XY) @@ -466,9 +484,9 @@ def test_2D_non_vectorized(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_fixed_parameter_does_not_change(fit_engine): @@ -489,10 +507,7 @@ def test_fixed_parameter_does_not_change(fit_engine): # THEN f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x=x, y=y, weights=weights) @@ -529,9 +544,9 @@ def test_fitter_new_model_base_integration(): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_fitter_variable_weights(fit_engine): @@ -564,10 +579,7 @@ def run_fit(weights): f = Fitter(model, model) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.fit(x=x, y=y, weights=weights) return model.offset.value, model.phase.value @@ -598,9 +610,9 @@ def _analytic_wls(design: np.ndarray, y: np.ndarray, point_weights: np.ndarray): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_weight_convention_matches_analytic_wls(fit_engine): @@ -638,10 +650,7 @@ def test_weight_convention_matches_analytic_wls(fit_engine): f = Fitter(model, model) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x=x, y=y, weights=1.0 / sigma) @@ -649,7 +658,7 @@ def test_weight_convention_matches_analytic_wls(fit_engine): assert model.slope.value == pytest.approx(beta[0], abs=tolerance) assert model.intercept.value == pytest.approx(beta[1], abs=tolerance) - if fit_engine in (None, AvailableMinimizers.LMFit): + if fit_engine in (None, Minimizers.LMFit): # lmfit scales the covariance by reduced chi-square (scale_covar=True) residual = y - design @ beta chi2 = float(np.sum((residual / sigma) ** 2)) diff --git a/tests/integration/fitting/test_multi_fitter.py b/tests/integration/fitting/test_multi_fitter.py index 25de53d5..46807d8e 100644 --- a/tests/integration/fitting/test_multi_fitter.py +++ b/tests/integration/fitting/test_multi_fitter.py @@ -4,47 +4,66 @@ import numpy as np import pytest -from easyscience import ObjBase +from easyscience import Minimizers from easyscience import Parameter +from easyscience.base_classes import ModelBase from easyscience.fitting.minimizers import FitError from easyscience.fitting.multi_fitter import MultiFitter -class Line(ObjBase): - m: Parameter - c: Parameter - +class Line(ModelBase): def __init__(self, m_val: float, c_val: float): - m = Parameter('m', m_val) - c = Parameter('c', c_val) - super(Line, self).__init__('line', m=m, c=c) + super().__init__() + self._m = Parameter(m_val, display_name='m') + self._c = Parameter(c_val, display_name='c') + + @property + def m(self) -> Parameter: + return self._m + + @m.setter + def m(self, value: float) -> None: + self._m.value = value + + @property + def c(self) -> Parameter: + return self._c + + @c.setter + def c(self, value: float) -> None: + self._c.value = value def __call__(self, x): return self.m.value * x + self.c.value -class AbsSin(ObjBase): - phase: Parameter - offset: Parameter - +class AbsSin(ModelBase): def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin', offset=offset, phase=phase) + super().__init__() + self._offset = Parameter(offset_val, display_name='offset') + self._phase = Parameter(phase_val, display_name='phase') - def __call__(self, x): - return np.abs(np.sin(self.phase.value * x + self.offset.value)) + @property + def offset(self) -> Parameter: + return self._offset + @offset.setter + def offset(self, value: float) -> None: + self._offset.value = value -class AbsSin2D(ObjBase): - phase: Parameter - offset: Parameter + @property + def phase(self) -> Parameter: + return self._phase + + @phase.setter + def phase(self, value: float) -> None: + self._phase.value = value + + def __call__(self, x): + return np.abs(np.sin(self.phase.value * x + self.offset.value)) - def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin2D', offset=offset, phase=phase) +class AbsSin2D(AbsSin): def __call__(self, x): X = x[:, :, 0] # x is a 2D array Y = x[:, :, 1] @@ -53,7 +72,10 @@ def __call__(self, x): ) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit(fit_engine): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -80,10 +102,7 @@ def test_multi_fit(fit_engine): f = MultiFitter([sp_sin_1, sp_sin_2], [sp_sin_1, sp_sin_2]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) results = f.fit(x=[x1, x2], y=[y1, y2], weights=[weights, weights]) X = [x1, x2] @@ -91,8 +110,8 @@ def test_multi_fit(fit_engine): F_ref = [ref_sin_1, ref_sin_2] F_real = [sp_sin_1, sp_sin_2] for idx, result in enumerate(results): - assert result.n_pars == len(sp_sin_1.get_fit_parameters()) + len( - sp_sin_2.get_fit_parameters() + assert result.n_pars == len(sp_sin_1.get_free_parameters()) + len( + sp_sin_2.get_free_parameters() ) assert result.chi2 == pytest.approx(0, abs=1.5e-3 * (len(result.x) - result.n_pars)) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) @@ -103,7 +122,10 @@ def test_multi_fit(fit_engine): assert result.residual == pytest.approx(F_real[idx](X[idx]) - F_ref[idx](X[idx]), abs=1e-2) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit_propagates_iteration_metadata_and_message(fit_engine): """Verify that fit metadata and message are copied into each per-dataset result.""" ref_sin_1 = AbsSin(0.2, np.pi) @@ -130,10 +152,7 @@ def test_multi_fit_propagates_iteration_metadata_and_message(fit_engine): f = MultiFitter([sp_sin_1, sp_sin_2], [sp_sin_1, sp_sin_2]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) results = f.fit(x=[x1, x2], y=[y1, y2], weights=[weights, weights]) for result in results: @@ -146,7 +165,10 @@ def test_multi_fit_propagates_iteration_metadata_and_message(fit_engine): assert isinstance(result.message, str) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit2(fit_engine): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -185,10 +207,7 @@ def test_multi_fit2(fit_engine): f = MultiFitter([sp_sin_1, sp_line, sp_sin_2], [sp_sin_1, sp_line, sp_sin_2]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) results = f.fit(x=[x1, x2, x3], y=[y1, y2, y3], weights=[weights, weights, weights]) X = [x1, x2, x3] @@ -199,9 +218,9 @@ def test_multi_fit2(fit_engine): assert len(results) == len(X) for idx, result in enumerate(results): - assert result.n_pars == len(sp_sin_1.get_fit_parameters()) + len( - sp_sin_2.get_fit_parameters() - ) + len(sp_line.get_fit_parameters()) + assert result.n_pars == len(sp_sin_1.get_free_parameters()) + len( + sp_sin_2.get_free_parameters() + ) + len(sp_line.get_free_parameters()) assert result.chi2 == pytest.approx(0, abs=1.5e-3 * (len(result.x) - result.n_pars)) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) assert result.success @@ -211,7 +230,10 @@ def test_multi_fit2(fit_engine): assert result.residual == pytest.approx(F_ref[idx](X[idx]) - F_real[idx](X[idx]), abs=1e-2) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit_1D_2D(fit_engine): # Generate fit and reference objects ref_sin1D = AbsSin(0.2, np.pi) @@ -241,10 +263,7 @@ def test_multi_fit_1D_2D(fit_engine): ff = MultiFitter([sp_sin1D, sp_sin2D], [sp_sin1D, sp_sin2D]) if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + ff.switch_minimizer(fit_engine) sp_sin1D.offset.fixed = False sp_sin1D.phase.fixed = False @@ -252,10 +271,7 @@ def test_multi_fit_1D_2D(fit_engine): f = MultiFitter([sp_sin1D, sp_sin2D], [sp_sin1D, sp_sin2D]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) try: results = f.fit( x=[x1D, x2D], y=[y1D, y2D], weights=[weights1D, weights2D], vectorized=True @@ -271,11 +287,11 @@ def test_multi_fit_1D_2D(fit_engine): F_ref = [ref_sin1D, ref_sin2D] F_real = [sp_sin1D, sp_sin2D] for idx, result in enumerate(results): - assert result.n_pars == len(sp_sin1D.get_fit_parameters()) + len( - sp_sin2D.get_fit_parameters() + assert result.n_pars == len(sp_sin1D.get_free_parameters()) + len( + sp_sin2D.get_free_parameters() ) if ( - fit_engine != 'DFO' + fit_engine != Minimizers.DFO ): # DFO apparently does not fit well with even weights. Can't be bothered to fix assert result.chi2 == pytest.approx(0, abs=1.5e-3 * (len(result.x) - result.n_pars)) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) diff --git a/tests/integration/fitting/test_sampler.py b/tests/integration/fitting/test_sampler.py index 032cc39f..efae0168 100644 --- a/tests/integration/fitting/test_sampler.py +++ b/tests/integration/fitting/test_sampler.py @@ -12,48 +12,67 @@ import numpy as np import pytest -from easyscience import ObjBase from easyscience import Parameter +from easyscience.base_classes import ModelBase +from easyscience.fitting import Fitter from easyscience.fitting import Sampler from easyscience.fitting import SamplingResults from easyscience.fitting.multi_fitter import MultiFitter -class Line(ObjBase): - m: Parameter - c: Parameter - +class Line(ModelBase): def __init__(self, m_val: float, c_val: float): - m = Parameter('m', m_val) - c = Parameter('c', c_val) - super(Line, self).__init__('line', m=m, c=c) + super().__init__() + self._m = Parameter(m_val, display_name='m') + self._c = Parameter(c_val, display_name='c') + + @property + def m(self) -> Parameter: + return self._m + + @m.setter + def m(self, value: float) -> None: + self._m.value = value + + @property + def c(self) -> Parameter: + return self._c + + @c.setter + def c(self, value: float) -> None: + self._c.value = value def __call__(self, x): return self.m.value * x + self.c.value -class AbsSin(ObjBase): - phase: Parameter - offset: Parameter - +class AbsSin(ModelBase): def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin', offset=offset, phase=phase) + super().__init__() + self._offset = Parameter(offset_val, display_name='offset') + self._phase = Parameter(phase_val, display_name='phase') - def __call__(self, x): - return np.abs(np.sin(self.phase.value * x + self.offset.value)) + @property + def offset(self) -> Parameter: + return self._offset + @offset.setter + def offset(self, value: float) -> None: + self._offset.value = value -class AbsSin2D(ObjBase): - phase: Parameter - offset: Parameter + @property + def phase(self) -> Parameter: + return self._phase - def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin2D', offset=offset, phase=phase) + @phase.setter + def phase(self, value: float) -> None: + self._phase.value = value + def __call__(self, x): + return np.abs(np.sin(self.phase.value * x + self.offset.value)) + + +class AbsSin2D(AbsSin): def __call__(self, x): X = x[:, :, 0] # x is a 2D array Y = x[:, :, 1] @@ -62,8 +81,13 @@ def __call__(self, x): ) -def _bumps_fitter_and_data(): - """Build a 2-parameter BUMPS MultiFitter over a small sine model.""" +def _model_and_data(): + """Build a 2-parameter sine model and a small dataset to sample. + + No ``Fitter`` is involved: sampling needs only the model, its fit + function, the data, and an installed ``bumps`` package. + """ + pytest.importorskip('bumps') ref_sin = AbsSin(0.2, np.pi) sp = AbsSin(0.354, 3.05) sp.offset.fixed = False @@ -71,22 +95,17 @@ def _bumps_fitter_and_data(): x = np.linspace(0, 5, 50) y = ref_sin(x) weights = np.ones_like(x) - f = MultiFitter([sp], [sp]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - return f, sp, x, y, weights + return sp, x, y, weights class TestSampler: - """Integration tests for ``Sampler(f, ...)`` / ``Sampler``.""" + """Integration tests for ``Sampler``.""" @pytest.mark.filterwarnings('ignore::UserWarning') def test_sample_returns_results_object(self): - """sample() returns a populated SamplingResults; to_legacy_dict() has the legacy shape.""" - f, sp, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + """sample() returns a populated SamplingResults, cached on the sampler.""" + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) results = sampler.sample(samples=100, burn=20, thin=2) @@ -97,7 +116,7 @@ def test_sample_returns_results_object(self): assert results.state is not None # param_names should match the model's fit parameters - expected_pars = {p.unique_name for p in sp.get_fit_parameters()} + expected_pars = {p.unique_name for p in sp.get_free_parameters()} assert set(results.param_names) == expected_pars # results cached on the sampler @@ -106,14 +125,9 @@ def test_sample_returns_results_object(self): assert sampler.draws is results.draws assert sampler.param_names == results.param_names - # legacy dict shape - legacy = results.to_legacy_dict() - assert set(legacy.keys()) == {'draws', 'param_names', 'internal_bumps_object', 'logp'} - assert legacy['internal_bumps_object'] is results.state - @pytest.mark.filterwarnings('ignore::UserWarning') def test_sample_multi_dataset(self): - """Multi-dataset sampling via Sampler(f, ...) has correct param_names.""" + """Multi-dataset sampling has correct param_names.""" ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) sp_line = Line(0.43, 6.1) @@ -133,24 +147,25 @@ def test_sample_multi_dataset(self): sp_sin_1.phase.fixed = False sp_line.c.fixed = False + pytest.importorskip('bumps') + # Direct multi-dataset construction: one container object exposing all + # parameters (here the one MultiFitter builds) and one function per + # dataset. f = MultiFitter([sp_sin_1, sp_line], [sp_sin_1, sp_line]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - - sampler = Sampler(f, [x1, x2], [y1, y2], [weights, weights]) + sampler = Sampler( + f.fit_object, [sp_sin_1, sp_line], [x1, x2], [y1, y2], [weights, weights] + ) results = sampler.sample(samples=100, burn=20, thin=2) # All parameters across both models should appear - all_params = {p.unique_name for p in sp_sin_1.get_fit_parameters()} - all_params |= {p.unique_name for p in sp_line.get_fit_parameters()} + all_params = {p.unique_name for p in sp_sin_1.get_free_parameters()} + all_params |= {p.unique_name for p in sp_line.get_free_parameters()} assert set(results.param_names) == all_params def test_sample_population(self): """Passing population should succeed and produce valid draws.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) results = sampler.sample(samples=100, burn=20, thin=2, population=5) assert results.draws.shape[0] > 0 @@ -169,13 +184,8 @@ def test_sample_vectorized_2d(self): sp.offset.fixed = False sp.phase.fixed = False - f = MultiFitter([sp], [sp]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - - sampler = Sampler(f, [x2D], [y2D], [weights], vectorized=True) + pytest.importorskip('bumps') + sampler = Sampler(sp, sp, x2D, y2D, weights, vectorized=True) results = sampler.sample(samples=100, burn=20, thin=2) assert results.draws.ndim == 2 @@ -183,20 +193,53 @@ def test_sample_vectorized_2d(self): assert results.draws.shape[1] == len(results.param_names) @pytest.mark.filterwarnings('ignore::UserWarning') - def test_fit_function_restored_on_success(self): - """fit_function must be restored after a successful sample().""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) - original_func = f.fit_function + def test_from_fitter_samples_and_leaves_fitter_untouched(self): + """from_fitter() on a plain, unfitted Fitter with its default LMFit + minimizer samples fine and never touches the fitter.""" + sp, x, y, weights = _model_and_data() + f = Fitter(sp, sp) + assert f.minimizer.package == 'lmfit' + + sampler = Sampler.from_fitter(f, x, y, weights) + results = sampler.sample(samples=100, burn=20, thin=2) - sampler.sample(samples=100, burn=20, thin=2) - assert f.fit_function is original_func + assert results.draws.shape[0] > 0 + assert f.fit_function is sp + assert f.minimizer.package == 'lmfit' + + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_from_multi_fitter_untouched_multi_dataset(self): + """from_fitter() on a 2-dataset MultiFitter samples each dataset with + its own function and leaves the fitter's fit_function alone.""" + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + sp_line = Line(0.43, 6.1) + sp_sin.offset.fixed = False + sp_line.c.fixed = False + + x1 = np.linspace(0, 5, 50) + y1 = ref_sin(x1) + x2 = np.copy(x1) + y2 = Line(1, 4.6)(x2) + weights = np.ones_like(x1) + + pytest.importorskip('bumps') + f = MultiFitter([sp_sin, sp_line], [sp_sin, sp_line]) + original = f.fit_function + assert original is sp_sin # two distinct per-dataset functions + + sampler = Sampler.from_fitter(f, [x1, x2], [y1, y2], [weights, weights]) + results = sampler.sample(samples=50, burn=5, thin=1) + + assert f.fit_function is original + assert sampler.fit_function == [sp_sin, sp_line] + assert results.draws.shape[0] > 0 @pytest.mark.filterwarnings('ignore::UserWarning') def test_sampler_kwargs_forwarded(self): """Per-call sampler_kwargs dict is forwarded to the BUMPS DREAM sampler.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) results = sampler.sample(samples=100, burn=20, thin=2, sampler_kwargs={'init': 'random'}) @@ -204,34 +247,34 @@ def test_sampler_kwargs_forwarded(self): assert results.draws.shape[0] > 0 @pytest.mark.filterwarnings('ignore::UserWarning') - def test_default_sampler_kwargs_merged(self): + def test_default_sampler_kwargs_merged(self, monkeypatch): """Constructor-level sampler_kwargs defaults are used; per-call kwargs win.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights], sampler_kwargs={'init': 'random'}) + from easyscience.fitting.samplers.sampler_bumps import DreamSampler + + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights, sampler_kwargs={'init': 'random'}) captured = {} - original_mcmc_sample = type(f.minimizer).mcmc_sample + original_run = DreamSampler.run def spy(self, **kwargs): captured.update(kwargs.get('sampler_kwargs') or {}) - return original_mcmc_sample(self, **kwargs) + return original_run(self, **kwargs) - try: - type(f.minimizer).mcmc_sample = spy - sampler.sample(samples=100, burn=20, thin=2) - assert captured == {'init': 'random'} + monkeypatch.setattr(DreamSampler, 'run', spy) + + sampler.sample(samples=100, burn=20, thin=2) + assert captured == {'init': 'random'} - captured.clear() - sampler.sample(samples=100, burn=20, thin=2, sampler_kwargs={'init': 'lhs'}) - assert captured == {'init': 'lhs'} # per-call overrides default - finally: - type(f.minimizer).mcmc_sample = original_mcmc_sample + captured.clear() + sampler.sample(samples=100, burn=20, thin=2, sampler_kwargs={'init': 'lhs'}) + assert captured == {'init': 'lhs'} # per-call overrides default @pytest.mark.filterwarnings('ignore::UserWarning') def test_extend_chain(self): """extend(additional_samples=) continues the chain; ring-buffer math is done for the user.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) first = sampler.sample(samples=100, burn=20, thin=1) n_first = first.draws.shape[0] @@ -250,8 +293,8 @@ def test_extend_with_thinning_keeps_existing_draws(self): generations (``Ngen * Npop``), not from the retained-draw count, which BUMPS divides by the thinning interval. """ - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) first = sampler.sample(samples=1000, burn=20, thin=10) n_first = first.draws.shape[0] @@ -264,8 +307,8 @@ def test_extend_with_thinning_keeps_existing_draws(self): @pytest.mark.filterwarnings('ignore::UserWarning') def test_extend_total_samples_override(self): """extend(total_samples=) bypasses the additional_samples arithmetic.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) sampler.sample(samples=100, burn=20, thin=1) extended = sampler.extend(total_samples=150, thin=1) @@ -282,14 +325,14 @@ def test_extend_after_save_load_roundtrip(self, tmp_path, caplog): """ import logging - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) first = sampler.sample(samples=100, burn=20, thin=1) prefix = str(tmp_path / 'chain') sampler.save(prefix) - sampler2 = Sampler(f, [x], [y], [weights]) + sampler2 = Sampler(sp, sp, x, y, weights) loaded = sampler2.load_state(prefix) assert loaded.draws.shape[1] == first.draws.shape[1] @@ -308,8 +351,8 @@ def test_extend_preserves_nondefault_population(self): saved state on resume, otherwise BUMPS regenerates the default population and raises ``Cannot change Nvar, Npop or Ncr on resize``. """ - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) first = sampler.sample(samples=100, burn=20, thin=1, population=5) first_npop = first.state.Npop @@ -327,8 +370,8 @@ def test_save_warns_when_fingerprint_unavailable(self, tmp_path, caplog, monkeyp logs a warning and records ``null`` in the sidecar.""" import logging - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) sampler.sample(samples=100, burn=20, thin=2) monkeypatch.setattr('easyscience.fitting.sampler._data_fingerprint', lambda *args: None) @@ -344,14 +387,14 @@ def test_save_warns_when_fingerprint_unavailable(self, tmp_path, caplog, monkeyp @pytest.mark.filterwarnings('ignore::UserWarning') def test_load_state_populates_results(self, tmp_path): """A freshly loaded sampler reports draws/logp/param_names without resampling.""" - f, sp, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) first = sampler.sample(samples=100, burn=20, thin=2) prefix = str(tmp_path / 'chain') sampler.save(prefix) - sampler2 = Sampler(f, [x], [y], [weights]) + sampler2 = Sampler(sp, sp, x, y, weights) assert sampler2.draws is None loaded = sampler2.load_state(prefix) @@ -377,22 +420,22 @@ def test_load_short_chain_regression(self, tmp_path): reader collapses it to a 1-D array and ``load_state`` raises ``IndexError`` without the 2-D coercion workaround. """ - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) sampler.sample(samples=20, burn=5, thin=1) prefix = str(tmp_path / 'short_chain') sampler.save(prefix) - sampler2 = Sampler(f, [x], [y], [weights]) + sampler2 = Sampler(sp, sp, x, y, weights) loaded = sampler2.load_state(prefix) assert loaded.draws.shape[0] > 0 @pytest.mark.filterwarnings('ignore::UserWarning') def test_load_fingerprint_mismatch_warns(self, tmp_path, caplog): """Loading a chain into a sampler bound to different data warns.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) sampler.sample(samples=100, burn=20, thin=2) prefix = str(tmp_path / 'chain') @@ -401,7 +444,7 @@ def test_load_fingerprint_mismatch_warns(self, tmp_path, caplog): import logging other_y = y + 0.5 - sampler2 = Sampler(f, [x], [other_y], [weights]) + sampler2 = Sampler(sp, sp, x, other_y, weights) with caplog.at_level(logging.WARNING, logger='easyscience.fitting'): sampler2.load_state(prefix) assert 'does not match the data fingerprint' in caplog.text diff --git a/tests/integration/fitting/test_stateless_calculator.py b/tests/integration/fitting/test_stateless_calculator.py new file mode 100644 index 00000000..69f2c230 --- /dev/null +++ b/tests/integration/fitting/test_stateless_calculator.py @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Fitting through an external, stateless calculator. + +Core keeps the only copy of parameter state. A calculator reads the +model when asked to calculate and does not write back. These tests pin +that contract down with a plain class, without any core calculator +machinery. +""" + +import numpy as np +import pytest +from easy_test_models import CoefficientModel + +from easyscience import Minimizers +from easyscience import Parameter +from easyscience import global_object +from easyscience.fitting import Fitter +from easyscience.fitting.multi_fitter import MultiFitter + + +class PolynomialCalculator: + """ + A stateless calculator owned by a technique library. + + It keeps no copy of any parameter: every evaluation reads the + current values from the model it is handed. + """ + + def __init__(self): + self.n_calls = 0 + + def calculate(self, model: CoefficientModel, x: np.ndarray) -> np.ndarray: + self.n_calls += 1 + coefficients = [float(c.value) for c in model.coefficients] + return np.polyval(coefficients, x) + + def fit_func_for(self, model: CoefficientModel): + return lambda x: self.calculate(model, x) + + +@pytest.fixture(autouse=True) +def clear(): + global_object.registry.clear() + yield + global_object.registry.clear() + + +def noise(x: np.ndarray) -> np.ndarray: + # Small, reproducible noise so every engine can estimate uncertainties + return np.random.default_rng(0).normal(0.0, 0.01, size=x.shape) + + +def snapshot(model: CoefficientModel): + return [ + (p.unique_name, p.value, p.variance, p.min, p.max, p.fixed, p.independent) + for p in model.coefficients + ] + + +def test_calculation_reads_current_values_and_leaves_model_untouched(): + # When + model = CoefficientModel(coefficients=[2.0, 1.0]) + calculator = PolynomialCalculator() + x = np.linspace(0, 1, 5) + before = snapshot(model) + + # Then Expect + np.testing.assert_allclose(calculator.calculate(model, x), 2.0 * x + 1.0) + assert snapshot(model) == before + + # Then # Edits made through core are seen by the next evaluation + model.coefficients[0].value = 3.0 + + # Expect + np.testing.assert_allclose(calculator.calculate(model, x), 3.0 * x + 1.0) + + +@pytest.mark.parametrize('fit_engine', [Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO]) +def test_fit_with_dependent_parameter(fit_engine): + # When # The intercept is tied to the slope, so only the slope is fitted + slope = Parameter(1.0, display_name='slope', min=-10, max=10) + intercept = Parameter.from_dependency('0.5 * s', {'s': slope}, display_name='intercept') + model = CoefficientModel(coefficients=[slope, intercept]) + calculator = PolynomialCalculator() + x = np.linspace(0, 10, 50) + y = 3.0 * x + 1.5 + noise(x) + weights = np.ones_like(x) + + fitter = Fitter(model, calculator.fit_func_for(model)) + fitter.switch_minimizer(fit_engine) + + # Then + result = fitter.fit(x=x, y=y, weights=weights) + + # Expect + assert result.success + assert calculator.n_calls > 0 + assert [p.unique_name for p in model.get_free_parameters()] == [slope.unique_name] + assert slope.value == pytest.approx(3.0, abs=1e-2) + assert intercept.value == pytest.approx(0.5 * slope.value) + np.testing.assert_allclose(calculator.calculate(model, x), y, atol=0.1) + + +@pytest.mark.parametrize('fit_engine', [Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO]) +def test_multi_fit_with_shared_parameter(fit_engine): + # When # Two models share their intercept + shared = Parameter(0.0, display_name='shared', min=-10, max=10) + model_1 = CoefficientModel( + coefficients=[Parameter(1.0, display_name='slope_1', min=-10, max=10), shared] + ) + model_2 = CoefficientModel( + coefficients=[Parameter(1.0, display_name='slope_2', min=-10, max=10), shared] + ) + calculator = PolynomialCalculator() + x_1 = np.linspace(0, 5, 30) + x_2 = np.linspace(-5, 0, 40) + y_1 = 2.0 * x_1 - 1.0 + noise(x_1) + y_2 = -3.0 * x_2 - 1.0 + noise(x_2) + + fitter = MultiFitter( + [model_1, model_2], + [calculator.fit_func_for(model_1), calculator.fit_func_for(model_2)], + ) + fitter.switch_minimizer(fit_engine) + + # Then + results = fitter.fit( + x=[x_1, x_2], y=[y_1, y_2], weights=[np.ones_like(x_1), np.ones_like(x_2)] + ) + + # Expect + assert all(result.success for result in results) + assert len({p.unique_name for p in fitter.fit_object.get_free_parameters()}) == 3 + assert model_1.coefficients[0].value == pytest.approx(2.0, abs=1e-2) + assert model_2.coefficients[0].value == pytest.approx(-3.0, abs=1e-2) + assert shared.value == pytest.approx(-1.0, abs=1e-2) + assert model_1.coefficients[1] is model_2.coefficients[1] diff --git a/tests/unit/base_classes/test_collection_base.py b/tests/unit/base_classes/test_collection_base.py deleted file mode 100644 index 732c3acf..00000000 --- a/tests/unit/base_classes/test_collection_base.py +++ /dev/null @@ -1,545 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - -import easyscience -from easyscience import DescriptorNumber -from easyscience import ObjBase -from easyscience import Parameter -from easyscience import global_object -from easyscience.base_classes import CollectionBase - -test_dict = { - '@module': 'easyscience.base_classes', - '@class': 'CollectionBase', - '@version': easyscience.__version__, - 'name': 'testing', - 'data': [ - { - '@module': DescriptorNumber.__module__, - '@class': DescriptorNumber.__name__, - '@version': easyscience.__version__, - 'name': 'par1', - 'value': 1.0, - 'unit': 'dimensionless', - 'variance': None, - 'unique_name': 'CollectionBase_0', - 'description': '', - 'url': '', - 'display_name': 'par1', - } - ], -} - - -class Alpha(CollectionBase): - pass - - -class_constructors = [CollectionBase, Alpha] - - -@pytest.fixture -def setup_pars(): - d = { - 'name': 'test', - 'par1': Parameter('p1', 0.1, fixed=True), - 'des1': DescriptorNumber('d1', 0.1), - 'par2': Parameter('p2', 0.1), - 'des2': DescriptorNumber('d2', 0.1), - 'par3': Parameter('p3', 0.1), - } - return d - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_from_base(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - coll = cls(name, **setup_pars) - - assert coll.name == name - assert len(coll) == 5 - assert coll.user_data == {} - - for item, key in zip(coll, setup_pars.keys()): - assert item.name == setup_pars[key].name - assert item.value == setup_pars[key].value - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', range(1, 11)) -def test_CollectionBase_from_ObjBase(cls, setup_pars: dict, value: int): - name = setup_pars['name'] - del setup_pars['name'] - objs = {} - - prefix = 'obj' - for idx in range(value): - objs[prefix + str(idx)] = ObjBase(prefix + str(idx), **setup_pars) - - coll = cls(name, **objs) - - assert coll.name == name - assert len(coll) == value - assert coll.user_data == {} - - idx = 0 - for item, key in zip(coll, objs.keys()): - assert item.name == prefix + str(idx) - assert isinstance(item, objs[key].__class__) - idx += 1 - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', ('abc', False, (), [])) -def test_CollectionBase_create_fail(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - setup_pars['to_fail'] = value - - with pytest.raises(AttributeError): - coll = cls(name, **setup_pars) - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('key', ('user_data', '_kwargs', 'interface')) -def test_CollectionBase_create_fail2(cls, setup_pars, key): - name = setup_pars['name'] - del setup_pars['name'] - setup_pars[key] = DescriptorNumber('fail_name', 0) - - with pytest.raises(AttributeError): - coll = cls(name, **setup_pars) - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_append_base(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - - new_item_name = 'boo' - new_item_value = 100 - new_item = Parameter(new_item_name, new_item_value) - - coll = cls(name, **setup_pars) - n_before = len(coll) - - coll.append(new_item) - assert len(coll) == n_before + 1 - assert coll[-1].name == new_item_name - assert coll[-1].value == new_item_value - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', ('abc', False, (), [])) -def test_CollectionBase_append_fail(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - with pytest.raises(AttributeError): - coll.append(value) - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', (0, 1, 3, 'par1', 'des1')) -def test_CollectionBase_getItem(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - - get_item = coll[value] - if isinstance(value, str): - key = value - else: - key = list(setup_pars.keys())[value] - assert get_item.name == setup_pars[key].name - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', (False, [], (), 100, 100.4)) -def test_CollectionBase_getItem_type_fail(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - - with pytest.raises((IndexError, TypeError)): - get_item = coll[value] - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_getItem_slice(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - - get_item = coll[0:2] - assert len(get_item) == 2 - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', (0, 1, 3)) -def test_CollectionBase_setItem(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - n_coll = len(coll) - name_coll_idx = coll[value].name - - new_item_value = 100 - - coll[value] = new_item_value - - assert len(coll) == n_coll - assert coll[value].name == name_coll_idx - assert coll[value].value == new_item_value - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', ('abc', (), [])) -def test_CollectionBase_setItem_fail(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - - with pytest.raises(NotImplementedError): - for idx in range(len(coll)): - coll[idx] = value - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', (0, 1, 3)) -def test_CollectionBase_delItem(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - coll = cls(name, **setup_pars) - n_coll = len(coll) - # On del we should shift left - name_coll_idx = coll[value].name - name_coll_idxp = coll[value + 1].name - - del coll[value] - - assert len(coll) == n_coll - 1 - assert coll[value].name == name_coll_idxp - assert name_coll_idx not in [col.name for col in coll] - - -@pytest.mark.parametrize('cls', class_constructors) -@pytest.mark.parametrize('value', (0, 1, 3)) -def test_CollectionBase_len(cls, setup_pars, value): - name = setup_pars['name'] - del setup_pars['name'] - - keys = list(setup_pars.keys()) - keys = keys[0 : (value + 1)] - - coll = cls(name, **{key: setup_pars[key] for key in keys}) - assert len(coll) == (value + 1) - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_get_parameters(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = cls(name, **setup_pars) - pars = obj.get_parameters() - assert len(pars) == 3 - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_get_parameters_nested(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - - name2 = name + '_2' - obj2 = cls(name2, obj=obj, **setup_pars) - - pars = obj2.get_parameters() - assert len(pars) == 6 - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_get_fit_parameters(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = cls(name, **setup_pars) - pars = obj.get_fit_parameters() - assert len(pars) == 2 - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_get_fit_parameters_nested(cls, setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - - name2 = name + '_2' - obj2 = cls(name2, obj=obj, **setup_pars) - - pars = obj2.get_fit_parameters() - assert len(pars) == 4 - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_dir(cls): - name = 'testing' - kwargs = {'p1': DescriptorNumber('par1', 1)} - obj = cls(name, **kwargs) - d = set(dir(obj)) - - expected = { - 'user_data', - 'reverse', - 'constraints', - 'get_fit_parameters', - 'append', - 'unique_name', - 'index', - 'as_dict', - 'clear', - 'extend', - 'encode', - 'remove', - 'interface', - 'from_dict', - 'name', - 'switch_interface', - 'get_parameters', - 'insert', - 'data', - 'pop', - 'count', - 'generate_bindings', - 'decode', - 'sort', - } - assert not d.difference(expected) - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_as_dict(cls): - name = 'testing' - kwargs = {'p1': DescriptorNumber('par1', 1)} - obj = cls(name, **kwargs) - d = obj.as_dict() - - def check_dict(dict_1: dict, dict_2: dict): - keys_1 = list(dict_1.keys()) - keys_2 = list(dict_2.keys()) - if 'unique_name' in keys_1: - del keys_1[keys_1.index('unique_name')] - if 'unique_name' in keys_2: - del keys_2[keys_2.index('unique_name')] - - assert not set(keys_1).difference(set(keys_2)) - - def testit(item1, item2): - if isinstance(item1, dict) and isinstance(item2, dict): - check_dict(item1, item2) - elif isinstance(item1, list) and isinstance(item2, list): - for v1, v2 in zip(item1, item2): - testit(v1, v2) - else: - if isinstance(item1, str) and isinstance(item2, str): - assert item1 == item2 - elif isinstance(item1, float) and isinstance(item2, float): - assert item1 == item2 - else: - assert item1 is item2 - - keys_1 = list(keys_1) - keys_1.sort() - keys_2 = list(keys_2) - keys_2.sort() - - for k1, k2 in zip(keys_1, keys_2): - if k1[0] == '@': - continue - testit(dict_1[k1], dict_2[k2]) - - check_dict(d, test_dict) - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_from_dict(cls): - global_object.map._clear() # TODO: figure out why this test fails without this line - name = 'testing' - kwargs = {'p1': DescriptorNumber('par1', 1)} - expected = cls.from_dict(test_dict) - ref = cls(name, **kwargs) - - assert ref.name == expected.name - assert len(ref) == len(expected) - for item1, item2 in zip(ref, expected): - assert item1.name == item2.name - assert item1.value == item2.value - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_repr(cls): - name = 'test' - p1 = Parameter('p1', 1) - obj = cls(name, p1) - test_str = str(obj) - ref_str = f'{cls.__name__} `{name}` of length 1' - assert test_str == ref_str - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_iterator(cls): - name = 'test' - p1 = Parameter('p1', 1) - p2 = Parameter('p2', 2) - p3 = Parameter('p3', 3) - p4 = Parameter('p4', 4) - - l_object = [p1, p2, p3, p4] - - obj = cls(name, *l_object) - - for index, item in enumerate(obj): - assert item == l_object[index] - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_iterator_dict(cls): - global_object.map._clear() # TODO: figure out why this test fails without this line - name = 'test' - p1 = Parameter('p1', 1) - p2 = Parameter('p2', 2) - p3 = Parameter('p3', 3) - p4 = Parameter('p4', 4) - - l_object = [p1, p2, p3, p4] - - obj = cls(name, *l_object) - d = obj.as_dict() - global_object.map._clear() - obj2 = cls.from_dict(d) - - for index, item in enumerate(obj2): - assert item.value == l_object[index].value - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_sameName(cls): - global_object.map._clear() # TODO: figure out why this test fails without this line - name = 'test' - p1 = Parameter('p1', 1) - p2 = Parameter('p1', 2) - p3 = Parameter('p3', 3) - p4 = Parameter('p4', 4) - - l_object = [p1, p2, p3, p4] - obj = cls(name, *l_object) - assert len(l_object) == len(obj) - for index, item in enumerate(obj): - assert item == l_object[index] - - obj12 = obj['p1'] - assert len(obj12) == 2 - for index, item in enumerate(obj12): - assert item == l_object[index] - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_set_index(cls): - name = 'test' - p1 = Parameter('p1', 1) - p2 = Parameter('p1', 2) - p3 = Parameter('p3', 3) - p4 = Parameter('p4', 4) - - l_object = [p1, p2, p3] - obj = cls(name, *l_object) - - idx = 1 - assert obj[idx] == p2 - obj[idx] = p4 - assert obj[idx] == p4 - edges = obj._global_object.map.get_edges(obj) - assert len(edges) == len(obj) - for item in obj: - assert item.unique_name in edges - assert p2.unique_name not in edges - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_set_index_based(cls): - name = 'test' - p1 = Parameter('p1', 1) - p2 = Parameter('p2', 2) - p3 = Parameter('p3', 3) - p4 = Parameter('p4', 4) - p5 = Parameter('p5', 5) - d = cls('testing', p1, p2) - - l_object = [p3, p4, p5] - obj = cls(name, *l_object) - - idx = 1 - assert obj[idx] == p4 - obj[idx] = d - assert obj[idx] == d - edges = obj._global_object.map.get_edges(obj) - assert len(edges) == len(obj) - for item in obj: - assert item.unique_name in edges - assert p4.unique_name not in edges - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_sort(cls): - name = 'test' - v = [1, 4, 3, 2, 5] - expected = [1, 2, 3, 4, 5] - d = cls(name, *[Parameter(f'p{i}', v[i]) for i in range(len(v))]) - d.sort(lambda x: x.value) - for i, item in enumerate(d): - assert item.value == expected[i] - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBase_sort_reverse(cls): - name = 'test' - v = [1, 4, 3, 2, 5] - expected = [1, 2, 3, 4, 5] - expected.reverse() - d = cls(name, *[Parameter(f'p{i}', v[i]) for i in range(len(v))]) - d.sort(lambda x: x.value, reverse=True) - for i, item in enumerate(d): - assert item.value == expected[i] - - -class Beta(ObjBase): - pass - - -@pytest.mark.parametrize('cls', class_constructors) -def test_CollectionBaseGraph(cls): - from easyscience import global_object - - G = global_object.map - name = 'test' - v = [1, 2] - p = [Parameter(f'p{i}', v[i]) for i in range(len(v))] - p_id = [_p.unique_name for _p in p] - bb = cls(name, *p) - bb_id = bb.unique_name - b = Beta('b', bb=bb) - b_id = b.unique_name - for _id in p_id: - assert _id in G.get_edges(bb) - assert len(p) == len(G.get_edges(bb)) - assert bb_id in G.get_edges(b) - assert 1 == len(G.get_edges(b)) diff --git a/tests/unit/base_classes/test_deprecated_wrappers.py b/tests/unit/base_classes/test_deprecated_wrappers.py deleted file mode 100644 index 492fc387..00000000 --- a/tests/unit/base_classes/test_deprecated_wrappers.py +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -"""Tests for the deprecated wrapper modules in easyscience.base_classes. - -These modules now only emit log warnings and re-export from -easyscience.legacy. We test that the log messages are raised and that -the classes are still usable. -""" - -import importlib -import logging - -import pytest - -from easyscience import global_object - - -@pytest.fixture(autouse=True) -def _clear_map(): - """Clear the global object map before and after each test.""" - global_object.map._clear() - yield - global_object.map._clear() - - -# --------------------------------------------------------------------------- -# Deprecated wrapper: easyscience.base_classes.collection_base -# --------------------------------------------------------------------------- - - -def test_import_collection_base_warns(caplog: 'pytest.LogCaptureFixture'): - """Importing easyscience.base_classes.collection_base logs a WARNING.""" - import easyscience.base_classes.collection_base - - with caplog.at_level(logging.WARNING, logger='easyscience'): - importlib.reload(easyscience.base_classes.collection_base) - - assert len(caplog.records) >= 1, 'Expected at least one log record' - messages = [r.message for r in caplog.records] - combined = ' '.join(messages) - assert 'deprecated' in combined.lower() - assert 'legacy.collection_base' in combined - - -def test_collection_base_still_works_from_deprecated(caplog: 'pytest.LogCaptureFixture'): - """The class imported from the deprecated wrapper still works.""" - with caplog.at_level(logging.ERROR, logger='easyscience'): - from easyscience.base_classes.collection_base import CollectionBase # noqa: F811 - - from easyscience import Parameter - - p = Parameter('p1', 1.0) - coll = CollectionBase('test', p) - assert len(coll) == 1 - assert coll[0].name == 'p1' - - -# --------------------------------------------------------------------------- -# Deprecated wrapper: easyscience.base_classes.obj_base -# --------------------------------------------------------------------------- - - -def test_import_obj_base_warns(caplog: 'pytest.LogCaptureFixture'): - """Importing easyscience.base_classes.obj_base logs a WARNING.""" - import easyscience.base_classes.obj_base - - with caplog.at_level(logging.WARNING, logger='easyscience'): - importlib.reload(easyscience.base_classes.obj_base) - - assert len(caplog.records) >= 1, 'Expected at least one log record' - messages = [r.message for r in caplog.records] - combined = ' '.join(messages) - assert 'deprecated' in combined.lower() - assert 'legacy.obj_base' in combined - - -def test_obj_base_still_works_from_deprecated(caplog: 'pytest.LogCaptureFixture'): - """The class imported from the deprecated wrapper still works.""" - with caplog.at_level(logging.ERROR, logger='easyscience'): - from easyscience.base_classes.obj_base import ObjBase # noqa: F811 - - from easyscience import Parameter - - p = Parameter('p1', 1.0) - obj = ObjBase('test', p1=p) - assert obj.p1.value == 1.0 diff --git a/tests/unit/base_classes/test_new_base.py b/tests/unit/base_classes/test_easy_base.py similarity index 55% rename from tests/unit/base_classes/test_new_base.py rename to tests/unit/base_classes/test_easy_base.py index 7d74cac2..338d3be5 100644 --- a/tests/unit/base_classes/test_new_base.py +++ b/tests/unit/base_classes/test_easy_base.py @@ -4,79 +4,92 @@ import pytest from easyscience import global_object -from easyscience.base_classes import NewBase +from easyscience.base_classes import EasyBase -class NestedNewBase(NewBase): - def __init__(self, unique_name=None, display_name=None, nested_new_base=None): +class NestedEasyBase(EasyBase): + def __init__(self, unique_name=None, display_name=None, nested_easy_base=None): super().__init__(unique_name=unique_name, display_name=display_name) - self.nested_new_base = nested_new_base or NewBase() + self.nested_easy_base = nested_easy_base or EasyBase() -class TestNewBase: +class TestEasyBase: @pytest.fixture def clear(self): - # Clear the global object map before each test - global_object.map._clear() + # Clear the global object registry before each test + global_object.registry.clear() def test_constructor_defaults(self, clear): # When Then - obj = NewBase() + obj = EasyBase() # Expect - assert obj._unique_name == 'NewBase_0' + assert obj._unique_name == 'EasyBase_0' assert obj._default_unique_name is True assert obj._display_name is None - assert global_object.map.get_item_by_key('NewBase_0') is obj + assert global_object.registry['EasyBase_0'] is obj def test_constructor_with_arguments(self): # When Then - obj = NewBase(unique_name='custom_name', display_name='My Object') + obj = EasyBase(unique_name='custom_name', display_name='My Object') # Expect assert obj._unique_name == 'custom_name' assert obj._default_unique_name is False assert obj._display_name == 'My Object' - assert global_object.map.get_item_by_key('custom_name') is obj + assert global_object.registry['custom_name'] is obj def test_constructor_invalid_unique_name(self): # When Then Expect with pytest.raises(TypeError, match='Unique name has to be a string.'): - NewBase(unique_name=123) + EasyBase(unique_name=123) def test_constructor_invalid_display_name(self): # When Then Expect with pytest.raises(TypeError, match='Display name must be a string or None'): - NewBase(display_name=456) + EasyBase(display_name=456) - def test_arg_spec(self): + def test_no_arg_spec_override(self): # When - obj = NewBase() + obj = EasyBase() + # Expect + assert not hasattr(obj, '_arg_spec') + + def test_to_dict_includes_keyword_only_args(self, clear): + # When + class KeywordOnly(EasyBase): + def __init__(self, value, *args, unit='', unique_name=None, **kwargs): + super().__init__(unique_name=unique_name) + self.value = value + self.unit = unit + + obj = KeywordOnly(1.0, unit='m', unique_name='kw') # Then - arg_spec = obj._arg_spec + d = obj.to_dict() # Expect - assert 'unique_name' in arg_spec - assert 'display_name' in arg_spec + assert d['value'] == 1.0 + assert d['unit'] == 'm' + assert d['unique_name'] == 'kw' def test_unique_name_setter(self, clear): # When - obj = NewBase() + obj = EasyBase() # Then obj.unique_name = 'new_unique_name' # Expect assert obj.unique_name == 'new_unique_name' assert obj._default_unique_name is False - assert global_object.map.get_item_by_key('new_unique_name') is obj - assert global_object.map.get_item_by_key('NewBase_0') is obj + assert global_object.registry['new_unique_name'] is obj + assert 'EasyBase_0' not in global_object.registry def test_unique_name_setter_invalid(self): # When - obj = NewBase() + obj = EasyBase() # Then Expect with pytest.raises(TypeError, match='Unique name has to be a string.'): obj.unique_name = 789 def test_default_display_name(self): # When - obj = NewBase() + obj = EasyBase() # Then display_name = obj.display_name # Expect @@ -85,7 +98,7 @@ def test_default_display_name(self): def test_display_name_setter(self): # When - obj = NewBase() + obj = EasyBase() # Then obj.display_name = 'Pretty Name' # Expect @@ -94,102 +107,129 @@ def test_display_name_setter(self): def test_display_name_setter_invalid(self): # When - obj = NewBase() + obj = EasyBase() # Then Expect with pytest.raises(TypeError, match='Display name must be a string or None'): obj.display_name = 101112 def test_to_dict_full_params(self): # When - obj = NewBase(unique_name='test_name', display_name='Test Object') + obj = EasyBase(unique_name='test_name', display_name='Test Object') # Then obj_dict = obj.to_dict() # Expect assert isinstance(obj_dict, dict) assert obj_dict['unique_name'] == 'test_name' assert obj_dict['display_name'] == 'Test Object' - assert obj_dict['@module'] == 'easyscience.base_classes.new_base' - assert obj_dict['@class'] == 'NewBase' + assert obj_dict['@module'] == 'easyscience.base_classes.easy_base' + assert obj_dict['@class'] == 'EasyBase' assert '@version' in obj_dict def test_to_dict_default_params(self): # When - obj = NewBase() + obj = EasyBase() # Then obj_dict = obj.to_dict() # Expect assert isinstance(obj_dict, dict) - assert obj_dict['@module'] == 'easyscience.base_classes.new_base' - assert obj_dict['@class'] == 'NewBase' + assert obj_dict['@module'] == 'easyscience.base_classes.easy_base' + assert obj_dict['@class'] == 'EasyBase' assert '@version' in obj_dict assert 'unique_name' not in obj_dict assert 'display_name' not in obj_dict + def test_to_dict_does_not_mutate_skip(self): + obj = EasyBase() + skip = ['foo'] + obj.to_dict(skip=skip) + assert skip == ['foo'] + + def test_to_dict_nested_object_keeps_own_names(self): + """A parent without names must not strip names from a nested object.""" + + class Holder(EasyBase): + def __init__(self, item=None, unique_name=None, display_name=None): + super().__init__(unique_name=unique_name, display_name=display_name) + self._item = item + + @property + def item(self): + return self._item + + inner = EasyBase(unique_name='inner', display_name='Inner') + outer = Holder(item=inner) # auto unique_name, no display_name + + d = outer.to_dict() + + assert 'unique_name' not in d + assert 'display_name' not in d + assert d['item']['unique_name'] == 'inner' + assert d['item']['display_name'] == 'Inner' + def test_to_dict_with_skip(self): # When - obj = NewBase(unique_name='skip_test', display_name='Skip Test Object') + obj = EasyBase(unique_name='skip_test', display_name='Skip Test Object') # Then obj_dict = obj.to_dict(skip=['display_name']) # Expect assert isinstance(obj_dict, dict) assert obj_dict['unique_name'] == 'skip_test' assert 'display_name' not in obj_dict - assert obj_dict['@module'] == 'easyscience.base_classes.new_base' - assert obj_dict['@class'] == 'NewBase' + assert obj_dict['@module'] == 'easyscience.base_classes.easy_base' + assert obj_dict['@class'] == 'EasyBase' assert '@version' in obj_dict - def test_to_dict_nested_new_base(self): + def test_to_dict_nested_easy_base(self): # When - obj = NewBase() - parent_obj = NestedNewBase(unique_name='parent', display_name='Parent Object') - parent_obj.nested_new_base = obj + obj = EasyBase() + parent_obj = NestedEasyBase(unique_name='parent', display_name='Parent Object') + parent_obj.nested_easy_base = obj # Then obj_dict = parent_obj.to_dict() # Expect assert isinstance(obj_dict, dict) - assert 'nested_new_base' in obj_dict + assert 'nested_easy_base' in obj_dict assert 'unique_name' in obj_dict assert 'display_name' in obj_dict - assert 'unique_name' not in obj_dict['nested_new_base'] - assert 'display_name' not in obj_dict['nested_new_base'] + assert 'unique_name' not in obj_dict['nested_easy_base'] + assert 'display_name' not in obj_dict['nested_easy_base'] - def test_to_dict_nested_new_base_reverse(self): + def test_to_dict_nested_easy_base_reverse(self): # When - obj = NewBase(unique_name='child', display_name='Child Object') - parent_obj = NestedNewBase() - parent_obj.nested_new_base = obj + obj = EasyBase(unique_name='child', display_name='Child Object') + parent_obj = NestedEasyBase() + parent_obj.nested_easy_base = obj # Then obj_dict = parent_obj.to_dict() # Expect assert isinstance(obj_dict, dict) - assert 'nested_new_base' in obj_dict + assert 'nested_easy_base' in obj_dict assert 'unique_name' not in obj_dict assert 'display_name' not in obj_dict - assert 'unique_name' in obj_dict['nested_new_base'] - assert 'display_name' in obj_dict['nested_new_base'] - + assert 'unique_name' in obj_dict['nested_easy_base'] + assert 'display_name' in obj_dict['nested_easy_base'] def test_from_dict(self): # When obj_dict = { - '@module': 'easyscience.base_classes.new_base', - '@class': 'NewBase', + '@module': 'easyscience.base_classes.easy_base', + '@class': 'EasyBase', 'unique_name': 'from_dict_name', 'display_name': 'From Dict Object', } # Then - obj = NewBase.from_dict(obj_dict) + obj = EasyBase.from_dict(obj_dict) # Expect - assert isinstance(obj, NewBase) + assert isinstance(obj, EasyBase) assert obj.unique_name == 'from_dict_name' assert obj.display_name == 'From Dict Object' - assert global_object.map.get_item_by_key('from_dict_name') is obj + assert global_object.registry['from_dict_name'] is obj def test_from_dict_not_easyscience(self): # When obj_dict = { '@module': 'some.other.module', - '@class': 'NewBase', + '@class': 'EasyBase', 'unique_name': 'invalid_from_dict', 'display_name': 'Invalid From Dict Object', } @@ -197,12 +237,12 @@ def test_from_dict_not_easyscience(self): with pytest.raises( ValueError, match='Input must be a dictionary representing an EasyScience object.' ): - NewBase.from_dict(obj_dict) + EasyBase.from_dict(obj_dict) def test_from_dict_wrong_class(self): # When obj_dict = { - '@module': 'easyscience.base_classes.new_base', + '@module': 'easyscience.base_classes.easy_base', '@class': 'SomeOtherClass', 'unique_name': 'wrong_class_name', 'display_name': 'Wrong Class Object', @@ -210,13 +250,13 @@ def test_from_dict_wrong_class(self): # Then Expect with pytest.raises( ValueError, - match='Class name in dictionary does not match the expected class: NewBase.', + match='Class name in dictionary does not match the expected class: EasyBase.', ): - NewBase.from_dict(obj_dict) + EasyBase.from_dict(obj_dict) def test__dir__(self): # When - obj = NewBase() + obj = EasyBase() dir_list = dir(obj) # Then expected_attributes = [ @@ -231,24 +271,24 @@ def test__dir__(self): def test_copy(self): # When - obj = NewBase(unique_name='original_name', display_name='Original Object') + obj = EasyBase(unique_name='original_name', display_name='Original Object') # Then obj_copy = obj.__copy__() # Expect - assert isinstance(obj_copy, NewBase) + assert isinstance(obj_copy, EasyBase) assert obj_copy.unique_name != obj.unique_name assert obj_copy.display_name == obj.display_name - assert global_object.map.get_item_by_key(obj_copy.unique_name) is obj_copy + assert global_object.registry[obj_copy.unique_name] is obj_copy def test_deepcopy(self): # When - obj = NewBase(unique_name='deepcopy_name', display_name='Deep Copy Object') + obj = EasyBase(unique_name='deepcopy_name', display_name='Deep Copy Object') # Then import copy obj_deepcopy = copy.deepcopy(obj) # Expect - assert isinstance(obj_deepcopy, NewBase) + assert isinstance(obj_deepcopy, EasyBase) assert obj_deepcopy.unique_name != obj.unique_name assert obj_deepcopy.display_name == obj.display_name - assert global_object.map.get_item_by_key(obj_deepcopy.unique_name) is obj_deepcopy + assert global_object.registry[obj_deepcopy.unique_name] is obj_deepcopy diff --git a/tests/unit/base_classes/test_easy_list.py b/tests/unit/base_classes/test_easy_list.py index 978d5571..9c776501 100644 --- a/tests/unit/base_classes/test_easy_list.py +++ b/tests/unit/base_classes/test_easy_list.py @@ -7,23 +7,23 @@ import pytest from easyscience import global_object +from easyscience.base_classes.easy_base import EasyBase from easyscience.base_classes.easy_list import EasyList from easyscience.base_classes.model_base import ModelBase -from easyscience.base_classes.new_base import NewBase -from easyscience.io.serializer_base import SerializerBase +from easyscience.base_classes.serializer_base import SerializerBase from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter -class Alpha(NewBase): - """Concrete subclass of NewBase for testing.""" +class Alpha(EasyBase): + """Concrete subclass of EasyBase for testing.""" def __init__(self, unique_name=None, display_name=None): super().__init__(unique_name=unique_name, display_name=display_name) -class Beta(NewBase): - """Another concrete subclass of NewBase for testing.""" +class Beta(EasyBase): + """Another concrete subclass of EasyBase for testing.""" def __init__(self, unique_name=None, display_name=None): super().__init__(unique_name=unique_name, display_name=display_name) @@ -32,10 +32,12 @@ def __init__(self, unique_name=None, display_name=None): class MockModel(ModelBase): """A ModelBase subclass with a Parameter and a DescriptorNumber for testing get_all_variables.""" - def __init__(self, unique_name=None, display_name=None, temperature=25, volume=1.0, items=None): + def __init__( + self, unique_name=None, display_name=None, temperature=25, volume=1.0, items=None + ): super().__init__(unique_name=unique_name, display_name=display_name) - self._temperature = Parameter(name='temperature', value=temperature) - self._volume = DescriptorNumber(name='volume', value=volume) + self._temperature = Parameter(display_name='temperature', value=temperature) + self._volume = DescriptorNumber(display_name='volume', value=volume) if items is None: items = EasyList() self._items = items @@ -62,7 +64,7 @@ class MockModelNested(ModelBase): def __init__(self, unique_name=None, display_name=None, component=None, pressure=0): super().__init__(unique_name=unique_name, display_name=display_name) - self._pressure = Parameter(name='pressure', value=pressure) + self._pressure = Parameter(display_name='pressure', value=pressure) self._component = component or MockModel(unique_name='inner', temperature=30, volume=2.0) @property @@ -81,7 +83,7 @@ def component(self): class TestEasyList: @pytest.fixture(autouse=True) def clear(self): - global_object.map._clear() + global_object.registry.clear() @pytest.fixture def populated_list(self): @@ -96,7 +98,7 @@ def test_empty_init(self): el = EasyList() assert len(el) == 0 assert len(el._protected_types) == 1 - assert el._protected_types[0] == NewBase + assert el._protected_types[0] == EasyBase def test_init_with_items(self): a = Alpha(unique_name='a1') @@ -120,11 +122,11 @@ def test_init_list_of_protected_types(self): assert el._protected_types == [Alpha, Beta] def test_init_invalid_protected_types_raises(self): - with pytest.raises(TypeError, match='protected_types must be a NewBase subclass'): + with pytest.raises(TypeError, match='protected_types must be an EasyBase subclass'): EasyList(protected_types=int) def test_init_invalid_protected_types_list_raises(self): - with pytest.raises(TypeError, match='protected_types must be a NewBase subclass'): + with pytest.raises(TypeError, match='protected_types must be an EasyBase subclass'): EasyList(protected_types=[int, str]) def test_init_with_data_kwarg(self): @@ -146,10 +148,10 @@ def test_append_correct_type_succeeds(self): el.append(a) assert len(el) == 1 - def test_append_non_newbase_raises(self): + def test_append_non_easybase_raises(self): el = EasyList() with pytest.raises(TypeError, match='Items must be one of'): - el.append('not a NewBase') + el.append('not an EasyBase') def test_insert_wrong_type_raises(self): el = EasyList(protected_types=Alpha) @@ -555,7 +557,7 @@ def test_to_dict(self): assert d['protected_types'][0]['@class'] == 'Alpha' def test_to_dict_default_protected_type_not_serialized(self): - a1 = NewBase(unique_name='nb1') + a1 = EasyBase(unique_name='nb1') el = EasyList(a1, unique_name='my_list') d = el.to_dict() assert 'protected_types' not in d @@ -588,19 +590,96 @@ def test_from_dict_round_trip(self): a2 = Alpha(unique_name='a2') el = EasyList(a1, a2, unique_name='my_list', protected_types=Alpha) d = el.to_dict() - # Clear the global map so deserialized objects can reuse the same unique names - global_object.map._clear() + # Clear the global registry so deserialized objects can reuse the same unique names + global_object.registry.clear() el2 = EasyList.from_dict(d) assert len(el2) == 2 assert el2[0].unique_name == 'a1' assert el2[1].unique_name == 'a2' assert d == el2.to_dict() # The dicts should be the same after round trip + def test_to_dict_forwards_skip_to_items(self): + a1 = Alpha(unique_name='a1', display_name='first') + el = EasyList(a1, unique_name='my_list', protected_types=Alpha) + + d = el.to_dict(skip=['display_name']) + + assert 'display_name' not in d['data'][0] + assert d['data'][0]['unique_name'] == 'a1' + + def test_to_dict_unnamed_list_keeps_item_names(self): + """A list without its own names must not strip names from its items.""" + a1 = Alpha(unique_name='a1', display_name='first') + el = EasyList(a1, protected_types=Alpha) # auto unique_name, no display_name + + d = el.to_dict() + + assert 'unique_name' not in d + assert 'display_name' not in d + assert d['data'][0]['unique_name'] == 'a1' + assert d['data'][0]['display_name'] == 'first' + + def test_to_dict_does_not_mutate_skip(self): + el = EasyList(Alpha(unique_name='a1'), protected_types=Alpha) + skip = ['display_name'] + + el.to_dict(skip=skip) + + assert skip == ['display_name'] + + def test_to_dict_without_skip(self): + """``to_dict`` works when called without ``skip``.""" + a1 = Alpha(unique_name='a1') + el = EasyList(a1, protected_types=Alpha) + + d = el.to_dict() + + assert d['data'][0]['unique_name'] == 'a1' + assert d['protected_types'][0]['@class'] == 'Alpha' + assert not hasattr(el, '_convert_to_dict') + + def test_to_dict_nested_list_uses_override(self): + """A list held by another object is serialized by its own ``to_dict``.""" + inner = EasyList(Alpha(unique_name='a1'), protected_types=Alpha) + outer = EasyList(inner, unique_name='outer') + + d = outer.to_dict() + + nested = d['data'][0] + assert nested['@class'] == 'EasyList' + assert nested['protected_types'][0]['@class'] == 'Alpha' + assert nested['data'][0]['unique_name'] == 'a1' + + global_object.registry.clear() + outer2 = EasyList.from_dict(d) + assert isinstance(outer2[0], EasyList) + assert outer2[0]._protected_types == [Alpha] + assert outer2[0][0].unique_name == 'a1' + + def test_from_dict_round_trip_keyword_only_arguments(self): + a1 = Alpha(unique_name='a1') + el = EasyList(a1, unique_name='my_list', display_name='My list', protected_types=Alpha) + d = el.to_dict() + assert d['unique_name'] == 'my_list' + assert d['display_name'] == 'My list' + global_object.registry.clear() + el2 = EasyList.from_dict(d) + assert el2.unique_name == 'my_list' + assert el2.display_name == 'My list' + assert el2._protected_types == [Alpha] + + def test_to_dict_skip(self): + a1 = Alpha(unique_name='a1') + el = EasyList(a1, unique_name='my_list', display_name='My list', protected_types=Alpha) + d = el.to_dict(skip=['display_name']) + assert 'display_name' not in d + assert 'protected_types' in d + def test_from_dict_round_trip_empty(self): el = EasyList(unique_name='my_list', protected_types=Alpha) d = el.to_dict() - # Clear the global map so deserialized objects can reuse the same unique names - global_object.map._clear() + # Clear the global registry so deserialized objects can reuse the same unique names + global_object.registry.clear() el2 = EasyList.from_dict(d) assert len(el2) == 0 assert d == el2.to_dict() # The dicts should be the same after round trip @@ -630,8 +709,8 @@ def test_from_dict_modelbase_round_trip(self, monkeypatch): ) # Then list_dict = el.to_dict() - # Clear the global map so deserialized objects can reuse the same unique names - global_object.map._clear() + # Clear the global registry so deserialized objects can reuse the same unique names + global_object.registry.clear() el2 = EasyList.from_dict(list_dict) # Expect assert len(el2) == 2 @@ -643,9 +722,12 @@ def test_from_dict_missing_data_key(self): el_dict = { '@module': 'easyscience', '@class': 'EasyList', - 'protected_types': [{'@module': 'easyscience.base_classes', '@class': 'ModelBase'}] + 'protected_types': [{'@module': 'easyscience.base_classes', '@class': 'ModelBase'}], } - with pytest.raises(ValueError, match='The provided dictionary does not represent an EasyList. Missing the "data" key.'): + with pytest.raises( + ValueError, + match='The provided dictionary does not represent an EasyList. Missing the "data" key.', + ): EasyList.from_dict(el_dict) # --- get_all_variables --- @@ -656,9 +738,9 @@ def test_get_all_variables_empty_list(self): assert el.get_all_variables() == [] def test_get_all_variables_no_modelbase_elements(self): - """An EasyList with only plain NewBase elements (not ModelBase) should return an empty list.""" + """An EasyList with only plain EasyBase elements (not ModelBase) should return an empty list.""" a1 = Alpha(unique_name='a1') - a2 = NewBase(unique_name='nb1') + a2 = EasyBase(unique_name='nb1') el = EasyList(a1, a2) assert el.get_all_variables() == [] @@ -668,13 +750,13 @@ def test_get_all_variables_single_modelbase(self): el = EasyList(m1, protected_types=ModelBase) vars = el.get_all_variables() assert len(vars) == 2 - names = {v.name for v in vars} + names = {v.display_name for v in vars} assert 'temperature' in names assert 'volume' in names # Verify specific values - temp_var = next(v for v in vars if v.name == 'temperature') + temp_var = next(v for v in vars if v.display_name == 'temperature') assert temp_var.value == 10 - vol_var = next(v for v in vars if v.name == 'volume') + vol_var = next(v for v in vars if v.display_name == 'volume') assert vol_var.value == 5.0 def test_get_all_variables_multiple_modelbase(self): @@ -684,17 +766,17 @@ def test_get_all_variables_multiple_modelbase(self): el = EasyList(m1, m2, protected_types=ModelBase) vars = el.get_all_variables() assert len(vars) == 4 - names = {v.name for v in vars} + names = {v.display_name for v in vars} assert names == {'temperature', 'volume'} def test_get_all_variables_mixed_elements(self): - """Only ModelBase-derived elements contribute variables; plain NewBase elements are skipped.""" + """Only ModelBase-derived elements contribute variables; plain EasyBase elements are skipped.""" m1 = MockModel(unique_name='m1', temperature=10, volume=5.0) a1 = Alpha(unique_name='a1') el = EasyList(m1, a1) vars = el.get_all_variables() assert len(vars) == 2 - names = {v.name for v in vars} + names = {v.display_name for v in vars} assert names == {'temperature', 'volume'} def test_get_all_variables_nested_model(self): @@ -705,7 +787,7 @@ def test_get_all_variables_nested_model(self): vars = el.get_all_variables() # parent: pressure (Parameter), inner: temperature (Parameter), volume (DescriptorNumber) assert len(vars) == 3 - names = {v.name for v in vars} + names = {v.display_name for v in vars} assert names == {'pressure', 'temperature', 'volume'} def test_get_all_variables_returns_descriptorbase_instances(self): @@ -718,10 +800,70 @@ def test_get_all_variables_returns_descriptorbase_instances(self): assert isinstance(v, DescriptorBase) + def test_get_all_variables_bare_parameters(self): + """Bare Parameters passed to the constructor should be collected directly.""" + p1 = Parameter(value=1.0, display_name='a') + p2 = Parameter(value=2.0, display_name='b') + el = EasyList(p1, p2) + vars = el.get_all_variables() + assert vars == [p1, p2] + assert el.get_free_parameters() == [p1, p2] + + def test_get_all_variables_bare_descriptors(self): + """Bare DescriptorNumbers are collected as variables but are not fit parameters.""" + p1 = Parameter(value=1.0, display_name='a') + d1 = DescriptorNumber(value=2.0, display_name='b') + el = EasyList(p1, d1) + assert el.get_all_variables() == [p1, d1] + assert el.get_all_parameters() == [p1] + assert el.get_free_parameters() == [p1] + + def test_get_all_variables_fixed_parameter_not_fitted(self): + """A fixed bare Parameter is returned as a variable but not as a fit parameter.""" + free = Parameter(value=1.0, display_name='a') + fixed = Parameter(value=2.0, fixed=True, display_name='b') + el = EasyList(free, fixed) + assert el.get_all_variables() == [free, fixed] + assert el.get_free_parameters() == [free] + + def test_get_all_variables_parameters_from_list(self): + """A plain list of Parameters should be flattened and collected.""" + p1 = Parameter(value=1.0, display_name='a') + p2 = Parameter(value=2.0, display_name='b') + el = EasyList([p1, p2]) + assert len(el) == 2 + assert el.get_all_variables() == [p1, p2] + + def test_get_all_variables_parameters_from_tuple(self): + """A plain tuple of Parameters should be flattened and collected.""" + p1 = Parameter(value=1.0, display_name='a') + p2 = Parameter(value=2.0, display_name='b') + el = EasyList((p1, p2)) + assert len(el) == 2 + assert el.get_all_variables() == [p1, p2] + + def test_get_all_variables_bare_parameters_and_model(self): + """Bare Parameters and ModelBase items should both contribute variables.""" + p1 = Parameter(value=1.0, display_name='a') + m1 = MockModel(unique_name='m1', temperature=10, volume=5.0) + el = EasyList(p1, m1) + vars = el.get_all_variables() + assert len(vars) == 3 + assert vars[0] is p1 + assert {v.display_name for v in vars[1:]} == {'temperature', 'volume'} + + def test_get_all_variables_bare_parameters_in_nested_easylist(self): + """Bare Parameters inside a nested EasyList should be collected by the outer list.""" + p1 = Parameter(value=1.0, display_name='a') + p2 = Parameter(value=2.0, display_name='b') + inner_list = EasyList(p1) + outer_list = EasyList(inner_list, p2) + assert outer_list.get_all_variables() == [p1, p2] + def test_get_all_variables_nested_easylist(self): - """An EasyList containing another EasyList with mixed NewBase/ModelBase elements + """An EasyList containing another EasyList with mixed EasyBase/ModelBase elements should collect variables from the inner EasyList's ModelBase items, - skipping plain NewBase items.""" + skipping plain EasyBase items.""" inner_model = MockModel(unique_name='inner_m', temperature=50, volume=3.0) inner_plain = Alpha(unique_name='inner_a') inner_list = EasyList(inner_model, inner_plain) @@ -751,7 +893,7 @@ def test_get_all_variables_nested_easylist(self): assert isinstance(v, DescriptorNumber) # Collect temperatures and volumes from both models - temps = {v.value for v in vars if v.name == 'temperature'} - vols = {v.value for v in vars if v.name == 'volume'} + temps = {v.value for v in vars if v.display_name == 'temperature'} + vols = {v.value for v in vars if v.display_name == 'volume'} assert temps == {50, 70} assert vols == {3.0, 4.0} diff --git a/tests/unit/base_classes/test_model_base.py b/tests/unit/base_classes/test_model_base.py index e91c20a9..47f9337a 100644 --- a/tests/unit/base_classes/test_model_base.py +++ b/tests/unit/base_classes/test_model_base.py @@ -9,7 +9,7 @@ from easyscience import Parameter from easyscience import global_object from easyscience.base_classes import ModelBase -from easyscience.io import SerializerBase +from easyscience.base_classes import SerializerBase from easyscience.variable import DescriptorStr @@ -20,9 +20,11 @@ class MockModelComponent(ModelBase): def __init__(self, display_name=None, unique_name=None, temperature=0, room_temperature=22): super().__init__(display_name=display_name, unique_name=unique_name) - self._temperature = Parameter(name='temperature', value=temperature) - self._room_temperature = DescriptorNumber(name='room_temperature', value=room_temperature) - self._status = DescriptorStr(name='status', value='OK') + self._temperature = Parameter(display_name='temperature', value=temperature) + self._room_temperature = DescriptorNumber( + display_name='room_temperature', value=room_temperature + ) + self._status = DescriptorStr(display_name='status', value='OK') @property def temperature(self): @@ -56,8 +58,8 @@ class MockModelFull(ModelBase): def __init__(self, component=None, display_name=None, unique_name=None, pressure=0, area=1): super().__init__(display_name=display_name, unique_name=unique_name) - self._pressure = Parameter(name='pressure', value=pressure) - self._area = Parameter(name='area', value=area) + self._pressure = Parameter(display_name='pressure', value=pressure) + self._area = Parameter(display_name='area', value=area) if component is not None: self._component = component else: @@ -102,8 +104,8 @@ def nested_model(self, monkeypatch): @pytest.fixture def clear(self): - # Clear the global object map before each test - global_object.map._clear() + # Clear the global object registry before each test + global_object.registry.clear() def test_init(self): # When Then @@ -156,15 +158,6 @@ def test_get_fittable_parameters_nested(self, nested_model): ) assert nested_model.component.get_all_variables.call_count == 1 - def test_get_fit_parameters_alias(self, monkeypatch): - # When - model = MockModelFull(pressure=1) - monkeypatch.setattr(model, 'get_free_parameters', MagicMock()) - # Then - fit_params = model.get_fit_parameters() - # Expect - assert model.get_free_parameters.call_count == 1 - def test_get_free_parameters_nested(self, nested_model): # When Then free_params = nested_model.get_free_parameters() @@ -193,7 +186,7 @@ def test_from_dict(self, monkeypatch, clear): ) ), ) - global_object.map._clear() + global_object.registry.clear() # Then new_model = MockModelComponent.from_dict(obj_dict) # Expect @@ -202,11 +195,10 @@ def test_from_dict(self, monkeypatch, clear): assert new_model._room_temperature.value == 22.0 assert isinstance(new_model._temperature, Parameter) assert isinstance(new_model._room_temperature, DescriptorNumber) - assert len(global_object.map.vertices()) == 4 - assert global_object.map.get_item_by_key('Parameter_0') is new_model._temperature + assert len(global_object.registry) == 4 + assert global_object.registry['Parameter_0'] is new_model._temperature assert ( - len([param for param in global_object.map.vertices() if param.startswith('Parameter')]) - == 1 + len([param for param in global_object.registry if param.startswith('Parameter')]) == 1 ) def test_from_dict_nested(self, monkeypatch, clear): @@ -234,7 +226,7 @@ def test_from_dict_nested(self, monkeypatch, clear): ) ), ) - global_object.map._clear() + global_object.registry.clear() # Then new_model = MockModelFull.from_dict(obj_dict) # Expect @@ -248,15 +240,14 @@ def test_from_dict_nested(self, monkeypatch, clear): assert isinstance(new_model._area, Parameter) assert isinstance(new_model.component._temperature, Parameter) assert isinstance(new_model.component._room_temperature, DescriptorNumber) - assert len(global_object.map.vertices()) == 7 - assert global_object.map.get_item_by_key('Parameter_0') in [ + assert len(global_object.registry) == 7 + assert global_object.registry['Parameter_0'] in [ new_model._pressure, new_model._area, new_model.component._temperature, ] assert ( - len([param for param in global_object.map.vertices() if param.startswith('Parameter')]) - == 3 + len([param for param in global_object.registry if param.startswith('Parameter')]) == 3 ) def test_from_dict_not_easyscience(self): @@ -312,7 +303,7 @@ def test_from_dict_parameter_setting_failure(self, monkeypatch, clear): 'temperature', property(lambda self: (_ for _ in ()).throw(Exception('Simulated failure'))), ) - global_object.map._clear() + global_object.registry.clear() # Then Expect with pytest.raises( SyntaxError, diff --git a/tests/unit/base_classes/test_model_with_easy_list.py b/tests/unit/base_classes/test_model_with_easy_list.py new file mode 100644 index 00000000..187b2637 --- /dev/null +++ b/tests/unit/base_classes/test_model_with_easy_list.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Behaviour of a ModelBase that holds its parameters in an EasyList.""" + +import copy + +import numpy as np +import pytest +from easy_test_models import CoefficientModel + +from easyscience import global_object +from easyscience.base_classes import EasyList +from easyscience.variable import Parameter + + +@pytest.fixture(autouse=True) +def clear(): + """Clear global object registry before each test.""" + global_object.registry.clear() + + +@pytest.mark.parametrize( + 'coefficients', + [(1.0,), (1.0, 2.0), (1.0, 2.0, 3.0), (-1.0, -2.0, -3.0), (0.72, 6.48, -0.48)], +) +def test_numbers_become_parameters(coefficients): + model = CoefficientModel(coefficients=coefficients) + + assert all(isinstance(c, Parameter) for c in model.coefficients) + assert [c.value for c in model.coefficients] == list(coefficients) + assert [c.display_name for c in model.coefficients] == [ + f'c{i}' for i in range(len(coefficients)) + ] + x = np.linspace(0, 10, 100) + assert np.allclose(model(x), np.polyval(coefficients, x)) + + +def test_default_initialization(): + model = CoefficientModel(display_name='empty') + + assert model.display_name == 'empty' + assert len(model.coefficients) == 0 + assert model.coefficients._protected_types == [Parameter] + + +def test_parameters_are_kept(): + p0 = Parameter(value=5.0, display_name='a') + p1 = Parameter(value=2.0, display_name='b') + + model = CoefficientModel(coefficients=[p0, p1, 1.0]) + + assert model.coefficients[0] is p0 + assert model.coefficients[1] is p1 + assert model.coefficients[2].display_name == 'c2' + + +def test_easy_list_is_used_directly(): + collection = EasyList(Parameter(value=1.0), Parameter(value=2.0), protected_types=Parameter) + + model = CoefficientModel(coefficients=collection) + + assert model.coefficients is collection + + +def test_get_free_parameters(): + """Parameters inside the EasyList are exposed for fitting.""" + model = CoefficientModel(coefficients=[1.0, 2.0]) + + assert model.get_free_parameters() == list(model.coefficients) + + +def test_dict_round_trip(): + model = CoefficientModel(display_name='round_trip', coefficients=[1.0, 2.0, 3.0]) + + new_model = CoefficientModel.from_dict(model.to_dict()) + + assert new_model.display_name == 'round_trip' + assert [c.value for c in new_model.coefficients] == [1.0, 2.0, 3.0] + assert [c.display_name for c in new_model.coefficients] == ['c0', 'c1', 'c2'] + + +def test_dict_round_trip_unnamed_model_keeps_coefficient_names(): + """The model's own missing names must not be skipped on nested items.""" + model = CoefficientModel( + coefficients=[Parameter(1.0, display_name='c0', unique_name='my_c0'), 2.0] + ) + + d = model.to_dict() + global_object.registry.clear() # the explicit name is kept, so free it first + new_model = CoefficientModel.from_dict(d) + + assert 'unique_name' not in d + assert 'display_name' not in d + assert [c.display_name for c in new_model.coefficients] == ['c0', 'c1'] + assert new_model.coefficients[0].unique_name == 'my_c0' + + +@pytest.mark.parametrize('copier', [copy.copy, copy.deepcopy], ids=['copy', 'deepcopy']) +def test_copy_with_named_coefficients(copier): + """Copies get fresh coefficient identities, even for explicit names.""" + model = CoefficientModel( + coefficients=[Parameter(1.0, unique_name='coefficient'), Parameter(2.0)] + ) + + data = model.to_dict(skip=['unique_name']) + assert all('unique_name' not in c for c in data['coefficients']['data']) + + new_model = copier(model) + + assert [c.value for c in new_model.coefficients] == [1.0, 2.0] + assert new_model.coefficients[0].unique_name != 'coefficient' + assert all(new is not old for new, old in zip(new_model.coefficients, model.coefficients)) diff --git a/tests/unit/base_classes/test_obj_base.py b/tests/unit/base_classes/test_obj_base.py deleted file mode 100644 index 5eb1b2ea..00000000 --- a/tests/unit/base_classes/test_obj_base.py +++ /dev/null @@ -1,498 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from contextlib import contextmanager -from copy import copy -from typing import ClassVar -from typing import List -from typing import Optional -from typing import Type -from typing import Union - -import numpy as np -import pytest - -import easyscience -from easyscience import DescriptorNumber -from easyscience import ObjBase -from easyscience import Parameter -from easyscience import global_object -from easyscience.io import SerializerDict - - -@pytest.fixture -def clear(): - global_object.map._clear() - - -@pytest.fixture -def setup_pars(): - d = { - 'name': 'test', - 'par1': Parameter('p1', 0.1, fixed=True), - 'des1': DescriptorNumber('d1', 0.1), - 'par2': Parameter('p2', 0.1), - 'des2': DescriptorNumber('d2', 0.1), - 'par3': Parameter('p3', 0.1), - } - return d - - -@contextmanager -def not_raises(expected_exception: Union[Type[BaseException], List[Type[BaseException]]]): - try: - yield - except expected_exception: - raise pytest.fail( - 'Did raise exception {0} when it should not.'.format(repr(expected_exception)) - ) - except Exception as err: - raise pytest.fail('An unexpected exception {0} raised.'.format(repr(err))) - - -@pytest.mark.parametrize( - 'a, kw', - [ - ([], ['par1']), - (['par1'], []), - (['par1'], ['par2']), - (['par1', 'des1'], ['par2', 'des2']), - ], -) -def test_ObjBase_create(setup_pars: dict, a: List[str], kw: List[str]): - name = setup_pars['name'] - args = [] - for key in a: - args.append(setup_pars[key]) - kwargs = {} - for key in kw: - kwargs[key] = setup_pars[key] - base = ObjBase(name, None, *args, **kwargs) - assert base.name == name - for key in a: - item = getattr(base, setup_pars[key].name) - assert isinstance(item, setup_pars[key].__class__) - - -def test_ObjBase_copy(setup_pars: dict): - # When - name = setup_pars['name'] - args = [] - for key in ['par1', 'des1']: - args.append(setup_pars[key]) - kwargs = {} - for key in ['par2', 'des2']: - kwargs[key] = setup_pars[key] - base = ObjBase(name, None, *args, **kwargs) - - # Then - base_copy = copy(base) - - # Expect - assert base_copy.name == name - assert base_copy.unique_name != base.unique_name - - for key in ['par1', 'des1']: - item = getattr(base, setup_pars[key].name) - assert isinstance(item, setup_pars[key].__class__) - - -def test_ObjBase_get(setup_pars: dict): - name = setup_pars['name'] - explicit_name1 = 'par1' - explicit_name2 = 'par2' - kwargs = { - setup_pars[explicit_name1].name: setup_pars[explicit_name1], - setup_pars[explicit_name2].name: setup_pars[explicit_name2], - } - obj = ObjBase(name, **kwargs) - with not_raises(AttributeError): - p1: Parameter = obj.p1 - with not_raises(AttributeError): - p2: Parameter = obj.p2 - - -def test_ObjBase_set(setup_pars: dict): - name = setup_pars['name'] - explicit_name1 = 'par1' - kwargs = { - setup_pars[explicit_name1].name: setup_pars[explicit_name1], - } - obj = ObjBase(name, **kwargs) - new_value = 5.0 - with not_raises([AttributeError, ValueError]): - obj.p1 = new_value - assert obj.p1.value == new_value - - -def test_ObjBase_get_parameters(setup_pars: dict): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - pars = obj.get_fit_parameters() - assert isinstance(pars, list) - assert len(pars) == 2 - par_names = [par.name for par in pars] - assert 'p2' in par_names - assert 'p3' in par_names - - -def test_ObjBase_fit_objects(setup_pars: dict): - pass - - -def test_ObjBase_as_dict(clear, setup_pars: dict): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - obtained = obj.as_dict() - assert isinstance(obtained, dict) - expected = { - '@module': 'easyscience.legacy.obj_base', - '@class': 'ObjBase', - '@version': easyscience.__version__, - 'name': 'test', - 'par1': { - '@module': Parameter.__module__, - '@class': Parameter.__name__, - '@version': easyscience.__version__, - 'name': 'p1', - 'value': 0.1, - 'variance': 0.0, - 'min': -np.inf, - 'max': np.inf, - 'fixed': True, - 'unit': 'dimensionless', - }, - 'des1': { - '@module': DescriptorNumber.__module__, - '@class': DescriptorNumber.__name__, - '@module': DescriptorNumber.__module__, - '@class': DescriptorNumber.__name__, - '@version': easyscience.__version__, - 'name': 'd1', - 'value': 0.1, - 'unit': 'dimensionless', - 'description': '', - 'url': '', - 'display_name': 'd1', - }, - 'par2': { - '@module': Parameter.__module__, - '@class': Parameter.__name__, - '@version': easyscience.__version__, - 'name': 'p2', - 'value': 0.1, - 'variance': 0.0, - 'min': -np.inf, - 'max': np.inf, - 'fixed': False, - 'unit': 'dimensionless', - }, - 'des2': { - '@module': DescriptorNumber.__module__, - '@class': DescriptorNumber.__name__, - '@module': DescriptorNumber.__module__, - '@class': DescriptorNumber.__name__, - '@version': easyscience.__version__, - 'name': 'd2', - 'value': 0.1, - 'unit': 'dimensionless', - 'description': '', - 'url': '', - 'display_name': 'd2', - }, - 'par3': { - '@module': Parameter.__module__, - '@class': Parameter.__name__, - '@version': easyscience.__version__, - 'name': 'p3', - 'value': 0.1, - 'variance': 0.0, - 'min': -np.inf, - 'max': np.inf, - 'fixed': False, - 'unit': 'dimensionless', - }, - } - - def check_dict(check, item): - if isinstance(check, dict) and isinstance(item, dict): - if '@module' in item.keys(): - with not_raises([ValueError, AttributeError]): - global_object.map._clear() - this_obj = SerializerDict().decode(item) - - for key in check.keys(): - assert key in item.keys() - check_dict(check[key], item[key]) - else: - assert isinstance(item, type(check)) - assert item == check - - check_dict(expected, obtained) - - -def test_ObjBase_dict_roundtrip(clear, setup_pars: dict): - # When - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars, unique_name='special_name') - obj_dict = obj.as_dict() - - global_object.map._clear() - - # Then - new_obj = ObjBase.from_dict(obj_dict) - - # Expect - new_obj_dict = new_obj.as_dict() - assert obj_dict == new_obj_dict - - -def test_ObjBase_dir(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - expected = [ - 'encode', - 'decode', - 'as_dict', - 'des1', - 'des2', - 'from_dict', - 'generate_bindings', - 'get_fit_parameters', - 'get_parameters', - 'interface', - 'unique_name', - 'name', - 'par1', - 'par2', - 'par3', - 'switch_interface', - 'user_data', - ] - obtained = dir(obj) - print(obtained) - print(expected) - assert len(obtained) == len(expected) - assert obtained == sorted(obtained) - assert len(set(expected).difference(set(obtained))) == 0 - - -def test_ObjBase_get_parameters(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - pars = obj.get_parameters() - assert len(pars) == 3 - - -def test_ObjBase_get_parameters_nested(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - - name2 = name + '_2' - obj2 = ObjBase(name2, obj=obj, **setup_pars) - - pars = obj2.get_parameters() - assert len(pars) == 6 - - pars = obj.get_parameters() - assert len(pars) == 3 - - -def test_ObjBase_get_fit_parameters(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - pars = obj.get_fit_parameters() - assert len(pars) == 2 - - -def test_ObjBase_get_fit_parameters_nested(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - - name2 = name + '_2' - obj2 = ObjBase(name2, obj=obj, **setup_pars) - - pars = obj2.get_fit_parameters() - assert len(pars) == 4 - - pars = obj.get_fit_parameters() - assert len(pars) == 2 - - -def test_ObjBase__add_component(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - - p = Parameter('added_par', 1) - new_item_name = 'Added' - obj._add_component(new_item_name, p) - - assert hasattr(obj, new_item_name) - a = getattr(obj, new_item_name) - assert isinstance(a, Parameter) - - -def test_ObjBase_name(setup_pars): - name = setup_pars['name'] - del setup_pars['name'] - obj = ObjBase(name, **setup_pars) - assert obj.name == name - - -def test_Base_GETSET(): - class A(ObjBase): - def __init__(self, a: Parameter): - super(A, self).__init__('a', a=a) - - @classmethod - def from_pars(cls, a: float): - return cls(a=Parameter('a', a)) - - a_start = 5 - a_end = 10 - a = A.from_pars(a_start) - graph = a._global_object.map - - assert a.a.value == a_start - assert len(graph.get_edges(a)) == 1 - - setattr(a, 'a', a_end) - assert a.a.value == a_end - assert len(graph.get_edges(a)) == 1 - - -def test_Base_GETSET(): - class A(ObjBase): - def __init__(self, a: Parameter): - super(A, self).__init__('a', a=a) - b = 0 - - @classmethod - def from_pars(cls, a: float): - return cls(a=Parameter('a', a)) - - a = A.from_pars(5) - b_new = 10 - a.b = b_new - assert a.b == b_new - - -def test_Base_GETSET_v2(): - class A(ObjBase): - a: ClassVar[Parameter] - - def __init__(self, a: Parameter): - super(A, self).__init__('a', a=a) - - @classmethod - def from_pars(cls, a: float): - return cls(a=Parameter('a', a)) - - a_start = 5 - a_end = 10 - a = A.from_pars(a_start) - graph = a._global_object.map - - assert a.a.value == a_start - assert len(graph.get_edges(a)) == 1 - - setattr(a, 'a', a_end) - assert a.a.value == a_end - assert len(graph.get_edges(a)) == 1 - - -def test_Base_GETSET_v3(): - class A(ObjBase): - a: ClassVar[Parameter] - - def __init__(self, a: Parameter): - super(A, self).__init__('a', a=a) - - @classmethod - def from_pars(cls, a: float): - return cls(a=Parameter('a', a)) - - a_start = 5 - a_end = 10 - a = A.from_pars(a_start) - graph = a._global_object.map - - assert a.a.value == a_start - assert len(graph.get_edges(a)) == 1 - a_ = Parameter('a', a_end) - assert a.a.unique_name in graph.get_edges(a) - a__ = a.a - - setattr(a, 'a', a_) - assert a.a.value == a_end - assert len(graph.get_edges(a)) == 1 - assert a_.unique_name in graph.get_edges(a) - assert a__.unique_name not in graph.get_edges(a) - - -def test_BaseCreation(): - class A(ObjBase): - def __init__(self, a: Optional[Union[Parameter, float]] = None): - super(A, self).__init__('A', a=Parameter('a', 1.0)) - if a is not None: - self.a = a - - a = A() - assert a.a.value == 1.0 - a = A(2.0) - assert a.a.value == 2.0 - a = A(Parameter('a', 3.0)) - assert a.a.value == 3.0 - a.a = 4.0 - assert a.a.value == 4.0 - - class B(ObjBase): - def __init__(self, b: Optional[Union[A, Parameter, float]] = None): - super(B, self).__init__('B', b=A()) - if b is not None: - if isinstance(b, (float, Parameter)): - b = A(b) - self.b = b - - b = B() - assert b.b.a.value == 1.0 - b = B(2.0) - assert b.b.a.value == 2.0 - b = B(A(3.0)) - assert b.b.a.value == 3.0 - b.b.a = 4.0 - assert b.b.a.value == 4.0 - - -def test_unique_name_generator(clear): - # When Then - test_obj = ObjBase(name='test') - # Expect - assert test_obj.unique_name == 'ObjBase_0' - - -def test_unique_name_change(clear): - # When - test_obj = ObjBase(name='test') - # Then - test_obj.unique_name = 'test' - # Expect - assert test_obj.unique_name == 'test' - - -@pytest.mark.parametrize('input', [2, 2.0, [2], {2}, None]) -def test_unique_name_change_exception(input): - # When - test_obj = ObjBase(name='test') - # Then Expect - with pytest.raises(TypeError): - test_obj.unique_name = input diff --git a/tests/unit/io/test_serializer_base.py b/tests/unit/base_classes/test_serializer_base.py similarity index 83% rename from tests/unit/io/test_serializer_base.py rename to tests/unit/base_classes/test_serializer_base.py index c3c8dd21..5af6c63b 100644 --- a/tests/unit/io/test_serializer_base.py +++ b/tests/unit/base_classes/test_serializer_base.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause import datetime +import json from enum import Enum from typing import Any from typing import List @@ -16,9 +17,16 @@ from easyscience import DescriptorNumber from easyscience import Parameter from easyscience import global_object -from easyscience.base_classes import NewBase -from easyscience.io import SerializerBase -from easyscience.io import SerializerComponent +from easyscience.base_classes import EasyBase +from easyscience.base_classes import ModelBase +from easyscience.base_classes import SerializerBase + + +class MockSerializable: + """Minimal stand-in for an object that knows how to encode itself.""" + + def encode(self, skip: Optional[List[str]] = None, encoder=None, **kwargs) -> Any: + return (encoder or ConcreteSerializer)().encode(self, skip=skip, **kwargs) class TestEnum(Enum): @@ -26,7 +34,7 @@ class TestEnum(Enum): ANOTHER_VALUE = 42 -class MockSerializerComponent(SerializerComponent): +class MockSerializableObj(MockSerializable): """Mock serializer component for testing""" def __init__( @@ -35,25 +43,34 @@ def __init__( self.name = name self.value = value self.optional_param = optional_param - self._kwargs = kwargs self.unique_name = f'mock_{name}' self._global_object = True -class MockSerializerWithRedirect(SerializerComponent): - """Mock with _REDIRECT for testing redirect functionality""" - - _REDIRECT = {'special_attr': lambda obj: obj.value * 2, 'none_attr': None} +class MockSerializerWithArgSpec(MockSerializable): + """Mock with an _arg_spec that leaves out one constructor argument""" def __init__(self, name: str = 'test', value: int = 1, special_attr: int = 5): self.name = name self.value = value self.special_attr = special_attr - self.unique_name = f'redirect_{name}' + self.unique_name = f'arg_spec_{name}' self._global_object = True + @property + def _arg_spec(self): + return {'name', 'value'} + + +class MockModelWithParameter(ModelBase): + """Model holding a Parameter as a constructor argument""" + + def __init__(self, p: Optional[Parameter] = None): + super().__init__() + self._p = p -class MockSerializerWithConvertToDict(SerializerComponent): + +class MockSerializerWithConvertToDict(MockSerializable): """Mock with custom _convert_to_dict method""" def __init__(self, name: str = 'test', value: int = 1): @@ -70,7 +87,7 @@ def _convert_to_dict(self, d, encoder, skip=None, **kwargs): class ConcreteSerializer(SerializerBase): """Concrete implementation for testing abstract methods""" - def encode(self, obj: SerializerComponent, skip: Optional[List[str]] = None, **kwargs) -> Any: + def encode(self, obj: Any, skip: Optional[List[str]] = None, **kwargs) -> Any: return self._convert_to_dict(obj, skip=skip, **kwargs) @classmethod @@ -82,13 +99,9 @@ class TestSerializerBase: @pytest.fixture def clear(self): """Clear everything before and after each test""" - global_object.map._clear() - if global_object.stack: - global_object.stack.clear() + global_object.registry.clear() yield - global_object.map._clear() - if global_object.stack: - global_object.stack.clear() + global_object.registry.clear() @pytest.fixture def serializer(self): @@ -96,7 +109,7 @@ def serializer(self): @pytest.fixture def mock_obj(self): - return MockSerializerComponent('test_obj', 42, 'optional_value') + return MockSerializableObj('test_obj', 42, 'optional_value') def test_abstract_methods_are_placeholders(self): """Test that SerializerBase can be instantiated but abstract methods just pass""" @@ -231,7 +244,6 @@ def test_convert_from_dict_easyscience_object(self, clear): '@module': 'easyscience.variable.parameter', '@class': 'Parameter', '@version': '0.6.0', - 'name': 'test_param', 'value': 5.0, 'unit': 'm', 'variance': 0.1, @@ -245,7 +257,7 @@ def test_convert_from_dict_easyscience_object(self, clear): result = SerializerBase._convert_from_dict(param_dict) assert isinstance(result, Parameter) - assert result.name == 'test_param' + assert result.display_name == 'test_param' assert result.value == 5.0 assert str(result.unit) == 'm' @@ -290,13 +302,37 @@ def test_convert_to_dict_with_skip(self, serializer, mock_obj, clear): assert 'value' not in result assert 'optional_param' not in result - def test_convert_to_dict_with_redirect(self, serializer, clear): - """Test _convert_to_dict with _REDIRECT""" - obj = MockSerializerWithRedirect('redirect_test', 10) + def test_convert_to_dict_with_arg_spec(self, serializer, clear): + """Test _convert_to_dict only collects the names in _arg_spec""" + obj = MockSerializerWithArgSpec('arg_spec_test', 10) result = serializer._convert_to_dict(obj) - assert result['special_attr'] == 20 # 10 * 2 from redirect - assert 'none_attr' not in result # Should be skipped due to None redirect + assert result['name'] == 'arg_spec_test' + assert result['value'] == 10 + assert 'special_attr' not in result + + def test_convert_to_dict_nested_parameter_round_trip(self, serializer, clear): + """A Parameter nested in a model survives a dict round trip""" + model = MockModelWithParameter( + p=Parameter(2.0, unit='m', variance=0.1, min=0, max=5, fixed=True, display_name='p') + ) + result = serializer._convert_to_dict(model) + + nested = result['p'] + assert nested['@class'] == 'Parameter' + json.dumps(nested) + + global_object.registry.clear() + restored = Parameter.from_dict(nested) + + assert restored is not model._p + assert restored.value == 2.0 + assert restored.unit == 'm' + assert restored.variance == 0.1 + assert restored.min == 0 + assert restored.max == 5 + assert restored.fixed is True + assert restored.display_name == 'p' def test_convert_to_dict_with_custom_convert_to_dict(self, serializer, clear): """Test _convert_to_dict with custom _convert_to_dict method""" @@ -311,7 +347,7 @@ def test_convert_to_dict_with_enum_object(self, serializer, clear): """Test _convert_to_dict when the object itself is an enum""" # Test that enum values in objects remain as enums without full_encode - class MockObjWithEnum(SerializerComponent): + class MockObjWithEnum(MockSerializable): def __init__(self, name: str, enum_val: TestEnum): self.name = name self.enum_val = enum_val @@ -329,7 +365,7 @@ def test_convert_to_dict_full_encode(self, serializer, clear): """Test _convert_to_dict with full_encode=True""" dt = datetime.datetime(2023, 10, 17, 14, 30, 45) - class MockObjWithDateTime(SerializerComponent): + class MockObjWithDateTime(MockSerializable): def __init__(self, name: str, dt: datetime.datetime): self.name = name self.dt = dt @@ -346,7 +382,7 @@ def __init__(self, name: str, dt: datetime.datetime): def test_convert_to_dict_without_global_object(self, serializer): """Test _convert_to_dict with object without _global_object""" - class MockObjNoGlobal(SerializerComponent): + class MockObjNoGlobal(MockSerializable): def __init__(self, name: str): self.name = name @@ -356,10 +392,10 @@ def __init__(self, name: str): assert result['name'] == 'test' assert 'unique_name' not in result - def test_convert_to_dict_with_arg_spec(self, serializer, clear): + def test_convert_to_dict_with_instance_arg_spec(self, serializer, clear): """Test _convert_to_dict with custom _arg_spec""" - class MockObjCustomArgSpec(SerializerComponent): + class MockObjCustomArgSpec(MockSerializable): def __init__(self, name: str, value: int, extra: str = 'default'): self.name = name self.value = value @@ -377,7 +413,7 @@ def __init__(self, name: str, value: int, extra: str = 'default'): def test_recursive_encoder_with_lists(self, serializer, clear): """Test _recursive_encoder with lists""" - mock_obj = MockSerializerComponent('list_test', 1) + mock_obj = MockSerializableObj('list_test', 1) data = [mock_obj, 'string', 42, {'key': 'value'}] result = serializer._recursive_encoder(data) @@ -392,7 +428,7 @@ def test_recursive_encoder_with_lists(self, serializer, clear): def test_recursive_encoder_with_dicts(self, serializer, clear): """Test _recursive_encoder with dictionaries""" - mock_obj = MockSerializerComponent('dict_test', 2) + mock_obj = MockSerializableObj('dict_test', 2) data = {'obj': mock_obj, 'simple': 'value'} result = serializer._recursive_encoder(data) @@ -404,7 +440,7 @@ def test_recursive_encoder_with_dicts(self, serializer, clear): def test_recursive_encoder_with_tuples(self, serializer, clear): """Test _recursive_encoder with tuples""" - mock_obj = MockSerializerComponent('tuple_test', 3) + mock_obj = MockSerializableObj('tuple_test', 3) data = (mock_obj, 'string', 42) result = serializer._recursive_encoder(data) @@ -425,36 +461,36 @@ def test_recursive_encoder_builtin_encode_method(self, serializer): def test_recursive_encoder_with_mutable_sequence(self, serializer, clear): """Test _recursive_encoder with MutableSequence objects""" - from easyscience.base_classes import CollectionBase + from easyscience.base_classes import EasyList - d0 = DescriptorNumber('a', 0) # type: ignore - d1 = DescriptorNumber('b', 1) # type: ignore - collection = CollectionBase('test_collection', d0, d1) + d0 = DescriptorNumber(0, display_name='a') # type: ignore + d1 = DescriptorNumber(1, display_name='b') # type: ignore + collection = EasyList(d0, d1) result = serializer._recursive_encoder(collection) assert isinstance(result, dict) - assert result['@class'] == 'CollectionBase' - assert 'data' in result + assert result['@class'] == 'EasyList' + assert len(result['data']) == 2 - @patch('easyscience.io.serializer_base.import_module') + @patch('easyscience.base_classes.serializer_base.import_module') def test_convert_to_dict_no_version(self, mock_import, serializer, clear): """Test _convert_to_dict when module has no __version__""" mock_module = Mock() del mock_module.__version__ # Remove version attribute mock_import.return_value = mock_module - mock_obj = MockSerializerComponent('no_version', 1) + mock_obj = MockSerializableObj('no_version', 1) result = serializer._convert_to_dict(mock_obj) assert result['@version'] is None - @patch('easyscience.io.serializer_base.import_module') + @patch('easyscience.base_classes.serializer_base.import_module') def test_convert_to_dict_import_error(self, mock_import, serializer, clear): """Test _convert_to_dict when import_module raises ImportError""" mock_import.side_effect = ImportError('Module not found') - mock_obj = MockSerializerComponent('import_error', 1) + mock_obj = MockSerializableObj('import_error', 1) result = serializer._convert_to_dict(mock_obj) assert result['@version'] is None @@ -462,7 +498,7 @@ def test_convert_to_dict_import_error(self, mock_import, serializer, clear): def test_convert_to_dict_attribute_error_handling(self, serializer, clear): """Test _convert_to_dict handles AttributeError for missing attributes""" - class MockObjMissingAttrs(SerializerComponent): + class MockObjMissingAttrs(MockSerializable): def __init__(self, name: str, missing_param: str = 'default'): self.name = name self.unique_name = f'missing_{name}' @@ -471,31 +507,13 @@ def __init__(self, name: str, missing_param: str = 'default'): obj = MockObjMissingAttrs('test') - with pytest.raises(NotImplementedError, match='Unable to automatically determine as_dict'): + with pytest.raises(NotImplementedError, match='Unable to automatically determine to_dict'): serializer._convert_to_dict(obj) - def test_convert_to_dict_with_kwargs_attribute(self, serializer, clear): - """Test _convert_to_dict with _kwargs attribute handling""" - - class MockObjWithKwargs(SerializerComponent): - def __init__(self, name: str, value: int): - self.name = name - self.value = value - self.unique_name = f'kwargs_{name}' - self._global_object = True - # Set up _kwargs to test the kwargs handling path - self._kwargs = {'extra_param': 'extra_value'} - - obj = MockObjWithKwargs('test', 42) - result = serializer._convert_to_dict(obj) - - # The extra_param from _kwargs should be included - assert result['extra_param'] == 'extra_value' - def test_convert_to_dict_varargs_handling(self, serializer, clear): """Test _convert_to_dict with varargs (*args) handling""" - class MockObjWithVarargs(SerializerComponent): + class MockObjWithVarargs(MockSerializable): def __init__(self, name: str, *args): self.name = name self.args = args @@ -539,7 +557,7 @@ def test_convert_from_dict_edge_cases(self): def test_concrete_serializer_implementation(self, clear): """Test that ConcreteSerializer works correctly""" serializer = ConcreteSerializer() - mock_obj = MockSerializerComponent('concrete_test', 100) + mock_obj = MockSerializableObj('concrete_test', 100) # Test encode encoded = serializer.encode(mock_obj) @@ -548,9 +566,9 @@ def test_concrete_serializer_implementation(self, clear): assert encoded['value'] == 100 # Test decode - global_object.map._clear() # Clear before decode + global_object.registry.clear() # Clear before decode decoded = ConcreteSerializer.decode(encoded) - assert isinstance(decoded, MockSerializerComponent) + assert isinstance(decoded, MockSerializableObj) assert decoded.name == 'concrete_test' assert decoded.value == 100 @@ -567,7 +585,18 @@ def complex_func(self, required_arg, optional_arg='default', *args, **kwargs): assert spec.varkw == 'kwargs' assert spec.defaults == ('default',) - @patch('easyscience.io.serializer_base.np', None) + def test_get_arg_spec_keyword_only(self): + """Keyword-only arguments are included after positional ones""" + + def kw_func(self, value, *, unit='', unique_name=None, **kwargs): + pass + + spec, args = SerializerBase.get_arg_spec(kw_func) + + assert args == ['value', 'unit', 'unique_name'] + assert spec.kwonlyargs == ['unit', 'unique_name'] + + @patch('easyscience.base_classes.serializer_base.np', None) def test_encode_objs_without_numpy(self): """Test _encode_objs when numpy is not available""" # This test patches np to None to simulate numpy not being installed @@ -579,7 +608,7 @@ def test_encode_objs_without_numpy(self): def test_recursive_encoder_with_nested_structures(self, serializer, clear): """Test _recursive_encoder with deeply nested structures""" - mock_obj = MockSerializerComponent('nested', 1) + mock_obj = MockSerializableObj('nested', 1) data = {'level1': {'level2': [mock_obj, {'level3': mock_obj}]}} @@ -646,23 +675,23 @@ def test_deserialize_value_easyscience_uses_from_dict(self, monkeypatch): # When serialized_dict = { '@module': 'easyscience.base_classes', - '@class': 'NewBase', + '@class': 'EasyBase', 'display_name': 'test', } - monkeypatch.setattr(NewBase, 'from_dict', MagicMock()) + monkeypatch.setattr(EasyBase, 'from_dict', MagicMock()) # Then obj = SerializerBase._deserialize_value(serialized_dict) # Expect - NewBase.from_dict.assert_called_once_with(serialized_dict) + EasyBase.from_dict.assert_called_once_with(serialized_dict) def test_deserialize_value_easyscience_no_from_dict(self, monkeypatch): # When serialized_dict = { '@module': 'easyscience.base_classes', - '@class': 'NewBase', + '@class': 'EasyBase', 'display_name': 'test', } - monkeypatch.delattr(NewBase, 'from_dict') + monkeypatch.delattr(EasyBase, 'from_dict') monkeypatch.setattr(SerializerBase, '_convert_from_dict', MagicMock()) # Then obj = SerializerBase._deserialize_value(serialized_dict) @@ -673,7 +702,7 @@ def test_deserialize_value_easyscience_import_error(self, monkeypatch): # When serialized_dict = { '@module': 'easyscience.base_classes', - '@class': 'NewBase', + '@class': 'EasyBase', 'display_name': 'test', } monkeypatch.setattr(SerializerBase, '_import_class', MagicMock(side_effect=ImportError())) @@ -701,7 +730,7 @@ def test_deserialize_dict(self, monkeypatch): '_deserialize_value', MagicMock( side_effect=[ - Parameter(name='param1', value=10.0), + Parameter(display_name='param1', value=10.0), np.array([0, 1], dtype=np.int64), ] ), diff --git a/tests/unit/fitting/calculators/test_interface_factory.py b/tests/unit/fitting/calculators/test_interface_factory.py deleted file mode 100644 index ce5bb17a..00000000 --- a/tests/unit/fitting/calculators/test_interface_factory.py +++ /dev/null @@ -1,563 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import logging -from unittest.mock import MagicMock - -import pytest - -from easyscience import global_object -from easyscience.fitting.calculators.interface_factory import InterfaceFactoryTemplate -from easyscience.fitting.calculators.interface_factory import ItemContainer - - -class TestInterfaceFactoryTemplate: - @pytest.fixture - def clear(self): - """Clear global map to avoid test contamination""" - global_object.map._clear() - yield - global_object.map._clear() - - @pytest.fixture - def mock_interface1(self): - """Create a mock interface class""" - - class MockInterface1: - name = 'MockInterface1' - - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - - def fit_func(self, *args, **kwargs): - return 'result1' - - def create(self, model): - return [] - - return MockInterface1 - - @pytest.fixture - def mock_interface2(self): - """Create a second mock interface class""" - - class MockInterface2: - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - - def fit_func(self, *args, **kwargs): - return 'result2' - - def create(self, model): - return [] - - return MockInterface2 - - @pytest.fixture - def mock_interface_with_custom_name(self): - """Create a mock interface with custom name attribute""" - - class MockInterfaceCustom: - name = 'CustomName' - - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - - def fit_func(self, *args, **kwargs): - return 'custom_result' - - def create(self, model): - return [] - - return MockInterfaceCustom - - @pytest.fixture - def factory_single_interface(self, clear, mock_interface1): - """Create factory with single interface""" - return InterfaceFactoryTemplate([mock_interface1]) - - @pytest.fixture - def factory_multiple_interfaces(self, clear, mock_interface1, mock_interface2): - """Create factory with multiple interfaces""" - return InterfaceFactoryTemplate([mock_interface1, mock_interface2]) - - def test_init_with_single_interface_default_selection(self, clear, mock_interface1): - """Test initialization with single interface uses first as default""" - # When - factory = InterfaceFactoryTemplate([mock_interface1]) - - # Then - assert factory._interfaces == [mock_interface1] - assert factory._current_interface == mock_interface1 - assert factory() is not None - assert isinstance(factory(), mock_interface1) - - def test_init_with_empty_interface_list_raises_error(self, clear): - """Test initialization with empty interface list raises NotImplementedError""" - # When/Then - with pytest.raises(NotImplementedError): - InterfaceFactoryTemplate([]) - - def test_init_with_specific_interface_name(self, clear, mock_interface1, mock_interface2): - """Test initialization with specific interface name""" - # When - factory = InterfaceFactoryTemplate( - [mock_interface1, mock_interface2], interface_name='MockInterface2' - ) - - # Then - assert factory._current_interface == mock_interface2 - assert isinstance(factory(), mock_interface2) - - def test_init_passes_args_and_kwargs_to_interface(self, clear, mock_interface1): - """Test that args and kwargs are passed to interface constructor""" - # When - factory = InterfaceFactoryTemplate([mock_interface1], 'arg1', 'arg2', kwarg1='value1') - - # Then - interface_obj = factory() - assert interface_obj.args == ('arg1', 'arg2') - assert interface_obj.kwargs == {'kwarg1': 'value1'} - - def test_create_with_interface_name(self, factory_multiple_interfaces, mock_interface2): - """Test create method with specific interface name""" - # When - factory_multiple_interfaces.create(interface_name='MockInterface2') - - # Then - assert factory_multiple_interfaces._current_interface == mock_interface2 - assert isinstance(factory_multiple_interfaces(), mock_interface2) - - def test_create_without_interface_name_uses_first( - self, factory_multiple_interfaces, mock_interface1 - ): - """Test create method without interface name uses first interface""" - # When - factory_multiple_interfaces.create() - - # Then - assert factory_multiple_interfaces._current_interface == mock_interface1 - - def test_available_interfaces_property(self, factory_multiple_interfaces): - """Test available_interfaces property returns correct names""" - # When - interfaces = factory_multiple_interfaces.available_interfaces - - # Then - assert 'MockInterface1' in interfaces - assert 'MockInterface2' in interfaces - assert len(interfaces) == 2 - - def test_current_interface_property(self, factory_single_interface, mock_interface1): - """Test current_interface property returns correct interface class""" - # When/Then - assert factory_single_interface.current_interface == mock_interface1 - - def test_current_interface_name_property(self, factory_single_interface): - """Test current_interface_name property returns correct name""" - # When/Then - assert factory_single_interface.current_interface_name == 'MockInterface1' - - def test_switch_to_valid_interface(self, factory_multiple_interfaces, mock_interface2): - """Test switching to a valid interface""" - # When - factory_multiple_interfaces.switch('MockInterface2') - - # Then - assert factory_multiple_interfaces._current_interface == mock_interface2 - assert isinstance(factory_multiple_interfaces(), mock_interface2) - - def test_switch_to_invalid_interface_raises_error(self, factory_single_interface): - """Test switching to invalid interface raises AttributeError""" - # When/Then - with pytest.raises(AttributeError, match='The user supplied interface is not valid'): - factory_single_interface.switch('NonExistentInterface') - - def test_switch_with_fitter_having_fit_object_with_update_bindings( - self, factory_single_interface - ): - """Test switch with fitter that has _fit_object with update_bindings method""" - # Given - mock_fit_object = MagicMock() - mock_fit_object.update_bindings = MagicMock() - mock_fitter = MagicMock() - mock_fitter._fit_object = mock_fit_object - - # When - factory_single_interface.switch('MockInterface1', fitter=mock_fitter) - - # Then - mock_fit_object.update_bindings.assert_called_once() - - def test_switch_with_fitter_having_generate_bindings(self, factory_single_interface): - """Test switch with fitter that has generate_bindings method""" - # Given - mock_fitter = MagicMock() - mock_fitter.generate_bindings = MagicMock() - del mock_fitter._fit_object # Ensure _fit_object doesn't exist - - # When - factory_single_interface.switch('MockInterface1', fitter=mock_fitter) - - # Then - mock_fitter.generate_bindings.assert_called_once() - - def test_switch_with_fitter_exception_handling( - self, factory_single_interface, caplog: 'pytest.LogCaptureFixture' - ): - """Test switch handles exceptions in fitter binding updates gracefully""" - # Given - mock_fit_object = MagicMock() - mock_fit_object.update_bindings.side_effect = Exception('Test exception') - mock_fitter = MagicMock() - mock_fitter._fit_object = mock_fit_object - - # When - with caplog.at_level(logging.WARNING, logger='easyscience.fitting.calculators'): - factory_single_interface.switch('MockInterface1', fitter=mock_fitter) - - # Then - assert 'Unable to auto generate bindings' in caplog.text - assert 'Test exception' in caplog.text - - def test_fit_func_property_returns_callable(self, factory_single_interface): - """Test fit_func property returns a callable""" - # When - fit_func = factory_single_interface.fit_func - - # Then - assert callable(fit_func) - - def test_fit_func_calls_interface_fit_func(self, factory_single_interface): - """Test fit_func calls the underlying interface's fit_func""" - # When - result = factory_single_interface.fit_func('arg1', kwarg1='value1') - - # Then - assert result == 'result1' - - def test_call_method(self, factory_single_interface): - """Test call method delegates to fit_func""" - # When - result = factory_single_interface.call('arg1', kwarg1='value1') - - # Then - assert result == 'result1' - - def test_generate_bindings_with_matching_properties(self, factory_single_interface): - """Test generate_bindings method with matching properties""" - # Given - mock_model = MagicMock() - mock_prop = MagicMock() - mock_prop.name = 'test_param' - mock_prop.value = 42 - mock_model._get_linkable_attributes.return_value = [mock_prop] - - mock_item = MagicMock() - mock_item.name_conversion = {'test_param': 'internal_param'} - mock_item.make_prop.return_value = MagicMock() - - # Mock the interface object's create method - interface_obj = factory_single_interface() - interface_obj.create = MagicMock(return_value=[mock_item]) - - # When - factory_single_interface.generate_bindings(mock_model) - - # Then - mock_item.make_prop.assert_called_once_with('test_param') - assert mock_prop._callback is not None - - def test_generate_bindings_with_value_no_call_back_property(self, factory_single_interface): - """Test generate_bindings handles properties with value_no_call_back""" - # Given - mock_model = MagicMock() - mock_prop = MagicMock() - mock_prop.name = 'test_param' - mock_prop.value_no_call_back = 24 - mock_model._get_linkable_attributes.return_value = [mock_prop] - - mock_item = MagicMock() - mock_item.name_conversion = {'test_param': 'internal_param'} - mock_callback = MagicMock() - mock_item.make_prop.return_value = mock_callback - - # Mock the interface object's create method - interface_obj = factory_single_interface() - interface_obj.create = MagicMock(return_value=[mock_item]) - - # When - factory_single_interface.generate_bindings(mock_model) - - # Then - mock_callback.fset.assert_called_once_with(24) - - def test_generate_bindings_skips_non_matching_properties(self, factory_single_interface): - """Test generate_bindings skips properties not in name_conversion""" - # Given - mock_model = MagicMock() - mock_prop = MagicMock() - mock_prop.name = 'non_matching_param' - mock_model._get_linkable_attributes.return_value = [mock_prop] - - mock_item = MagicMock() - mock_item.name_conversion = {'different_param': 'internal_param'} - - # Mock the interface object's create method - interface_obj = factory_single_interface() - interface_obj.create = MagicMock(return_value=[mock_item]) - - # When - factory_single_interface.generate_bindings(mock_model) - - # Then - mock_item.make_prop.assert_not_called() - - def test_call_dunder_method_returns_interface_obj(self, factory_single_interface): - """Test __call__ method returns the interface object""" - # When - result1 = factory_single_interface() - result2 = factory_single_interface() - - # Then - assert result1 is not None - assert result1 is result2 # Should return the same object - - def test_reduce_method_for_pickle_support(self, factory_single_interface): - """Test __reduce__ method for pickle serialization support""" - # When - result = factory_single_interface.__reduce__() - - # Then - assert len(result) == 2 - assert result[0] == InterfaceFactoryTemplate.__state_restore__ - assert result[1][0] == InterfaceFactoryTemplate - assert result[1][1] == 'MockInterface1' - - def test_state_restore_static_method_implementation_issue( - self, clear, mock_interface1, mock_interface2 - ): - """Test __state_restore__ static method - demonstrates implementation issue""" - # Given - original_factory = InterfaceFactoryTemplate([mock_interface1, mock_interface2]) - - # When/Then - this implementation has a bug where it tries to call cls() without arguments - # which would fail since the constructor requires interface_list parameter - with pytest.raises(TypeError): - InterfaceFactoryTemplate.__state_restore__(InterfaceFactoryTemplate, 'MockInterface2') - - def test_return_name_static_method_with_name_attribute(self, mock_interface_with_custom_name): - """Test return_name static method with interface having name attribute""" - # When - name = InterfaceFactoryTemplate.return_name(mock_interface_with_custom_name) - - # Then - assert name == 'CustomName' - - def test_return_name_static_method_without_name_attribute(self, mock_interface1): - """Test return_name static method without name attribute uses __name__""" - # When - name = InterfaceFactoryTemplate.return_name(mock_interface1) - - # Then - assert name == 'MockInterface1' - - def test_switch_with_fitter_generate_bindings_exception( - self, factory_single_interface, caplog: 'pytest.LogCaptureFixture' - ): - """Test switch handles exceptions in fitter.generate_bindings gracefully""" - # Given - mock_fitter = MagicMock() - mock_fitter.generate_bindings.side_effect = Exception('Generate bindings test exception') - # Ensure _fit_object doesn't exist so it goes to the elif branch - if hasattr(mock_fitter, '_fit_object'): - del mock_fitter._fit_object - - # When - with caplog.at_level(logging.WARNING, logger='easyscience.fitting.calculators'): - factory_single_interface.switch('MockInterface1', fitter=mock_fitter) - - # Then - assert 'Unable to auto generate bindings' in caplog.text - assert 'Generate bindings test exception' in caplog.text - - def test_generate_bindings_with_property_without_value_no_call_back( - self, factory_single_interface - ): - """Test generate_bindings handles properties without value_no_call_back (descriptor objects)""" - # Given - mock_model = MagicMock() - mock_prop = MagicMock() - mock_prop.name = 'test_param' - mock_prop.value = 99 # This will be used since no value_no_call_back attribute - # Explicitly remove value_no_call_back to force the else branch - del mock_prop.value_no_call_back - mock_model._get_linkable_attributes.return_value = [mock_prop] - - mock_item = MagicMock() - mock_item.name_conversion = {'test_param': 'internal_param'} - mock_callback = MagicMock() - mock_item.make_prop.return_value = mock_callback - - interface_obj = factory_single_interface() - interface_obj.create = MagicMock(return_value=[mock_item]) - - # When - factory_single_interface.generate_bindings(mock_model) - - # Then - mock_callback.fset.assert_called_once_with(99) - - -class TestItemContainer: - @pytest.fixture - def mock_getter(self): - """Mock getter function""" - - def getter(link_name, key): - return f'got_{link_name}_{key}' - - return getter - - @pytest.fixture - def mock_setter(self): - """Mock setter function""" - - def setter(link_name, **kwargs): - pass - - return setter - - @pytest.fixture - def item_container(self, mock_getter, mock_setter): - """Create ItemContainer instance""" - return ItemContainer( - link_name='test_link', - name_conversion={'param1': 'internal_param1', 'param2': 'internal_param2'}, - getter_fn=mock_getter, - setter_fn=mock_setter, - ) - - def test_item_container_creation(self, item_container): - """Test ItemContainer can be created with required fields""" - # Then - assert item_container.link_name == 'test_link' - assert item_container.name_conversion == { - 'param1': 'internal_param1', - 'param2': 'internal_param2', - } - assert callable(item_container.getter_fn) - assert callable(item_container.setter_fn) - - def test_convert_key_with_existing_key(self, item_container): - """Test convert_key method with existing key""" - # When - result = item_container.convert_key('param1') - - # Then - assert result == 'internal_param1' - - def test_convert_key_with_non_existing_key(self, item_container): - """Test convert_key method with non-existing key returns None""" - # When - result = item_container.convert_key('non_existing') - - # Then - assert result is None - - def test_make_prop_returns_property(self, item_container): - """Test make_prop method returns a property object""" - # When - prop = item_container.make_prop('param1') - - # Then - assert isinstance(prop, property) - assert prop.fget is not None - assert prop.fset is not None - - def test_make_getter_function_behavior(self, item_container): - """Test the behavior of the __make_getter method indirectly""" - # When - prop = item_container.make_prop('param1') - - # Then - assert isinstance(prop, property) - assert prop.fget is not None - assert callable(prop.fget) - - def test_make_setter_function_behavior(self, item_container): - """Test the behavior of the __make_setter method indirectly""" - # Given - setter_calls = [] - - def mock_setter(link_name, **kwargs): - setter_calls.append((link_name, kwargs)) - - container = ItemContainer( - link_name='test_link', - name_conversion={'param1': 'internal_param1'}, - getter_fn=lambda x, y: None, - setter_fn=mock_setter, - ) - - # When - prop = container.make_prop('param1') - - # Then - assert isinstance(prop, property) - assert prop.fset is not None - assert callable(prop.fset) - - def test_convert_key_behavior_in_property_creation(self, item_container): - """Test that convert_key behavior is used correctly in property creation""" - # When - prop1 = item_container.make_prop('param1') # exists in name_conversion - prop2 = item_container.make_prop('non_existing_param') # doesn't exist - - # Then - # Both should create valid property objects - assert isinstance(prop1, property) - assert isinstance(prop2, property) - # The difference is in the internal key mapping which affects runtime behavior - - def test_actual_getter_setter_execution(self): - """Test that getter and setter functions are actually executed""" - # Given - getter_calls = [] - setter_calls = [] - - def mock_getter(link_name, key): - getter_calls.append((link_name, key)) - return f'value_for_{key}' - - def mock_setter(link_name, **kwargs): - setter_calls.append((link_name, kwargs)) - - container = ItemContainer( - link_name='test_link', - name_conversion={'param1': 'internal_param1'}, - getter_fn=mock_getter, - setter_fn=mock_setter, - ) - - # When - prop = container.make_prop('param1') - - # Execute the getter function directly (this covers the actual getter execution) - getter_func = prop.fget - getter_result = getter_func() # type: ignore - - # Execute the setter function directly (this covers the actual setter execution) - setter_func = prop.fset - setter_func('test_value') # type: ignore - - # Then - assert getter_result == 'value_for_internal_param1' - assert len(getter_calls) == 1 - assert getter_calls[0] == ('test_link', 'internal_param1') - - assert len(setter_calls) == 1 - assert setter_calls[0] == ('test_link', {'internal_param1': 'test_value'}) diff --git a/tests/unit/legacy/__init__.py b/tests/unit/fitting/minimizers/bumps_utils/__init__.py similarity index 99% rename from tests/unit/legacy/__init__.py rename to tests/unit/fitting/minimizers/bumps_utils/__init__.py index b1990ec8..4e798e20 100644 --- a/tests/unit/legacy/__init__.py +++ b/tests/unit/fitting/minimizers/bumps_utils/__init__.py @@ -1,3 +1,2 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - diff --git a/tests/unit/fitting/minimizers/bumps_utils/test_problem.py b/tests/unit/fitting/minimizers/bumps_utils/test_problem.py new file mode 100644 index 00000000..23912f55 --- /dev/null +++ b/tests/unit/fitting/minimizers/bumps_utils/test_problem.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for the shared BUMPS problem-construction helpers.""" + +from unittest.mock import MagicMock + +import numpy as np +import pytest + +import easyscience.fitting.minimizers.bumps_utils.problem +from easyscience.fitting.minimizers.bumps_utils import build_curve_problem +from easyscience.fitting.minimizers.bumps_utils import parameter_names +from easyscience.fitting.minimizers.bumps_utils import parameter_snapshot +from easyscience.fitting.minimizers.bumps_utils import to_bumps_parameter + + +class TestToBumpsParameter: + def test_convert_parameter_object(self) -> None: + from easyscience.variable import Parameter + + param = Parameter(42.0, min=0.0, max=100.0, display_name='thickness') + param.fixed = False + + result = to_bumps_parameter(param) + + # to_bumps_parameter uses obj.unique_name which is auto-assigned + assert result.name.startswith('p') + assert result.value == 42.0 + assert result.bounds == (0.0, 100.0) + assert result.fixed is False + + def test_convert_fixed_parameter(self) -> None: + from easyscience.variable import Parameter + + param = Parameter(5.0, min=0.0, max=20.0, display_name='roughness') + param.fixed = True + + result = to_bumps_parameter(param) + + assert result.name.startswith('p') + assert result.fixed is True + + +class TestBuildCurveProblem: + """Curve/FitProblem assembly, with the BUMPS classes mocked out.""" + + @pytest.fixture(autouse=True) + def _mock_bumps_classes(self, monkeypatch): + self.mock_curve_cls = MagicMock(return_value='curve') + self.mock_problem_cls = MagicMock(return_value='problem') + monkeypatch.setattr( + easyscience.fitting.minimizers.bumps_utils.problem, 'Curve', self.mock_curve_cls + ) + monkeypatch.setattr( + easyscience.fitting.minimizers.bumps_utils.problem, + 'FitProblem', + self.mock_problem_cls, + ) + self.mock_convert = MagicMock(side_effect=lambda par: f'converted-{par.unique_name}') + monkeypatch.setattr( + easyscience.fitting.minimizers.bumps_utils.problem, + 'to_bumps_parameter', + self.mock_convert, + ) + + @staticmethod + def _engine_with_cached_pars(cached_pars): + engine = MagicMock() + engine._generate_fit_function = MagicMock( + return_value=MagicMock(return_value=np.array([2.0])) + ) + engine._cached_pars = cached_pars + return engine + + def test_uses_cached_parameters_by_default(self): + cached_par = MagicMock() + cached_par.unique_name = 'alpha' + engine = self._engine_with_cached_pars({'alpha': cached_par}) + + problem, counter, curve = build_curve_problem( + engine, np.array([1.0]), np.array([2.0]), np.array([4.0]) + ) + + assert problem == 'problem' + # The Curve is surfaced directly so callers never have to read the + # deprecated ``FitProblem.fitness`` property (CR-1). + assert curve == 'curve' + engine._generate_fit_function.assert_called_once_with() + self.mock_convert.assert_called_once_with(cached_par) + assert self.mock_curve_cls.call_args.kwargs['palpha'] == 'converted-alpha' + self.mock_problem_cls.assert_called_once_with('curve') + + def test_curve_receives_data_and_dy(self): + """weights are converted to dy = 1 / weights.""" + engine = self._engine_with_cached_pars({}) + x = np.array([1.0, 2.0]) + y = np.array([10.0, 20.0]) + weights = np.array([2.0, 4.0]) + + build_curve_problem(engine, x, y, weights) + + call = self.mock_curve_cls.call_args + np.testing.assert_array_equal(call.args[1], x) + np.testing.assert_array_equal(call.args[2], y) + np.testing.assert_array_equal(call.kwargs['dy'], 1 / weights) + + def test_counter_wraps_fit_function(self): + """The returned EvalCounter wraps the wrapped fit function and counts calls.""" + inner = MagicMock(return_value=np.array([11.0, 22.0])) + engine = self._engine_with_cached_pars({}) + engine._generate_fit_function = MagicMock(return_value=inner) + + _, counter, _ = build_curve_problem( + engine, np.array([1.0]), np.array([2.0]), np.array([4.0]) + ) + + # The counter itself is what Curve receives as the fit function. + assert self.mock_curve_cls.call_args.args[0] is counter + assert counter.count == 0 + counter(np.array([1.0])) + assert counter.count == 1 + inner.assert_called_once() + + +class TestParameterNames: + def test_strips_prefix(self): + params = [] + for name in ('palpha', 'pbeta'): + p = MagicMock() + p.name = name + params.append(p) + problem = MagicMock() + problem._parameters = params + + assert parameter_names(problem) == ['alpha', 'beta'] + + def test_tolerates_none_name(self): + p = MagicMock() + p.name = None + problem = MagicMock() + problem._parameters = [p] + + assert parameter_names(problem) == [''] + + +class TestParameterSnapshot: + def test_snapshot_from_point(self) -> None: + mock_problem = MagicMock() + mock_problem.labels.return_value = ['palpha', 'pbeta'] + + point = np.array([1.5, 2.5]) + + snapshot = parameter_snapshot(mock_problem, point) + + assert snapshot == {'alpha': 1.5, 'beta': 2.5} + mock_problem.getp.assert_not_called() + + def test_snapshot_falls_back_to_getp(self) -> None: + mock_problem = MagicMock() + mock_problem.labels.return_value = ['palpha'] + mock_problem.getp.return_value = np.array([3.5]) + + snapshot = parameter_snapshot(mock_problem, None) + + assert snapshot == {'alpha': 3.5} + mock_problem.getp.assert_called_once() diff --git a/tests/unit/fitting/minimizers/test_factory.py b/tests/unit/fitting/minimizers/test_factory.py index 8ee4df6c..4be4dd4c 100644 --- a/tests/unit/fitting/minimizers/test_factory.py +++ b/tests/unit/fitting/minimizers/test_factory.py @@ -5,14 +5,13 @@ import pytest -from easyscience import AvailableMinimizers -from easyscience.fitting.available_minimizers import from_string_to_enum +from easyscience import Minimizers from easyscience.fitting.minimizers import MinimizerBase from easyscience.fitting.minimizers.factory import factory class TestFactory: - def pull_minminizer(self, minimizer: AvailableMinimizers) -> MinimizerBase: + def pull_minminizer(self, minimizer: Minimizers) -> MinimizerBase: mock_fit_object = MagicMock() mock_fit_function = MagicMock() minimizer = factory(minimizer, mock_fit_object, mock_fit_function) @@ -21,12 +20,12 @@ def pull_minminizer(self, minimizer: AvailableMinimizers) -> MinimizerBase: @pytest.mark.parametrize( 'minimizer_method,minimizer_enum', [ - ('leastsq', AvailableMinimizers.LMFit), - ('leastsq', AvailableMinimizers.LMFit_leastsq), - ('powell', AvailableMinimizers.LMFit_powell), - ('cobyla', AvailableMinimizers.LMFit_cobyla), - ('differential_evolution', AvailableMinimizers.LMFit_differential_evolution), - ('least_squares', AvailableMinimizers.LMFit_scipy_least_squares), + ('leastsq', Minimizers.LMFit), + ('leastsq', Minimizers.LMFit_leastsq), + ('powell', Minimizers.LMFit_powell), + ('cobyla', Minimizers.LMFit_cobyla), + ('differential_evolution', Minimizers.LMFit_differential_evolution), + ('least_squares', Minimizers.LMFit_scipy_least_squares), ], ) def test_factory_lm_fit(self, minimizer_method, minimizer_enum): @@ -37,10 +36,10 @@ def test_factory_lm_fit(self, minimizer_method, minimizer_enum): @pytest.mark.parametrize( 'minimizer_method,minimizer_enum', [ - ('amoeba', AvailableMinimizers.Bumps), - ('amoeba', AvailableMinimizers.Bumps_simplex), - ('newton', AvailableMinimizers.Bumps_newton), - ('lm', AvailableMinimizers.Bumps_lm), + ('amoeba', Minimizers.Bumps), + ('amoeba', Minimizers.Bumps_simplex), + ('newton', Minimizers.Bumps_newton), + ('lm', Minimizers.Bumps_lm), ], ) def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): @@ -50,61 +49,9 @@ def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): @pytest.mark.parametrize( 'minimizer_method,minimizer_enum', - [('leastsq', AvailableMinimizers.DFO), ('leastsq', AvailableMinimizers.DFO_leastsq)], + [('leastsq', Minimizers.DFO), ('leastsq', Minimizers.DFO_leastsq)], ) def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): minimizer = self.pull_minminizer(minimizer_enum) assert minimizer._method == minimizer_method assert minimizer.package == 'dfo' - - -@pytest.mark.parametrize( - 'minimizer_name,expected', - [ - ('LMFit', AvailableMinimizers.LMFit), - ('LMFit_leastsq', AvailableMinimizers.LMFit_leastsq), - ('LMFit_powell', AvailableMinimizers.LMFit_powell), - ('LMFit_cobyla', AvailableMinimizers.LMFit_cobyla), - ('LMFit_differential_evolution', AvailableMinimizers.LMFit_differential_evolution), - ('LMFit_scipy_least_squares', AvailableMinimizers.LMFit_scipy_least_squares), - ], -) -def test_from_string_to_enum_lmfit(minimizer_name, expected): - assert from_string_to_enum(minimizer_name) == expected - - -@pytest.mark.parametrize( - 'minimizer_name,expected', - [ - ('Bumps', AvailableMinimizers.Bumps), - ('Bumps_simplex', AvailableMinimizers.Bumps_simplex), - ('Bumps_newton', AvailableMinimizers.Bumps_newton), - ('Bumps_lm', AvailableMinimizers.Bumps_lm), - ], -) -def test_from_string_to_enum_bumps(minimizer_name, expected): - assert from_string_to_enum(minimizer_name) == expected - - -@pytest.mark.parametrize( - 'minimizer_name,expected', - [('DFO', AvailableMinimizers.DFO), ('DFO_leastsq', AvailableMinimizers.DFO_leastsq)], -) -def test_from_string_to_enum_dfo(minimizer_name, expected): - assert from_string_to_enum(minimizer_name) == expected - - -def test_available_minimizers(): - assert AvailableMinimizers.LMFit - assert AvailableMinimizers.LMFit_leastsq - assert AvailableMinimizers.LMFit_powell - assert AvailableMinimizers.LMFit_cobyla - assert AvailableMinimizers.LMFit_differential_evolution - assert AvailableMinimizers.LMFit_scipy_least_squares - assert AvailableMinimizers.Bumps - assert AvailableMinimizers.Bumps_simplex - assert AvailableMinimizers.Bumps_newton - assert AvailableMinimizers.Bumps_lm - assert AvailableMinimizers.DFO - assert AvailableMinimizers.DFO_leastsq - assert len(AvailableMinimizers) == 12 diff --git a/tests/unit/fitting/minimizers/test_minimizer_base.py b/tests/unit/fitting/minimizers/test_minimizer_base.py index 86ec4292..467264f4 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_base.py +++ b/tests/unit/fitting/minimizers/test_minimizer_base.py @@ -61,7 +61,7 @@ def test_evaluate(self, minimizer: MinimizerBase): # Then result = minimizer.evaluate( 'x', - minimizer_parameters={'parms_key': 'parms_val'}, + parameters={'parms_key': 'parms_val'}, kwargs={'kwargs_key': 'kwargs_val'}, ) @@ -84,7 +84,7 @@ def test_evaluate_no_fit_function(self, minimizer: MinimizerBase): # Then minimizer.evaluate( 'x', - minimizer_parameters={'parms_key': 'parms_val'}, + parameters={'parms_key': 'parms_val'}, kwargs={'kwargs_key': 'kwargs_val'}, ) @@ -108,11 +108,11 @@ def test_evaluate_no_parameters(self, minimizer: MinimizerBase): def test_evaluate_exception(self, minimizer: MinimizerBase): # When - minimizer_parameters = 'not dict type' + parameters = 'not dict type' # Then Expect with pytest.raises(TypeError): - minimizer.evaluate('x', minimizer_parameters=minimizer_parameters) + minimizer.evaluate('x', parameters=parameters) def test_prepare_parameters(self, minimizer: MinimizerBase): # When @@ -142,7 +142,7 @@ def test_generate_fit_function(self, minimizer: MinimizerBase) -> None: mock_parm_2.unique_name = 'mock_parm_2' mock_parm_2.value = 2.0 mock_parm_2.error = 0.2 - minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) + minimizer._object.get_free_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) # Then fit_function = minimizer._generate_fit_function() diff --git a/tests/unit/fitting/minimizers/test_minimizer_bumps.py b/tests/unit/fitting/minimizers/test_minimizer_bumps.py index 28d4a049..93e42aac 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_bumps.py +++ b/tests/unit/fitting/minimizers/test_minimizer_bumps.py @@ -41,16 +41,19 @@ def test_all_methods(self, minimizer: Bumps) -> None: # When Then Expect assert minimizer.all_methods() == ['amoeba', 'de', 'dream', 'newton', 'lm'] + def test_all_methods_returns_a_copy(self, minimizer: Bumps) -> None: + """Callers must not be able to mutate the module-level list in place.""" + methods = minimizer.all_methods() + methods.append('tampered') + + assert 'tampered' not in minimizer.all_methods() + def test_supported_methods(self, minimizer: Bumps) -> None: # When Then Expect assert set(minimizer.supported_methods()) == set(['newton', 'lm', 'amoeba']) def test_fit(self, minimizer: Bumps, monkeypatch) -> None: # When - from easyscience import global_object - - global_object.stack.enabled = False - # Mock FitDriver. driver.fit() returns (x, fx); driver.stderr() returns dx. mock_driver_instance = MagicMock() mock_driver_instance.clip = MagicMock() @@ -65,17 +68,17 @@ def test_fit(self, minimizer: Bumps, monkeypatch) -> None: # Prepare a mock parameter with .name = 'pmock_parm_1' mock_bumps_param = MagicMock() mock_bumps_param.name = 'pmock_parm_1' - # Patch FitProblem to have _parameters attribute as expected - mock_FitProblem_instance = MagicMock() - mock_FitProblem_instance._parameters = [mock_bumps_param] - mock_FitProblem = MagicMock(return_value=mock_FitProblem_instance) + # A mock problem with _parameters, plus the Curve model returned + # directly by the helper (never via the deprecated problem.fitness) + mock_model = MagicMock() + mock_problem = MagicMock() + mock_problem._parameters = [mock_bumps_param] + mock_counter = MagicMock() + mock_build = MagicMock(return_value=(mock_problem, mock_counter, mock_model)) monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_bumps, 'FitProblem', mock_FitProblem + easyscience.fitting.minimizers.minimizer_bumps, 'build_curve_problem', mock_build ) - mock_model = MagicMock() - mock_model_function = MagicMock(return_value=mock_model) - minimizer._make_model = MagicMock(return_value=mock_model_function) minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') cached_par = MagicMock() @@ -84,11 +87,10 @@ def test_fit(self, minimizer: Bumps, monkeypatch) -> None: minimizer._cached_pars = cached_pars minimizer._cached_pars_vals = {'mock_parm_1': (1, 0.0)} - # Patch _set_parameter_fit_result - def fake_set_parameter_fit_result(fit_result, stack_status, par_list): - for index, name in enumerate([par.name for par in par_list]): - dict_name = name[len('p') :] - minimizer._cached_pars[dict_name].value = fit_result.x[index] + # Patch _set_parameter_fit_result. It now receives prefix-stripped names. + def fake_set_parameter_fit_result(fit_result, par_names): + for index, name in enumerate(par_names): + minimizer._cached_pars[name].value = fit_result.x[index] minimizer._set_parameter_fit_result = fake_set_parameter_fit_result @@ -104,7 +106,17 @@ def fake_set_parameter_fit_result(fit_result, stack_status, par_list): mock_FitDriver.assert_called_once() mock_driver_instance.clip.assert_called_once() mock_driver_instance.fit.assert_called_once() - minimizer._make_model.assert_called_once_with(parameters=None) + # The problem is built via the shared helper and its Curve is cached + mock_build.assert_called_once() + build_args = mock_build.call_args + assert build_args.args[0] is minimizer + assert np.array_equal(build_args.args[1], np.asarray(1.0)) + assert np.array_equal(build_args.args[2], np.asarray(2.0)) + assert np.array_equal(build_args.args[3], np.asarray(1)) + assert build_args.kwargs == {} + assert minimizer._eval_counter is mock_counter + assert minimizer._cached_model is mock_model + assert mock_FitDriver.call_args.kwargs['problem'] is mock_problem # _gen_fit_results is called with the OptimizeResult built from driver.fit() minimizer._gen_fit_results.assert_called_once() passed_result = minimizer._gen_fit_results.call_args.args[0] @@ -114,8 +126,6 @@ def fake_set_parameter_fit_result(fit_result, stack_status, par_list): 'max_evaluations': None, 'tolerance': None, } - mock_model_function.assert_called_once_with(1.0, 2.0, 1) - mock_FitProblem.assert_called_once_with(mock_model) @pytest.mark.parametrize( 'weights', @@ -134,35 +144,7 @@ def test_fit_weight_exceptions(self, minimizer: Bumps, weights) -> None: with pytest.raises(ValueError): minimizer.fit(x=np.array([1, 2, 3]), y=np.array([1, 2, 3]), weights=weights) - def test_make_model(self, minimizer: Bumps, monkeypatch) -> None: - # When - mock_fit_function = MagicMock(return_value=np.array([11, 22])) - minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) - - mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' - minimizer.convert_to_par_object = MagicMock(return_value='converted_parm_1') - - mock_Curve = MagicMock(return_value='curve') - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, 'Curve', mock_Curve) - - # Then - model = minimizer._make_model(parameters=[mock_parm_1]) - curve_for_model = model( - x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200]) - ) - wrapped_fit_function = mock_Curve.call_args[0][0] - wrapped_fit_function(np.array([1, 2]), pmock_parm_1=3) - - # Expect - minimizer._generate_fit_function.assert_called_once_with() - assert minimizer._eval_counter is wrapped_fit_function - assert minimizer._eval_counter.count == 1 - assert all(mock_Curve.call_args[0][1] == np.array([1, 2])) - assert all(mock_Curve.call_args[0][2] == np.array([10, 20])) - assert curve_for_model == 'curve' - - def test_set_parameter_fit_result_no_stack_status(self, minimizer: Bumps): + def test_set_parameter_fit_result(self, minimizer: Bumps): # When minimizer._cached_pars = { 'a': MagicMock(), @@ -179,15 +161,8 @@ def test_set_parameter_fit_result_no_stack_status(self, minimizer: Bumps): mock_fit_result.x = np.array([1.0, 2.0]) mock_fit_result.dx = np.array([0.1, 0.2]) - # The new argument: par_list (list of mock parameters) - mock_par_a = MagicMock() - mock_par_a.name = 'pa' - mock_par_b = MagicMock() - mock_par_b.name = 'pb' - par_list = [mock_par_a, mock_par_b] - - # Then - minimizer._set_parameter_fit_result(mock_fit_result, False, par_list) + # Then - names arrive already stripped of the minimizer prefix + minimizer._set_parameter_fit_result(mock_fit_result, ['a', 'b']) # Expect assert minimizer._cached_pars['a'].value == 1.0 @@ -195,6 +170,21 @@ def test_set_parameter_fit_result_no_stack_status(self, minimizer: Bumps): assert minimizer._cached_pars['b'].value == 2.0 assert minimizer._cached_pars['b'].error == 0.2 + def test_set_parameter_fit_result_without_stderr(self, minimizer: Bumps): + """Fitters that cannot produce a covariance hand back ``dx=None``; + those parameters get ``error=None`` (no uncertainty information) + rather than a misleading ``0.0``.""" + minimizer._cached_pars = {'a': MagicMock()} + + mock_fit_result = MagicMock() + mock_fit_result.x = np.array([1.0]) + mock_fit_result.dx = None + + minimizer._set_parameter_fit_result(mock_fit_result, ['a']) + + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error is None + def test_gen_fit_results( self, minimizer: Bumps, monkeypatch, caplog: 'pytest.LogCaptureFixture' ): @@ -258,7 +248,7 @@ def test_gen_fit_results( assert domain_fit_results.fit_args is None assert domain_fit_results.engine_result == mock_fit_result minimizer.evaluate.assert_called_once_with( - 'x', minimizer_parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} + 'x', parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} ) @pytest.mark.parametrize( @@ -302,6 +292,51 @@ def test_gen_fit_results_max_evaluations_boundary( assert mock_domain_fit_results.success is expected_success + def test_gen_fit_results_applies_extra_kwargs(self, minimizer: Bumps) -> None: + """Extra kwargs land on a real FitResults. Guarding the copy on the + current value instead of `hasattr` would drop every one of them, since + all FitResults fields start out falsy.""" + mock_cached_model = MagicMock() + mock_cached_model.x = np.array([1.0]) + mock_cached_model.y = np.array([2.0]) + mock_cached_model.dy = np.array([1.0]) + mock_cached_model.pars = {'ppar_1': 0} + minimizer._cached_model = mock_cached_model + minimizer._cached_pars = {'par_1': MagicMock(value=1.0)} + minimizer._p_0 = {} + minimizer._eval_counter = None + minimizer.evaluate = MagicMock(return_value=np.array([2.0])) + + mock_fit_result = MagicMock() + mock_fit_result.success = True + mock_fit_result.nit = 1 + + results = minimizer._gen_fit_results(mock_fit_result, x_matrices='copied') + + assert results.x_matrices == 'copied' + + def test_gen_fit_results_propagates_failure_message(self, minimizer: Bumps) -> None: + mock_cached_model = MagicMock() + mock_cached_model.x = np.array([1.0]) + mock_cached_model.y = np.array([2.0]) + mock_cached_model.dy = np.array([1.0]) + mock_cached_model.pars = {'ppar_1': 0} + minimizer._cached_model = mock_cached_model + minimizer._cached_pars = {'par_1': MagicMock(value=1.0)} + minimizer._p_0 = {} + minimizer._eval_counter = None + minimizer.evaluate = MagicMock(return_value=np.array([2.0])) + + mock_fit_result = MagicMock() + mock_fit_result.success = False + mock_fit_result.nit = 1 + mock_fit_result.message = 'Fit aborted before convergence' + + results = minimizer._gen_fit_results(mock_fit_result) + + assert results.success is False + assert results.message == 'Fit aborted before convergence' + def test_resolve_fitclass_valid(self, minimizer: Bumps) -> None: # When Then fitclass = Bumps._resolve_fitclass('lm') @@ -316,10 +351,6 @@ def test_resolve_fitclass_invalid(self, minimizer: Bumps) -> None: def test_fit_progress_callback(self, minimizer: Bumps, monkeypatch) -> None: # When - from easyscience import global_object - - global_object.stack.enabled = False - progress_callback = MagicMock(return_value=True) mock_driver_instance = MagicMock() @@ -334,16 +365,14 @@ def test_fit_progress_callback(self, minimizer: Bumps, monkeypatch) -> None: mock_bumps_param = MagicMock() mock_bumps_param.name = 'pmock_parm_1' - mock_FitProblem_instance = MagicMock() - mock_FitProblem_instance._parameters = [mock_bumps_param] - mock_FitProblem = MagicMock(return_value=mock_FitProblem_instance) + mock_problem = MagicMock() + mock_problem._parameters = [mock_bumps_param] monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_bumps, 'FitProblem', mock_FitProblem + easyscience.fitting.minimizers.minimizer_bumps, + 'build_curve_problem', + MagicMock(return_value=(mock_problem, MagicMock(), MagicMock())), ) - mock_model = MagicMock() - mock_model_function = MagicMock(return_value=mock_model) - minimizer._make_model = MagicMock(return_value=mock_model_function) minimizer._set_parameter_fit_result = MagicMock() minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') @@ -363,70 +392,10 @@ def test_fit_progress_callback(self, minimizer: Bumps, monkeypatch) -> None: monitors = driver_call_kwargs.kwargs.get('monitors', driver_call_kwargs[1].get('monitors')) assert len(monitors) == 1 assert isinstance(monitors[0], BumpsProgressMonitor) - assert monitors[0]._problem is mock_FitProblem_instance + assert monitors[0]._problem is mock_problem assert monitors[0]._callback is progress_callback assert monitors[0]._payload_builder == minimizer._build_progress_payload - def test_fit_uses_supplied_model_and_optional_kwargs( - self, minimizer: Bumps, monkeypatch - ) -> None: - from easyscience import global_object - - global_object.stack.enabled = False - - mock_driver_instance = MagicMock() - mock_driver_instance.clip = MagicMock() - mock_driver_instance.fit = MagicMock(return_value=(np.array([3.0]), 0.0)) - mock_driver_instance.stderr = MagicMock(return_value=np.array([0.1])) - mock_driver_instance.monitor_runner.history.step = [0] - mock_FitDriver = MagicMock(return_value=mock_driver_instance) - monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_bumps, 'FitDriver', mock_FitDriver - ) - - mock_bumps_param = MagicMock() - mock_bumps_param.name = 'pmock_parm_1' - mock_problem = MagicMock() - mock_problem._parameters = [mock_bumps_param] - monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_bumps, - 'FitProblem', - MagicMock(return_value=mock_problem), - ) - - minimizer._make_model = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - minimizer._resolve_fitclass = MagicMock(return_value=MagicMock(id='amoeba')) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._cached_pars = {'mock_parm_1': MagicMock(value=1.0)} - minimizer._cached_pars_vals = {'mock_parm_1': (1.0, 0.0)} - - supplied_model = MagicMock() - minimizer_kwargs = {'existing_option': 'minimizer'} - engine_kwargs = {'engine_option': 'engine'} - - result = minimizer.fit( - x=np.array([1.0]), - y=np.array([2.0]), - weights=np.array([1.0]), - model=supplied_model, - tolerance=0.25, - max_evaluations=7, - minimizer_kwargs=minimizer_kwargs, - engine_kwargs=engine_kwargs, - ) - - assert result == 'gen_fit_results' - minimizer._make_model.assert_not_called() - fit_driver_kwargs = mock_FitDriver.call_args.kwargs - assert fit_driver_kwargs['problem'] is mock_problem - assert fit_driver_kwargs['existing_option'] == 'minimizer' - assert fit_driver_kwargs['engine_option'] == 'engine' - assert fit_driver_kwargs['ftol'] == 0.25 - assert fit_driver_kwargs['xtol'] == 0.25 - assert fit_driver_kwargs['steps'] == 7 - mock_driver_instance.fit.assert_called_once() - def test_fit_rejects_non_callable_progress_callback( self, minimizer: Bumps, monkeypatch ) -> None: @@ -442,7 +411,6 @@ def test_fit_rejects_non_callable_progress_callback( x=np.array([1.0]), y=np.array([2.0]), weights=np.array([1.0]), - model=MagicMock(), progress_callback='not-callable', ) @@ -522,60 +490,6 @@ def test_build_progress_payload_reduced_chi2_positive_dof(self, minimizer: Bumps ((), {'nllf': 5.0, 'norm': True}), ] - def test_current_parameter_snapshot(self, minimizer: Bumps) -> None: - # When - mock_problem = MagicMock() - mock_problem.labels.return_value = ['palpha', 'pbeta'] - - point = np.array([1.5, 2.5]) - - # Then - snapshot = minimizer._current_parameter_snapshot(mock_problem, point) - - # Expect - assert snapshot == {'alpha': 1.5, 'beta': 2.5} - - @pytest.mark.parametrize('par_list', [None, [MagicMock(unique_name='alpha')]]) - def test_convert_to_pars_obj_optional_parameter_list( - self, minimizer: Bumps, par_list, monkeypatch - ) -> None: - object_parameters = [MagicMock(unique_name='beta')] - minimizer._object = MagicMock() - minimizer._object.get_fit_parameters = MagicMock(return_value=object_parameters) - monkeypatch.setattr( - Bumps, - 'convert_to_par_object', - staticmethod(lambda parameter: parameter.unique_name), - ) - - converted = minimizer.convert_to_pars_obj(par_list) - - expected_parameters = object_parameters if par_list is None else par_list - assert converted == [parameter.unique_name for parameter in expected_parameters] - if par_list is None: - minimizer._object.get_fit_parameters.assert_called_once_with() - else: - minimizer._object.get_fit_parameters.assert_not_called() - - def test_make_model_without_parameters_uses_cached_parameters( - self, minimizer: Bumps, monkeypatch - ) -> None: - minimizer._generate_fit_function = MagicMock( - return_value=MagicMock(return_value=np.array([2.0])) - ) - minimizer._cached_pars = {'alpha': MagicMock(value=1.0)} - minimizer.convert_to_par_object = MagicMock(return_value='converted-alpha') - - mock_curve = MagicMock(return_value='curve') - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, 'Curve', mock_curve) - - model = minimizer._make_model() - curve = model(np.array([1.0]), np.array([2.0]), np.array([3.0])) - - assert curve == 'curve' - minimizer.convert_to_par_object.assert_called_once_with(minimizer._cached_pars['alpha']) - assert mock_curve.call_args.kwargs['palpha'] == 'converted-alpha' - def test_bumps_progress_monitor_calls_callback(self, minimizer: Bumps) -> None: # When callback = MagicMock(return_value=True) @@ -603,10 +517,6 @@ def test_bumps_progress_monitor_calls_callback(self, minimizer: Bumps) -> None: def test_fit_exception_restores_values(self, minimizer: Bumps, monkeypatch) -> None: # When - from easyscience import global_object - - global_object.stack.enabled = False - from easyscience.variable import Parameter parameter = MagicMock(Parameter) @@ -622,16 +532,13 @@ def test_fit_exception_restores_values(self, minimizer: Bumps, monkeypatch) -> N easyscience.fitting.minimizers.minimizer_bumps, 'FitDriver', mock_FitDriver ) - mock_FitProblem_instance = MagicMock() - mock_FitProblem_instance._parameters = [] - mock_FitProblem = MagicMock(return_value=mock_FitProblem_instance) + mock_problem = MagicMock() + mock_problem._parameters = [] monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_bumps, 'FitProblem', mock_FitProblem + easyscience.fitting.minimizers.minimizer_bumps, + 'build_curve_problem', + MagicMock(return_value=(mock_problem, MagicMock(), MagicMock())), ) - - mock_model = MagicMock() - mock_model_function = MagicMock(return_value=mock_model) - minimizer._make_model = MagicMock(return_value=mock_model_function) minimizer._resolve_fitclass = MagicMock(return_value=MagicMock(id='amoeba')) # Then Expect @@ -683,462 +590,113 @@ def test_gen_fit_results_uses_nit_for_budget_check( # =================================================================== -# Bumps.mcmc_sample() β€” Bayesian DREAM sampling +# fit() β€” tolerance / budget defaults are reported, never forced # =================================================================== -class TestBumpsSample: - """Tests for the ``Bumps.mcmc_sample()`` method and its helpers.""" - - # Sentinel value to signal "set fitter.state = None" in _setup_driver_mock - ABORT = object() +class TestFitToleranceAndBudgetDefaults: + """BUMPS pairs an independent ftol/xtol default per fitter. Resolving + them for reporting must not push a single collapsed value back into the + fitter, which would silently tighten its convergence criteria.""" @pytest.fixture def minimizer(self) -> Bumps: return Bumps( obj='obj', fit_function='fit_function', - minimizer_enum=MagicMock(package='bumps', method='amoeba'), - ) - - @pytest.fixture(autouse=True) - def _mock_bumps_internals(self, monkeypatch): - """Prevent sample() from constructing real BUMPS objects. - - ``sample()`` imports ``DreamFit`` and ``FitProblem`` from the real - ``bumps`` package internally, which would try to build real model - objects. We redirect those to mocks and also mock ``FitDriver`` - (which *is* a module-level import) so the whole flow stays under - test control. - - Also mock ``_make_model`` on the class so that the ``minimizer`` - fixture (which uses ``obj='obj'``) doesn't fail inside ``sample()``. - """ - import bumps.fitters - import bumps.names - - monkeypatch.setattr(bumps.fitters, 'DreamFit', MagicMock()) - monkeypatch.setattr(bumps.names, 'FitProblem', MagicMock(return_value=MagicMock())) - monkeypatch.setattr( - Bumps, '_make_model', MagicMock(return_value=MagicMock(return_value=MagicMock())) + minimizer_enum=MagicMock(package='bumps', method='newton'), ) - def _setup_driver_mock( - self, monkeypatch, fitter_state_value=None, fit_result=None, fit_side_effect=None - ): - """Helper to create a mocked FitDriver with configurable behavior. - - :param fitter_state_value: If ``None``, ``driver.fitter.state`` will be - a regular MagicMock (non-None). Pass ``ABORT`` to set it to ``None`` - and simulate user abort. - """ - from easyscience import global_object - - global_object.stack.enabled = False - + @staticmethod + def _patch_driver_and_problem(minimizer: Bumps, monkeypatch) -> MagicMock: mock_driver = MagicMock() - mock_driver.clip = MagicMock() - - if fit_side_effect is not None: - mock_driver.fit.side_effect = fit_side_effect - else: - mock_driver.fit.return_value = fit_result or (np.array([1.0]), 0.0) - + mock_driver.fit = MagicMock(return_value=(np.array([42.0]), 0.0)) mock_driver.stderr = MagicMock(return_value=np.array([0.1])) - - if fitter_state_value is TestBumpsSample.ABORT: - mock_driver.fitter.state = None - else: - mock_state = MagicMock() - mock_state.Nvar = 1 - mock_state.Npop = 5 - mock_state.labels = ['p_param_0'] - mock_draw = MagicMock() - mock_draw.points = np.array([[1.0]]) - mock_draw.logp = np.array([0.5]) - mock_state.draw.return_value = mock_draw - mock_driver.fitter.state = mock_state - + mock_driver.monitor_runner.history.step = [0] mock_FitDriver = MagicMock(return_value=mock_driver) monkeypatch.setattr( easyscience.fitting.minimizers.minimizer_bumps, 'FitDriver', mock_FitDriver ) - return mock_FitDriver, mock_driver - - @pytest.mark.parametrize( - 'kwargs, match', - [ - ({'samples': 0}, 'samples must be a positive integer'), - ({'samples': -1}, 'samples must be a positive integer'), - ({'burn': -1}, 'burn must be a non-negative integer'), - ({'thin': 0}, 'thin must be a positive integer'), - ], - ) - def test_sample_invalid_args(self, minimizer: Bumps, kwargs, match) -> None: - """Invalid samples/burn/thin values raise ValueError before any sampling. - - This is the single source of truth for these checks β€” the higher-level - ``Sampler`` relies on it. - """ - with pytest.raises(ValueError, match=match): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=kwargs.get('samples', 10), - burn=kwargs.get('burn', 0), - thin=kwargs.get('thin', 1), - ) - - @pytest.mark.parametrize( - 'overrides, match', - [ - ({'y': np.array([0.1])}, 'x and y must have the same shape'), - ({'x': np.array([1.0, np.nan])}, 'x cannot contain NaN'), - ({'y': np.array([0.1, np.inf])}, 'y cannot contain NaN'), - ({'weights': np.array([1.0])}, 'Weights must have the same shape'), - ({'weights': np.array([1.0, np.nan])}, 'Weights cannot be NaN'), - ({'weights': np.array([1.0, 0.0])}, 'Weights must be strictly positive'), - ], - ) - def test_sample_invalid_data(self, minimizer: Bumps, overrides, match) -> None: - """Shape mismatches and non-finite/non-positive data raise ValueError - before any sampling.""" - data = { - 'x': np.array([1.0, 2.0]), - 'y': np.array([0.1, 0.2]), - 'weights': np.array([1.0, 1.0]), - } - data.update(overrides) - with pytest.raises(ValueError, match=match): - minimizer.mcmc_sample(**data, samples=10, burn=0, thin=1) - - def test_sample_basic(self, minimizer: Bumps, monkeypatch) -> None: - """Verify that mcmc_sample() returns a dict with expected keys.""" - mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - - result = minimizer.mcmc_sample( - x=np.array([1.0, 2.0]), - y=np.array([0.1, 0.2]), - weights=np.array([1.0, 1.0]), - samples=100, - burn=20, - thin=2, - population=5, - ) - assert isinstance(result, dict) - assert 'draws' in result - assert 'param_names' in result - assert 'internal_bumps_object' in result - assert 'logp' in result - mock_FitDriver.assert_called_once() - - def test_sample_with_progress_callback(self, minimizer: Bumps, monkeypatch) -> None: - """Verify progress callback is wired up as a monitor.""" - mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - progress_callback = MagicMock() - - result = minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=5, - thin=1, - progress_callback=progress_callback, + mock_problem = MagicMock() + mock_problem._parameters = [] + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_bumps, + 'build_curve_problem', + MagicMock(return_value=(mock_problem, MagicMock(count=3), MagicMock())), ) - assert result is not None - call_kwargs = mock_FitDriver.call_args.kwargs - assert 'monitors' in call_kwargs - assert len(call_kwargs['monitors']) == 1 - assert isinstance(call_kwargs['monitors'][0], BumpsProgressMonitor) - - def test_sample_aborted_by_user_raises_fit_error(self, minimizer: Bumps, monkeypatch) -> None: - """Verify that sampling abortion raises FitError.""" - self._setup_driver_mock(monkeypatch, fitter_state_value=TestBumpsSample.ABORT) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - - with pytest.raises(FitError, match='Sampling aborted by user'): - minimizer.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + minimizer._gen_fit_results = MagicMock(return_value='result') + minimizer._set_parameter_fit_result = MagicMock() + minimizer._cached_pars = {} + minimizer._cached_pars_vals = {} + return mock_FitDriver - def test_sample_driver_exception_restores_parameters( + def test_tolerance_none_does_not_override_fitter_defaults( self, minimizer: Bumps, monkeypatch ) -> None: - """Verify that a driver exception during sampling restores parameter values.""" - self._setup_driver_mock(monkeypatch, fit_side_effect=RuntimeError('driver failed')) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - minimizer._restore_parameter_values = MagicMock() + mock_FitDriver = self._patch_driver_and_problem(minimizer, monkeypatch) - with pytest.raises(RuntimeError, match='driver failed'): - minimizer.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + minimizer.fit(x=np.array([1.0]), y=np.array([2.0]), weights=np.array([1.0])) - minimizer._restore_parameter_values.assert_called_once() + # The real 'newton' settings are ftol=1e-6 / xtol=1e-12. Neither may be + # forwarded, or BUMPS would run against a tolerance the caller never asked for. + driver_kwargs = mock_FitDriver.call_args.kwargs + assert 'ftol' not in driver_kwargs + assert 'xtol' not in driver_kwargs + assert 'steps' not in driver_kwargs + + # ...but the resolved defaults are still reported for the budget check. + gen_kwargs = minimizer._gen_fit_results.call_args.kwargs + assert gen_kwargs['tolerance'] == 1e-12 # min(ftol, xtol) + assert gen_kwargs['max_evaluations'] == 3000 # 'newton' default steps - def test_sample_population_param(self, minimizer: Bumps, monkeypatch) -> None: - """population kwarg is forwarded to DREAM as pop.""" - mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + def test_explicit_tolerance_is_forwarded(self, minimizer: Bumps, monkeypatch) -> None: + mock_FitDriver = self._patch_driver_and_problem(minimizer, monkeypatch) - minimizer.mcmc_sample( + minimizer.fit( x=np.array([1.0]), - y=np.array([0.1]), + y=np.array([2.0]), weights=np.array([1.0]), - samples=10, - burn=0, - thin=1, - population=7, + tolerance=1e-3, + max_evaluations=11, ) - call_kwargs = mock_FitDriver.call_args.kwargs - assert call_kwargs['pop'] == 7 + driver_kwargs = mock_FitDriver.call_args.kwargs + assert driver_kwargs['ftol'] == 1e-3 + assert driver_kwargs['xtol'] == 1e-3 + assert driver_kwargs['steps'] == 11 - def test_sample_sampler_kwargs_forwarded(self, minimizer: Bumps, monkeypatch) -> None: - """sampler_kwargs entries are merged into the DREAM kwargs.""" - mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + def test_minimizer_kwargs_is_not_mutated(self, minimizer: Bumps, monkeypatch) -> None: + mock_FitDriver = self._patch_driver_and_problem(minimizer, monkeypatch) - minimizer.mcmc_sample( + minimizer_kwargs = {'existing': 'value'} + minimizer.fit( x=np.array([1.0]), - y=np.array([0.1]), + y=np.array([2.0]), weights=np.array([1.0]), - samples=10, - burn=0, - thin=1, - sampler_kwargs={'trim': False}, - ) - - assert mock_FitDriver.call_args.kwargs['trim'] is False - - def test_sample_rejects_non_callable_callback(self, minimizer: Bumps, monkeypatch) -> None: - import bumps.names - - monkeypatch.setattr(bumps.names, 'FitProblem', MagicMock(return_value=MagicMock())) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - - with pytest.raises(ValueError, match='progress_callback must be callable'): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=5, - thin=1, - progress_callback='not-callable', - ) - - # --- Resume-state tests ------------------------------------------------- - - def _make_resume_state_mock(self, *, nvar=2, npop=10, labels=None): - """Build a mock MCMCDraw for resume tests. - - BUMPS labels follow the pattern ``'p'`` (the - ``MINIMIZER_PARAMETER_PREFIX`` concatenated with the unique name), - e.g. ``'pFilm_thickness'``. - - :param nvar: Number of parameters. - :param npop: Population size. - :param labels: Parameter labels (defaults to ``['pa', 'pb']`` - which strip to ``['a', 'b']``). - """ - if labels is None: - labels = ['pa', 'pb'] - mock_state = MagicMock() - mock_state.Nvar = nvar - mock_state.Npop = npop - mock_state.labels = labels - mock_draw = MagicMock() - mock_draw.points = np.ones((20, nvar)) - mock_draw.logp = np.ones(20) - mock_state.draw.return_value = mock_draw - return mock_state - - def _make_problem_with_parameters(self, param_names): - """Build a mock FitProblem whose ``_parameters`` yields the given names.""" - params = [] - for name in param_names: - p = MagicMock() - p.name = 'p' + name - params.append(p) - mock_problem = MagicMock() - mock_problem._parameters = params - return mock_problem - - def test_sample_resume_state(self, minimizer: Bumps, monkeypatch) -> None: - """Verify resume_state is forwarded to driver.fit().""" - mock_FitDriver, mock_driver = self._setup_driver_mock(monkeypatch) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - resume_state = self._make_resume_state_mock() - - import bumps.names - - monkeypatch.setattr( - bumps.names, - 'FitProblem', - MagicMock(return_value=self._make_problem_with_parameters(['a', 'b'])), - ) - - result = minimizer.mcmc_sample( - x=np.array([1.0, 2.0]), - y=np.array([0.1, 0.2]), - weights=np.array([1.0, 1.0]), - samples=10, - burn=0, - thin=1, - resume_state=resume_state, - ) - - assert result is not None - # Verify a fit_state (defensive copy of resume_state) was passed to driver.fit() - call_kwargs = mock_driver.fit.call_args.kwargs - assert call_kwargs.get('fit_state') is not None - assert call_kwargs['fit_state'] is not resume_state - - def test_sample_resume_param_mismatch_raises(self, minimizer: Bumps, monkeypatch) -> None: - """Parameter count mismatch raises ValueError before driver.fit().""" - import bumps.names - - monkeypatch.setattr( - bumps.names, - 'FitProblem', - MagicMock(return_value=self._make_problem_with_parameters(['a'])), - ) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - # resume_state has 2 params, model has 1 - resume_state = self._make_resume_state_mock(nvar=2) - - with pytest.raises(ValueError, match='resume_state has 2 parameters'): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=0, - thin=1, - resume_state=resume_state, - ) - - def test_sample_resume_param_name_mismatch_raises(self, minimizer: Bumps, monkeypatch) -> None: - """Parameter name/order mismatch raises ValueError before driver.fit().""" - import bumps.names - - monkeypatch.setattr( - bumps.names, - 'FitProblem', - MagicMock(return_value=self._make_problem_with_parameters(['a', 'b'])), - ) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - # resume_state has labels ['px', 'py'] β†’ stripped to ['x', 'y'] - # Current model has params ['pa', 'pb'] β†’ stripped to ['a', 'b'] - # β†’ mismatch - resume_state = self._make_resume_state_mock(nvar=2, labels=['px', 'py']) - - with pytest.raises(ValueError, match='Parameter names/order mismatch'): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=0, - thin=1, - resume_state=resume_state, - ) - - def test_sample_resume_population_mismatch_raises(self, minimizer: Bumps, monkeypatch) -> None: - """Explicit population differing from state.Npop raises ValueError.""" - import bumps.names - - monkeypatch.setattr( - bumps.names, - 'FitProblem', - MagicMock(return_value=self._make_problem_with_parameters(['a', 'b'])), - ) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - resume_state = self._make_resume_state_mock(nvar=2, npop=10) - - with pytest.raises(ValueError, match='would produce'): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=0, - thin=1, - population=3, # ceil(3*2)=6 β‰  10 - resume_state=resume_state, - ) - - def test_sample_resume_forces_burn_to_zero( - self, minimizer: Bumps, monkeypatch, caplog: 'pytest.LogCaptureFixture' - ) -> None: - """burn>0 with resume_state warns and is forced to 0.""" - mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) - import bumps.names - - monkeypatch.setattr( - bumps.names, - 'FitProblem', - MagicMock(return_value=self._make_problem_with_parameters(['a', 'b'])), - ) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - resume_state = self._make_resume_state_mock() - - with caplog.at_level(logging.WARNING, logger='easyscience.fitting.bumps'): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=5, - thin=1, - resume_state=resume_state, - ) - - assert 'ignored on resume' in caplog.text - # burn must be forced to 0 in the kwargs passed to BUMPS - assert mock_FitDriver.call_args.kwargs['burn'] == 0 - - def test_sample_resume_unlabeled_state_warns_and_uses_absolute_pop( - self, minimizer: Bumps, monkeypatch, caplog: 'pytest.LogCaptureFixture' - ) -> None: - """A state reloaded from disk carries default labels ('P0', ...), so - name validation is skipped with a warning, and the saved population is - reproduced as a negative pop (BUMPS' absolute-chain-count convention).""" - mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) - import bumps.names - - monkeypatch.setattr( - bumps.names, - 'FitProblem', - MagicMock(return_value=self._make_problem_with_parameters(['a', 'b'])), + tolerance=1e-3, + max_evaluations=11, + minimizer_kwargs=minimizer_kwargs, + engine_kwargs={'engine': 'option'}, ) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - resume_state = self._make_resume_state_mock(nvar=2, npop=10, labels=['P0', 'P1']) - with caplog.at_level(logging.WARNING, logger='easyscience.fitting.bumps'): - minimizer.mcmc_sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - samples=10, - burn=0, - thin=1, - resume_state=resume_state, - ) + # Both mappings reach the driver... + driver_kwargs = mock_FitDriver.call_args.kwargs + assert driver_kwargs['existing'] == 'value' + assert driver_kwargs['engine'] == 'option' - assert 'does not carry parameter names' in caplog.text - assert mock_FitDriver.call_args.kwargs['pop'] == -10 + # ...and the caller's mapping is untouched, so reusing it cannot leak + # settings from one fit into the next. + assert minimizer_kwargs == {'existing': 'value'} # =================================================================== -# _build_sample_progress_payload +# fit() β€” unsuccessful and aborted outcomes # =================================================================== -class TestBuildSampleProgressPayload: +class TestFitUnsuccessfulOutcomes: @pytest.fixture def minimizer(self) -> Bumps: return Bumps( @@ -1147,143 +705,96 @@ def minimizer(self) -> Bumps: minimizer_enum=MagicMock(package='bumps', method='amoeba'), ) - def test_payload_structure_and_sampling_flag(self, minimizer: Bumps) -> None: - b = minimizer - - mock_problem = MagicMock() - mock_problem.chisq.side_effect = [25.0, 12.5] - mock_problem.labels.return_value = ['palpha'] - mock_problem.getp.return_value = np.array([1.0]) - b._cached_pars = {'alpha': MagicMock(value=1.0)} - - payload = b._build_sample_progress_payload(mock_problem, 7, np.array([1.0]), 12.5) - - assert payload['iteration'] == 7 - assert payload['chi2'] == 25.0 - assert payload['reduced_chi2'] == 12.5 - assert payload['parameter_values'] == {'alpha': 1.0} - assert payload['sampling'] is True - assert payload['finished'] is False - assert payload['refresh_plots'] is False - - def test_payload_keys(self, minimizer: Bumps) -> None: - b = minimizer - mock_problem = MagicMock() - mock_problem.chisq.side_effect = [10.0, 5.0] - mock_problem.labels.return_value = ['pa'] - mock_problem.getp.return_value = np.array([5.0]) - b._cached_pars = {'a': MagicMock(value=5.0)} - - payload = b._build_sample_progress_payload(mock_problem, 1, np.array([5.0]), nllf=5.0) - - expected_keys = { - 'iteration', - 'chi2', - 'reduced_chi2', - 'parameter_values', - 'refresh_plots', - 'finished', - 'sampling', - } - assert set(payload.keys()) == expected_keys + @staticmethod + def _patch(minimizer: Bumps, monkeypatch, driver_result, history_step=None) -> MagicMock: + mock_driver = MagicMock() + mock_driver.fit = MagicMock(return_value=driver_result) + mock_driver.stderr = MagicMock(return_value=np.array([0.1])) + mock_driver.monitor_runner.history.step = [] if history_step is None else history_step + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_bumps, + 'FitDriver', + MagicMock(return_value=mock_driver), + ) - def test_delegates_to_build_progress_payload(self, minimizer: Bumps) -> None: - """_build_sample_progress_payload calls _build_progress_payload and adds sampling.""" mock_problem = MagicMock() - - # Patch _build_progress_payload to track calls - base_payload = { - 'iteration': 3, - 'chi2': 42.0, - 'reduced_chi2': 21.0, - 'parameter_values': {'x': 7.0}, - 'refresh_plots': False, - 'finished': False, - } - with patch.object( - minimizer, '_build_progress_payload', return_value=base_payload - ) as mock_bpp: - result = minimizer._build_sample_progress_payload( - mock_problem, 3, np.array([7.0]), 21.0 - ) - - mock_bpp.assert_called_once_with(mock_problem, 3, np.array([7.0]), 21.0) - assert result == {**base_payload, 'sampling': True} - - -# =================================================================== -# _set_parameter_fit_result with stack_status=True -# =================================================================== - - -class TestSetParameterFitResultWithStack: - @pytest.fixture - def minimizer(self) -> Bumps: - return Bumps( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='bumps', method='amoeba'), + mock_problem._parameters = [] + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_bumps, + 'build_curve_problem', + MagicMock(return_value=(mock_problem, MagicMock(count=3), MagicMock())), ) - def test_stack_status_true_calls_begin_end_macro(self, minimizer: Bumps) -> None: - from easyscience import global_object - - global_object.stack.enabled = False - - minimizer._cached_pars = {'a': MagicMock(), 'b': MagicMock()} - minimizer._cached_pars['a'].value = 'old_a' - minimizer._cached_pars['b'].value = 'old_b' + minimizer._gen_fit_results = MagicMock(return_value='result') + minimizer._set_parameter_fit_result = MagicMock() + minimizer._resolve_fitclass = MagicMock(return_value=MagicMock(id='amoeba')) + minimizer._cached_pars = {} + minimizer._cached_pars_vals = {} + return mock_driver + + def test_no_solution_is_reported_not_raised(self, minimizer: Bumps, monkeypatch) -> None: + """BUMPS returns x=None for a failed optimization (e.g. LM landing on + non-finite values). That is a non-converged fit, not an exception.""" + self._patch(minimizer, monkeypatch, driver_result=(None, None), history_step=[4]) minimizer._restore_parameter_values = MagicMock() - mock_fit_result = MagicMock() - mock_fit_result.x = np.array([1.0, 2.0]) - mock_fit_result.dx = np.array([0.1, 0.2]) - - mock_par_a = MagicMock() - mock_par_a.name = 'pa' - mock_par_b = MagicMock() - mock_par_b.name = 'pb' - par_list = [mock_par_a, mock_par_b] - - minimizer._set_parameter_fit_result(mock_fit_result, True, par_list) + result = minimizer.fit(x=np.array([1.0]), y=np.array([2.0]), weights=np.array([1.0])) - assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.1 - assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.2 + assert result == 'result' + passed = minimizer._gen_fit_results.call_args.args[0] + assert passed.success is False + assert passed.x is None + assert passed.dx is None # stderr() needs a solution to expand around + assert 'did not converge' in passed.message + # Parameters are rolled back and never written from a missing solution minimizer._restore_parameter_values.assert_called_once() + minimizer._set_parameter_fit_result.assert_not_called() + def test_abort_is_reported_as_unsuccessful(self, minimizer: Bumps, monkeypatch) -> None: + self._patch( + minimizer, monkeypatch, driver_result=(np.array([42.0]), 0.0), history_step=[2] + ) -# =================================================================== -# convert_to_par_object -# =================================================================== - - -class TestConvertToParObject: - def test_convert_parameter_object(self) -> None: - from easyscience.variable import Parameter + result = minimizer.fit( + x=np.array([1.0]), + y=np.array([2.0]), + weights=np.array([1.0]), + abort_test=lambda: True, + ) - param = Parameter('thickness', 42.0, min=0.0, max=100.0) - param.fixed = False + assert result == 'result' + passed = minimizer._gen_fit_results.call_args.args[0] + assert passed.success is False + assert passed.message == 'Fit aborted before convergence' + # The best point reached before the abort is still applied + minimizer._set_parameter_fit_result.assert_called_once() - result = Bumps.convert_to_par_object(param) + def test_empty_step_history_does_not_raise(self, minimizer: Bumps, monkeypatch) -> None: + """An abort before the fitter reports its first step leaves the BUMPS + history trace empty; indexing it would raise IndexError.""" + self._patch(minimizer, monkeypatch, driver_result=(np.array([42.0]), 0.0), history_step=[]) - # convert_to_par_object uses obj.unique_name which is auto-assigned - assert result.name.startswith('p') - assert result.value == 42.0 - assert result.bounds == (0.0, 100.0) - assert result.fixed is False + result = minimizer.fit(x=np.array([1.0]), y=np.array([2.0]), weights=np.array([1.0])) - def test_convert_fixed_parameter(self) -> None: - from easyscience.variable import Parameter + assert result == 'result' + assert minimizer._gen_fit_results.call_args.args[0].nit is None - param = Parameter('roughness', 5.0, min=0.0, max=20.0) - param.fixed = True + def test_successful_fit_reports_success(self, minimizer: Bumps, monkeypatch) -> None: + self._patch( + minimizer, monkeypatch, driver_result=(np.array([42.0]), 0.0), history_step=[7] + ) - result = Bumps.convert_to_par_object(param) + minimizer.fit( + x=np.array([1.0]), + y=np.array([2.0]), + weights=np.array([1.0]), + abort_test=lambda: False, + ) - assert result.name.startswith('p') - assert result.fixed is True + passed = minimizer._gen_fit_results.call_args.args[0] + assert passed.success is True + assert passed.message == 'Fit converged' + assert passed.nit == 7 # =================================================================== @@ -1301,10 +812,6 @@ def minimizer(self) -> Bumps: ) def test_abort_test_passed_to_fit_driver(self, minimizer: Bumps, monkeypatch) -> None: - from easyscience import global_object - - global_object.stack.enabled = False - mock_driver = MagicMock() mock_driver.clip = MagicMock() mock_driver.fit = MagicMock(return_value=(np.array([42.0]), 0.0)) @@ -1319,11 +826,10 @@ def test_abort_test_passed_to_fit_driver(self, minimizer: Bumps, monkeypatch) -> mock_problem._parameters = [] monkeypatch.setattr( easyscience.fitting.minimizers.minimizer_bumps, - 'FitProblem', - MagicMock(return_value=mock_problem), + 'build_curve_problem', + MagicMock(return_value=(mock_problem, MagicMock(), MagicMock())), ) - minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) minimizer._gen_fit_results = MagicMock(return_value='result') minimizer._resolve_fitclass = MagicMock(return_value=MagicMock(id='amoeba')) minimizer._set_parameter_fit_result = MagicMock() diff --git a/tests/unit/fitting/minimizers/test_minimizer_dfo.py b/tests/unit/fitting/minimizers/test_minimizer_dfo.py index 823dcce5..56bf3a49 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_dfo.py +++ b/tests/unit/fitting/minimizers/test_minimizer_dfo.py @@ -46,10 +46,6 @@ def test_supported_methods(self, minimizer: DFO) -> None: def test_fit(self, minimizer: DFO) -> None: # When - from easyscience import global_object - - global_object.stack.enabled = False - mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) @@ -70,19 +66,12 @@ def test_fit(self, minimizer: DFO) -> None: minimizer._dfo_fit.assert_called_once_with( cached_pars, mock_model, user_params={'logging.save_diagnostic_info': True} ) - minimizer._make_model.assert_called_once_with( - parameters=None, - callback=None, - ) - minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) + minimizer._make_model.assert_called_once_with(callback=None) + minimizer._set_parameter_fit_result.assert_called_once_with('fit') minimizer._gen_fit_results.assert_called_once_with('fit', 1) mock_model_function.assert_called_once_with(1.0, 2.0, 1) def test_fit_passes_callback_to_model_builder(self, minimizer: DFO) -> None: - from easyscience import global_object - - global_object.stack.enabled = False - mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) @@ -98,78 +87,7 @@ def test_fit_passes_callback_to_model_builder(self, minimizer: DFO) -> None: minimizer.fit(x=1.0, y=2.0, weights=1, callback=callback) - minimizer._make_model.assert_called_once_with( - parameters=None, - callback=callback, - ) - - def test_fit_wraps_supplied_model_with_explicit_callback(self, minimizer: DFO) -> None: - from easyscience import global_object - - global_object.stack.enabled = False - - supplied_model = MagicMock() - wrapped_model = MagicMock() - explicit_callback = MagicMock() - - minimizer._make_model = MagicMock() - minimizer._wrap_model_with_callback = MagicMock(return_value=wrapped_model) - minimizer._get_callback_parameter_names = MagicMock(return_value=['palpha']) - minimizer._dfo_fit = MagicMock(return_value='fit') - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - minimizer._cached_pars = {'alpha': MagicMock(value=1.0)} - - result = minimizer.fit( - x=np.array([1.0]), - y=np.array([2.0]), - weights=np.array([1.0]), - model=supplied_model, - callback=explicit_callback, - ) - - assert result == 'gen_fit_results' - minimizer._make_model.assert_not_called() - minimizer._wrap_model_with_callback.assert_called_once_with( - supplied_model, - ['palpha'], - explicit_callback, - ) - minimizer._dfo_fit.assert_called_once_with( - minimizer._cached_pars, - wrapped_model, - user_params={'logging.save_diagnostic_info': True}, - ) - - def test_fit_uses_supplied_model_without_callback(self, minimizer: DFO) -> None: - from easyscience import global_object - - global_object.stack.enabled = False - - supplied_model = MagicMock() - - minimizer._make_model = MagicMock() - minimizer._wrap_model_with_callback = MagicMock() - minimizer._dfo_fit = MagicMock(return_value='fit') - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - minimizer._cached_pars = {'alpha': MagicMock(value=1.0)} - - result = minimizer.fit( - x=np.array([1.0]), - y=np.array([2.0]), - weights=np.array([1.0]), - model=supplied_model, - ) - - assert result == 'gen_fit_results' - minimizer._make_model.assert_not_called() - minimizer._wrap_model_with_callback.assert_not_called() - minimizer._dfo_fit.assert_called_once_with( - minimizer._cached_pars, - supplied_model, - user_params={'logging.save_diagnostic_info': True}, - ) + minimizer._make_model.assert_called_once_with(callback=callback) def test_generate_fit_function(self, minimizer: DFO) -> None: # When @@ -184,7 +102,7 @@ def test_generate_fit_function(self, minimizer: DFO) -> None: mock_parm_2.unique_name = 'mock_parm_2' mock_parm_2.value = 2.0 mock_parm_2.error = 0.2 - minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) + minimizer._object.get_free_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) # Then fit_function = minimizer._generate_fit_function() @@ -219,14 +137,13 @@ def test_make_model(self, minimizer: DFO) -> None: minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' mock_parm_1.value = 1000.0 mock_parm_2 = MagicMock() - mock_parm_2.unique_name = 'mock_parm_2' mock_parm_2.value = 2000.0 + minimizer._cached_pars = {'mock_parm_1': mock_parm_1, 'mock_parm_2': mock_parm_2} # Then - model = minimizer._make_model(parameters=[mock_parm_1, mock_parm_2]) + model = minimizer._make_model() residuals_for_model = model( x=np.array([1, 2]), y=np.array([10, 20]), @@ -247,15 +164,14 @@ def test_make_model_callback(self, minimizer: DFO) -> None: minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' mock_parm_1.value = 1000.0 mock_parm_2 = MagicMock() - mock_parm_2.unique_name = 'mock_parm_2' mock_parm_2.value = 2000.0 + minimizer._cached_pars = {'mock_parm_1': mock_parm_1, 'mock_parm_2': mock_parm_2} callback = MagicMock() - model = minimizer._make_model(parameters=[mock_parm_1, mock_parm_2], callback=callback) + model = minimizer._make_model(callback=callback) residuals_for_model = model( x=np.array([1, 2]), y=np.array([10, 20]), @@ -299,7 +215,7 @@ def test_make_model_without_parameters_uses_cached_parameters(self, minimizer: D assert mock_fit_function.call_args.kwargs == {'palpha': 1111.0} - def test_set_parameter_fit_result_no_stack_status(self, minimizer: DFO): + def test_set_parameter_fit_result(self, minimizer: DFO): # When minimizer._cached_pars = { 'a': MagicMock(), @@ -316,7 +232,7 @@ def test_set_parameter_fit_result_no_stack_status(self, minimizer: DFO): minimizer._error_from_jacobian = MagicMock(return_value=np.array([[0.1, 0.0], [0.0, 0.2]])) # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) + minimizer._set_parameter_fit_result(mock_fit_result) # Expect assert minimizer._cached_pars['a'].value == 1.0 @@ -613,10 +529,6 @@ def test_fit_generic_exception_resets_parameters_and_raises_fit_error( ) -> None: """When _dfo_fit raises a non-FitError exception, fit() must reset parameter values to cached originals and re-raise as FitError.""" - from easyscience import global_object - - global_object.stack.enabled = True - mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) @@ -634,14 +546,9 @@ def test_fit_generic_exception_resets_parameters_and_raises_fit_error( assert cached_par_1.value == 1.0 assert cached_par_2.value == 2.0 - assert global_object.stack.enabled is True def test_fit_fit_error_resets_parameters_and_reraises(self, minimizer: DFO) -> None: """When _dfo_fit raises FitError, fit() must reset parameter values and re-raise it.""" - from easyscience import global_object - - global_object.stack.enabled = False - mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) @@ -684,10 +591,6 @@ def test_progress_callback_creates_adapter_when_no_explicit_callback( ) -> None: """When progress_callback is provided without an explicit callback, fit() should auto-create a DFO callback adapter.""" - from easyscience import global_object - - global_object.stack.enabled = False - mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) @@ -730,10 +633,6 @@ def test_progress_callback_creates_adapter_when_no_explicit_callback( def test_progress_callback_not_used_when_explicit_callback_given(self, minimizer: DFO) -> None: """When both progress_callback and callback are given, the explicit callback takes precedence and progress_callback is ignored.""" - from easyscience import global_object - - global_object.stack.enabled = False - mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) @@ -759,22 +658,6 @@ def test_progress_callback_not_used_when_explicit_callback_given(self, minimizer call_kwargs = minimizer._make_model.call_args[1] assert call_kwargs['callback'] is explicit_cb - @pytest.mark.parametrize( - ('parameters', 'expected_names'), - [ - ([MagicMock(unique_name='alpha')], ['palpha']), - (None, ['pbeta']), - ], - ) - def test_get_callback_parameter_names_optional_parameters( - self, minimizer: DFO, parameters, expected_names - ) -> None: - minimizer._cached_pars = {'beta': MagicMock(value=1.0)} - - parameter_names = minimizer._get_callback_parameter_names(parameters) - - assert parameter_names == expected_names - def test_wrap_model_with_callback_invokes_on_each_evaluation(self, minimizer: DFO) -> None: callback = MagicMock() wrapped_model = minimizer._wrap_model_with_callback( diff --git a/tests/unit/fitting/minimizers/test_minimizer_lmfit.py b/tests/unit/fitting/minimizers/test_minimizer_lmfit.py index 781893d9..1c4dc228 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_lmfit.py +++ b/tests/unit/fitting/minimizers/test_minimizer_lmfit.py @@ -115,10 +115,6 @@ def test_make_model_no_pars(self, minimizer: LMFit, monkeypatch) -> None: def test_fit(self, minimizer: LMFit) -> None: # When - from easyscience import global_object - - global_object.stack.enabled = False - mock_model = MagicMock() mock_model.fit = MagicMock(return_value='fit') minimizer._make_model = MagicMock(return_value=mock_model) @@ -135,27 +131,9 @@ def test_fit(self, minimizer: LMFit) -> None: ) assert callable(mock_model.fit.call_args.kwargs['iter_cb']) minimizer._make_model.assert_called_once_with() - minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) + minimizer._set_parameter_fit_result.assert_called_once_with('fit') minimizer._gen_fit_results.assert_called_once_with('fit', iterations=None) - def test_fit_model(self, minimizer: LMFit) -> None: - # When - mock_model = MagicMock() - mock_model.fit = MagicMock(return_value='fit') - minimizer._make_model = MagicMock(return_value=mock_model) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - # Then - minimizer.fit(x=1.0, y=2.0, weights=1, model=mock_model) - - # Expect - mock_model.fit.assert_called_once_with( - 2.0, x=1.0, weights=1, max_nfev=None, iter_cb=ANY, fit_kws={}, method='leastsq' - ) - assert callable(mock_model.fit.call_args.kwargs['iter_cb']) - minimizer._make_model.assert_not_called() - def test_fit_method(self, minimizer: LMFit) -> None: # When mock_model = MagicMock() @@ -212,6 +190,48 @@ def test_fit_kwargs(self, minimizer: LMFit) -> None: ) assert callable(mock_model.fit.call_args.kwargs['iter_cb']) + @pytest.mark.parametrize( + 'minimizer_method, passed_method, expected', + [ + ('leastsq', None, {'ftol': 0.1}), + ('least_squares', None, {'ftol': 0.1}), + ('powell', None, {'tol': 0.1}), + ('differential_evolution', None, {'tol': 0.1}), + ('cobyla', None, {'tol': 0.1}), + ('leastsq', 'powell', {'tol': 0.1}), + ('powell', 'leastsq', {'ftol': 0.1}), + ('nelder', None, {}), + ], + ids=[ + 'leastsq', + 'least_squares', + 'powell', + 'differential_evolution', + 'cobyla', + 'explicit_method_overrides', + 'explicit_leastsq_overrides', + 'unmapped_method', + ], + ) + def test_get_fit_kws_tolerance( + self, minimizer: LMFit, minimizer_method, passed_method, expected + ) -> None: + # When + minimizer._method = minimizer_method + + # Then + fit_kws = minimizer._get_fit_kws(passed_method, 0.1, None) + + # Expect + assert fit_kws == expected + + def test_get_fit_kws_no_tolerance(self, minimizer: LMFit) -> None: + # When Then + fit_kws = minimizer._get_fit_kws(None, None, {'existing': 'kwarg'}) + + # Expect + assert fit_kws == {'existing': 'kwarg'} + def test_fit_progress_callback(self, minimizer: LMFit) -> None: # When progress_callback = MagicMock(return_value=True) @@ -229,9 +249,7 @@ def test_fit_progress_callback(self, minimizer: LMFit) -> None: iter_cb = mock_model.fit.call_args.kwargs['iter_cb'] assert callable(iter_cb) - def test_fit_progress_callback_with_supplied_model_uses_iter_params( - self, minimizer: LMFit - ) -> None: + def test_fit_progress_callback_uses_iter_params(self, minimizer: LMFit) -> None: progress_callback = MagicMock(return_value=True) mock_model = MagicMock() mock_param_alpha = MagicMock() @@ -252,12 +270,10 @@ def test_fit_progress_callback_with_supplied_model_uses_iter_params( x=np.array([1.0, 2.0]), y=np.array([1.0, 2.0]), weights=np.array([1.0, 1.0]), - model=mock_model, progress_callback=progress_callback, ) assert result == 'gen_fit_results' - minimizer._make_model.assert_not_called() payload = progress_callback.call_args[0][0] assert payload['parameter_values'] == {'alpha': 1.0} assert payload['chi2'] == 25.0 @@ -385,87 +401,7 @@ def test_gen_fit_results_success_does_not_warn( assert len(caplog.records) == 0 assert result.success is True - def test_convert_to_pars_obj(self, minimizer: LMFit, monkeypatch) -> None: - # When - minimizer._object = MagicMock() - minimizer._object.get_fit_parameters = MagicMock(return_value=['parm_1', 'parm_2']) - - minimizer.convert_to_par_object = MagicMock(return_value='convert_to_par_object') - - mock_lm_parameter = MagicMock() - mock_lm_parameter.add_many = MagicMock(return_value='add_many') - mock_LMParameters = MagicMock(return_value=mock_lm_parameter) - monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_lmfit, 'LMParameters', mock_LMParameters - ) - - # Then - pars = minimizer.convert_to_pars_obj() - - # Expect - assert pars == 'add_many' - assert minimizer.convert_to_par_object.call_count == 2 - minimizer._object.get_fit_parameters.assert_called_once_with() - minimizer.convert_to_par_object.assert_called_with('parm_2') - mock_lm_parameter.add_many.assert_called_once_with([ - 'convert_to_par_object', - 'convert_to_par_object', - ]) - - def test_convert_to_pars_obj_with_parameters(self, minimizer: LMFit, monkeypatch) -> None: - # When - minimizer.convert_to_par_object = MagicMock(return_value='convert_to_par_object') - - mock_lm_parameter = MagicMock() - mock_lm_parameter.add_many = MagicMock(return_value='add_many') - mock_LMParameters = MagicMock(return_value=mock_lm_parameter) - monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_lmfit, 'LMParameters', mock_LMParameters - ) - - # Then - pars = minimizer.convert_to_pars_obj(['parm_1', 'parm_2']) - - # Expect - assert pars == 'add_many' - assert minimizer.convert_to_par_object.call_count == 2 - minimizer.convert_to_par_object.assert_called_with('parm_2') - mock_lm_parameter.add_many.assert_called_once_with([ - 'convert_to_par_object', - 'convert_to_par_object', - ]) - - def test_convert_to_par_object(self, minimizer: LMFit, monkeypatch) -> None: - # When - mock_lm_parameter = MagicMock() - mock_LMParameter = MagicMock(return_value=mock_lm_parameter) - monkeypatch.setattr( - easyscience.fitting.minimizers.minimizer_lmfit, 'LMParameter', mock_LMParameter - ) - - mock_parm = MagicMock(Parameter) - mock_parm.value = 1.0 - mock_parm.fixed = True - mock_parm.min = -10.0 - mock_parm.max = 10.0 - mock_parm.unique_name = 'key_converted' - - # Then - par = minimizer.convert_to_par_object(mock_parm) - - # Expect - assert par == mock_lm_parameter - mock_LMParameter.assert_called_once_with( - 'pkey_converted', - value=1.0, - vary=False, - min=-10.0, - max=10.0, - expr=None, - brute_step=None, - ) - - def test_set_parameter_fit_result_no_stack_status(self, minimizer: LMFit) -> None: + def test_set_parameter_fit_result(self, minimizer: LMFit) -> None: # When minimizer._cached_pars = { 'a': MagicMock(), @@ -485,7 +421,7 @@ def test_set_parameter_fit_result_no_stack_status(self, minimizer: LMFit) -> Non mock_fit_result.errorbars = True # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) + minimizer._set_parameter_fit_result(mock_fit_result) # Expect assert minimizer._cached_pars['a'].value == 1.0 @@ -493,7 +429,7 @@ def test_set_parameter_fit_result_no_stack_status(self, minimizer: LMFit) -> Non assert minimizer._cached_pars['b'].value == 2.0 assert minimizer._cached_pars['b'].error == 0.2 - def test_set_parameter_fit_result_no_stack_status_no_error(self, minimizer: LMFit) -> None: + def test_set_parameter_fit_result_no_error(self, minimizer: LMFit) -> None: # When minimizer._cached_pars = { 'a': MagicMock(), @@ -513,13 +449,13 @@ def test_set_parameter_fit_result_no_stack_status_no_error(self, minimizer: LMFi mock_fit_result.errorbars = False # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) + minimizer._set_parameter_fit_result(mock_fit_result) # Expect assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.0 + assert minimizer._cached_pars['a'].error is None assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.0 + assert minimizer._cached_pars['b'].error is None def test_gen_fit_results(self, minimizer: LMFit, monkeypatch) -> None: # When diff --git a/src/easyscience/global_object/hugger/__init__.py b/tests/unit/fitting/samplers/__init__.py similarity index 99% rename from src/easyscience/global_object/hugger/__init__.py rename to tests/unit/fitting/samplers/__init__.py index b1990ec8..4e798e20 100644 --- a/src/easyscience/global_object/hugger/__init__.py +++ b/tests/unit/fitting/samplers/__init__.py @@ -1,3 +1,2 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - diff --git a/tests/unit/fitting/samplers/test_sampler_bumps.py b/tests/unit/fitting/samplers/test_sampler_bumps.py new file mode 100644 index 00000000..c755094c --- /dev/null +++ b/tests/unit/fitting/samplers/test_sampler_bumps.py @@ -0,0 +1,486 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for ``DreamSampler`` β€” mirrors +``src/easyscience/fitting/samplers/sampler_bumps.py``. + +Ported from the former ``TestBumpsSample`` suite in +``tests/unit/fitting/minimizers/test_minimizer_bumps.py`` when the former +``Bumps.mcmc_sample`` moved here as ``DreamSampler.run`` (easyscience/core#280). +That entry point, and ``Fitter.mcmc_sample``, have since been removed β€” +``Sampler`` and ``DreamSampler.run`` are the supported APIs. +""" + +import logging +from unittest.mock import MagicMock + +import numpy as np +import pytest + +import easyscience.fitting.samplers.sampler_bumps +from easyscience.fitting.engine_base import EngineBase +from easyscience.fitting.minimizers.bumps_utils import BumpsProgressMonitor +from easyscience.fitting.minimizers.utils import FitError +from easyscience.fitting.samplers import DreamSampler + + +class TestDreamSamplerRun: + """Tests for ``DreamSampler.run()`` and its helpers.""" + + # Sentinel value to signal "set fitter.state = None" in _setup_driver_mock + ABORT = object() + + @pytest.fixture + def engine(self) -> DreamSampler: + return DreamSampler(obj='obj', fit_function='fit_function') + + @pytest.fixture(autouse=True) + def _mock_bumps_internals(self, monkeypatch): + """Prevent run() from constructing real BUMPS objects. + + ``run()`` imports ``DreamFit`` from the real ``bumps`` package + internally and builds its problem via ``build_curve_problem``, + which would try to build real model objects. We redirect those + to mocks and also mock ``FitDriver`` (a module-level import) so + the whole flow stays under test control. + """ + import bumps.fitters + + monkeypatch.setattr(bumps.fitters, 'DreamFit', MagicMock()) + self._set_problem(monkeypatch, MagicMock()) + + @staticmethod + def _set_problem(monkeypatch, problem): + """Point ``build_curve_problem`` at a canned (problem, counter, curve) triple.""" + monkeypatch.setattr( + easyscience.fitting.samplers.sampler_bumps, + 'build_curve_problem', + MagicMock(return_value=(problem, MagicMock(), MagicMock())), + ) + + def _setup_driver_mock( + self, monkeypatch, fitter_state_value=None, fit_result=None, fit_side_effect=None + ): + """Helper to create a mocked FitDriver with configurable behavior. + + :param fitter_state_value: If ``None``, ``driver.fitter.state`` will be + a regular MagicMock (non-None). Pass ``ABORT`` to set it to ``None`` + and simulate user abort. + """ + mock_driver = MagicMock() + mock_driver.clip = MagicMock() + + if fit_side_effect is not None: + mock_driver.fit.side_effect = fit_side_effect + else: + mock_driver.fit.return_value = fit_result or (np.array([1.0]), 0.0) + + mock_driver.stderr = MagicMock(return_value=np.array([0.1])) + + if fitter_state_value is TestDreamSamplerRun.ABORT: + mock_driver.fitter.state = None + else: + mock_state = MagicMock() + mock_state.Nvar = 1 + mock_state.Npop = 5 + mock_state.labels = ['p_param_0'] + mock_draw = MagicMock() + mock_draw.points = np.array([[1.0]]) + mock_draw.logp = np.array([0.5]) + mock_state.draw.return_value = mock_draw + mock_driver.fitter.state = mock_state + + mock_FitDriver = MagicMock(return_value=mock_driver) + monkeypatch.setattr( + easyscience.fitting.samplers.sampler_bumps, 'FitDriver', mock_FitDriver + ) + return mock_FitDriver, mock_driver + + def test_is_an_engine(self, engine: DreamSampler) -> None: + assert isinstance(engine, EngineBase) + assert engine.package == 'bumps' + + @pytest.mark.parametrize( + 'kwargs, match', + [ + ({'samples': 0}, 'samples must be a positive integer'), + ({'samples': -1}, 'samples must be a positive integer'), + ({'burn': -1}, 'burn must be a non-negative integer'), + ({'thin': 0}, 'thin must be a positive integer'), + ], + ) + def test_run_invalid_args(self, engine: DreamSampler, kwargs, match) -> None: + """Invalid samples/burn/thin values raise ValueError before any sampling. + + This is the single source of truth for these checks β€” the higher-level + ``Sampler`` relies on it. + """ + with pytest.raises(ValueError, match=match): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=kwargs.get('samples', 10), + burn=kwargs.get('burn', 0), + thin=kwargs.get('thin', 1), + ) + + @pytest.mark.parametrize( + 'overrides, match', + [ + ({'y': np.array([0.1])}, 'x and y must have the same shape'), + ({'x': np.array([1.0, np.nan])}, 'x cannot contain NaN'), + ({'y': np.array([0.1, np.inf])}, 'y cannot contain NaN'), + ({'weights': np.array([1.0])}, 'Weights must have the same shape'), + ({'weights': np.array([1.0, np.nan])}, 'Weights cannot be NaN'), + ({'weights': np.array([1.0, 0.0])}, 'Weights must be strictly positive'), + ], + ) + def test_run_invalid_data(self, engine: DreamSampler, overrides, match) -> None: + """Shape mismatches and non-finite/non-positive data raise ValueError + before any sampling.""" + data = { + 'x': np.array([1.0, 2.0]), + 'y': np.array([0.1, 0.2]), + 'weights': np.array([1.0, 1.0]), + } + data.update(overrides) + with pytest.raises(ValueError, match=match): + engine.run(**data, samples=10, burn=0, thin=1) + + def test_run_rejects_none_weights(self, engine: DreamSampler) -> None: + """weights are required; a None slipping past ``Sampler`` still gets a + clear ValueError from the dtype guard rather than an opaque numpy + TypeError out of ``np.isfinite`` (CR-5).""" + with pytest.raises(ValueError, match='weights must hold numeric values'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=None, + samples=10, + burn=0, + thin=1, + ) + + def test_run_basic(self, engine: DreamSampler, monkeypatch) -> None: + """Verify that run() returns a dict with expected keys.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + + result = engine.run( + x=np.array([1.0, 2.0]), + y=np.array([0.1, 0.2]), + weights=np.array([1.0, 1.0]), + samples=100, + burn=20, + thin=2, + population=5, + ) + + assert isinstance(result, dict) + assert 'draws' in result + assert 'param_names' in result + assert 'internal_bumps_object' in result + assert 'logp' in result + mock_FitDriver.assert_called_once() + + def test_run_with_progress_callback(self, engine: DreamSampler, monkeypatch) -> None: + """Verify progress callback is wired up as a monitor.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + progress_callback = MagicMock() + + result = engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=5, + thin=1, + progress_callback=progress_callback, + ) + + assert result is not None + call_kwargs = mock_FitDriver.call_args.kwargs + assert 'monitors' in call_kwargs + assert len(call_kwargs['monitors']) == 1 + assert isinstance(call_kwargs['monitors'][0], BumpsProgressMonitor) + + def test_run_aborted_by_user_raises_fit_error(self, engine: DreamSampler, monkeypatch) -> None: + """Verify that sampling abortion raises FitError.""" + self._setup_driver_mock(monkeypatch, fitter_state_value=TestDreamSamplerRun.ABORT) + + with pytest.raises(FitError, match='Sampling aborted by user'): + engine.run(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + def test_run_driver_exception_restores_parameters( + self, engine: DreamSampler, monkeypatch + ) -> None: + """Verify that a driver exception during sampling restores parameter values.""" + self._setup_driver_mock(monkeypatch, fit_side_effect=RuntimeError('driver failed')) + engine._restore_parameter_values = MagicMock() + + with pytest.raises(RuntimeError, match='driver failed'): + engine.run(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + engine._restore_parameter_values.assert_called_once() + + def test_run_population_param(self, engine: DreamSampler, monkeypatch) -> None: + """population kwarg is forwarded to DREAM as pop.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + population=7, + ) + + call_kwargs = mock_FitDriver.call_args.kwargs + assert call_kwargs['pop'] == 7 + + def test_run_sampler_kwargs_forwarded(self, engine: DreamSampler, monkeypatch) -> None: + """sampler_kwargs entries are merged into the DREAM kwargs.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + sampler_kwargs={'trim': False}, + ) + + assert mock_FitDriver.call_args.kwargs['trim'] is False + + def test_run_rejects_non_callable_callback(self, engine: DreamSampler) -> None: + with pytest.raises(ValueError, match='progress_callback must be callable'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=5, + thin=1, + progress_callback='not-callable', + ) + + # --- Resume-state tests ------------------------------------------------- + + def _make_resume_state_mock(self, *, nvar=2, npop=10, labels=None): + """Build a mock MCMCDraw for resume tests. + + BUMPS labels follow the pattern ``'p'`` (the + ``PARAMETER_PREFIX`` concatenated with the unique name), + e.g. ``'pFilm_thickness'``. + + :param nvar: Number of parameters. + :param npop: Population size. + :param labels: Parameter labels (defaults to ``['pa', 'pb']`` + which strip to ``['a', 'b']``). + """ + if labels is None: + labels = ['pa', 'pb'] + mock_state = MagicMock() + mock_state.Nvar = nvar + mock_state.Npop = npop + mock_state.labels = labels + mock_draw = MagicMock() + mock_draw.points = np.ones((20, nvar)) + mock_draw.logp = np.ones(20) + mock_state.draw.return_value = mock_draw + return mock_state + + def _make_problem_with_parameters(self, param_names): + """Build a mock FitProblem whose ``_parameters`` yields the given names.""" + params = [] + for name in param_names: + p = MagicMock() + p.name = 'p' + name + params.append(p) + mock_problem = MagicMock() + mock_problem._parameters = params + return mock_problem + + def test_run_resume_state(self, engine: DreamSampler, monkeypatch) -> None: + """Verify resume_state is forwarded to driver.fit().""" + mock_FitDriver, mock_driver = self._setup_driver_mock(monkeypatch) + resume_state = self._make_resume_state_mock() + self._set_problem(monkeypatch, self._make_problem_with_parameters(['a', 'b'])) + + result = engine.run( + x=np.array([1.0, 2.0]), + y=np.array([0.1, 0.2]), + weights=np.array([1.0, 1.0]), + samples=10, + burn=0, + thin=1, + resume_state=resume_state, + ) + + assert result is not None + # Verify a fit_state (defensive copy of resume_state) was passed to driver.fit() + call_kwargs = mock_driver.fit.call_args.kwargs + assert call_kwargs.get('fit_state') is not None + assert call_kwargs['fit_state'] is not resume_state + + def test_run_resume_param_mismatch_raises(self, engine: DreamSampler, monkeypatch) -> None: + """Parameter count mismatch raises ValueError before driver.fit().""" + self._set_problem(monkeypatch, self._make_problem_with_parameters(['a'])) + # resume_state has 2 params, model has 1 + resume_state = self._make_resume_state_mock(nvar=2) + + with pytest.raises(ValueError, match='resume_state has 2 parameters'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + resume_state=resume_state, + ) + + def test_run_resume_param_name_mismatch_raises( + self, engine: DreamSampler, monkeypatch + ) -> None: + """Parameter name/order mismatch raises ValueError before driver.fit().""" + self._set_problem(monkeypatch, self._make_problem_with_parameters(['a', 'b'])) + # resume_state has labels ['px', 'py'] β†’ stripped to ['x', 'y'] + # Current model has params ['pa', 'pb'] β†’ stripped to ['a', 'b'] + # β†’ mismatch + resume_state = self._make_resume_state_mock(nvar=2, labels=['px', 'py']) + + with pytest.raises(ValueError, match='Parameter names/order mismatch'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + resume_state=resume_state, + ) + + def test_run_resume_population_mismatch_raises( + self, engine: DreamSampler, monkeypatch + ) -> None: + """Explicit population differing from state.Npop raises ValueError.""" + self._set_problem(monkeypatch, self._make_problem_with_parameters(['a', 'b'])) + resume_state = self._make_resume_state_mock(nvar=2, npop=10) + + with pytest.raises(ValueError, match='would produce'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + population=3, # ceil(3*2)=6 β‰  10 + resume_state=resume_state, + ) + + def test_run_resume_forces_burn_to_zero( + self, engine: DreamSampler, monkeypatch, caplog: 'pytest.LogCaptureFixture' + ) -> None: + """burn>0 with resume_state warns and is forced to 0.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + self._set_problem(monkeypatch, self._make_problem_with_parameters(['a', 'b'])) + resume_state = self._make_resume_state_mock() + + with caplog.at_level(logging.WARNING, logger='easyscience.fitting.bumps'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=5, + thin=1, + resume_state=resume_state, + ) + + assert 'ignored on resume' in caplog.text + # burn must be forced to 0 in the kwargs passed to BUMPS + assert mock_FitDriver.call_args.kwargs['burn'] == 0 + + def test_run_resume_unlabeled_state_warns_and_uses_absolute_pop( + self, engine: DreamSampler, monkeypatch, caplog: 'pytest.LogCaptureFixture' + ) -> None: + """A state reloaded from disk carries default labels ('P0', ...), so + name validation is skipped with a warning, and the saved population is + reproduced as a negative pop (BUMPS' absolute-chain-count convention).""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + self._set_problem(monkeypatch, self._make_problem_with_parameters(['a', 'b'])) + resume_state = self._make_resume_state_mock(nvar=2, npop=10, labels=['P0', 'P1']) + + with caplog.at_level(logging.WARNING, logger='easyscience.fitting.bumps'): + engine.run( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + resume_state=resume_state, + ) + + assert 'does not carry parameter names' in caplog.text + assert mock_FitDriver.call_args.kwargs['pop'] == -10 + + +class TestDreamSamplerProgressPayload: + """``DreamSampler._build_sample_progress_payload``.""" + + @pytest.fixture + def engine(self) -> DreamSampler: + return DreamSampler(obj='obj', fit_function='fit_function') + + def test_payload_structure_and_sampling_flag(self, engine: DreamSampler) -> None: + mock_problem = MagicMock() + mock_problem.chisq.side_effect = [25.0, 12.5] + mock_problem.labels.return_value = ['palpha'] + mock_problem.getp.return_value = np.array([1.0]) + + payload = engine._build_sample_progress_payload( + mock_problem, 7, np.array([1.0]), 12.5, 100 + ) + + assert payload['iteration'] == 7 + assert payload['total_steps'] == 100 + assert payload['chi2'] == 25.0 + assert payload['reduced_chi2'] == 12.5 + assert payload['parameter_values'] == {'alpha': 1.0} + assert payload['sampling'] is True + assert payload['finished'] is False + assert payload['refresh_plots'] is False + # The nllf already computed by the sampler is reused β€” no model + # re-evaluation via setp. + mock_problem.chisq.assert_any_call(nllf=12.5, norm=False) + mock_problem.chisq.assert_any_call(nllf=12.5, norm=True) + mock_problem.setp.assert_not_called() + + def test_payload_keys(self, engine: DreamSampler) -> None: + """Same keys as the classical-fit payload, plus ``sampling``.""" + mock_problem = MagicMock() + mock_problem.chisq.side_effect = [10.0, 5.0] + mock_problem.labels.return_value = ['pa'] + mock_problem.getp.return_value = np.array([5.0]) + + payload = engine._build_sample_progress_payload( + mock_problem, 1, np.array([5.0]), nllf=5.0, total_steps=50 + ) + + expected_keys = { + 'iteration', + 'chi2', + 'reduced_chi2', + 'parameter_values', + 'refresh_plots', + 'finished', + 'sampling', + 'total_steps', + } + assert set(payload.keys()) == expected_keys diff --git a/tests/unit/fitting/samplers/test_validation.py b/tests/unit/fitting/samplers/test_validation.py new file mode 100644 index 00000000..6a4b5dc0 --- /dev/null +++ b/tests/unit/fitting/samplers/test_validation.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for the DREAM run-settings validation helper.""" + +import pytest + +from easyscience.fitting.samplers.validation import validate_run_settings + + +class TestValidateRunSettings: + @pytest.mark.parametrize( + 'kwargs, match', + [ + ({'samples': 0}, 'samples must be a positive integer'), + ({'samples': -1}, 'samples must be a positive integer'), + ({'samples': 10.0}, 'samples must be a positive integer'), + # bool is an int subclass; True must not sneak through as 1. + ({'samples': True}, 'samples must be a positive integer'), + ({'burn': -1}, 'burn must be a non-negative integer'), + ({'burn': 1.5}, 'burn must be a non-negative integer'), + ({'burn': True}, 'burn must be a non-negative integer'), + ({'burn': False}, 'burn must be a non-negative integer'), + ({'thin': 0}, 'thin must be a positive integer'), + ({'thin': 2.0}, 'thin must be a positive integer'), + ({'thin': True}, 'thin must be a positive integer'), + ], + ) + def test_invalid_settings_raise(self, kwargs, match): + settings = {'samples': 10, 'burn': 0, 'thin': 1} + settings.update(kwargs) + with pytest.raises(ValueError, match=match): + validate_run_settings(**settings) + + def test_valid_settings_pass(self): + validate_run_settings(samples=1, burn=0, thin=1) + validate_run_settings(samples=10000, burn=2000, thin=10) diff --git a/tests/unit/fitting/test_engine_base.py b/tests/unit/fitting/test_engine_base.py new file mode 100644 index 00000000..92886ccf --- /dev/null +++ b/tests/unit/fitting/test_engine_base.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for the engine-agnostic helpers in ``engine_base.py``.""" + +import numpy as np +import pytest + +from easyscience.fitting.engine_base import EngineBase + +validate_arrays = EngineBase.validate_arrays + + +class TestValidateArrays: + @staticmethod + def _data(): + return { + 'x': np.array([1.0, 2.0]), + 'y': np.array([0.1, 0.2]), + 'weights': np.array([1.0, 1.0]), + } + + @pytest.mark.parametrize( + 'overrides, match', + [ + ({'y': np.array([0.1])}, 'x and y must have the same shape'), + ({'x': np.array([1.0, np.nan])}, 'x cannot contain NaN'), + ({'x': np.array([1.0, np.inf])}, 'x cannot contain NaN'), + ({'y': np.array([0.1, np.nan])}, 'y cannot contain NaN'), + ({'y': np.array([0.1, np.inf])}, 'y cannot contain NaN'), + ({'weights': np.array([1.0])}, 'Weights must have the same shape'), + ({'weights': np.array([1.0, np.nan])}, 'Weights cannot be NaN'), + ({'weights': np.array([1.0, np.inf])}, 'Weights cannot be NaN'), + ({'weights': np.array([1.0, 0.0])}, 'Weights must be strictly positive'), + ({'weights': np.array([1.0, -1.0])}, 'Weights must be strictly positive'), + ({'x': np.array([1.0, None], dtype=object)}, 'x must hold numeric values'), + ({'y': np.array(['a', 'b'])}, 'y must hold numeric values'), + ({'weights': np.asarray(None)}, 'weights must hold numeric values'), + ], + ) + def test_invalid_arrays_raise(self, overrides, match): + data = self._data() + data.update(overrides) + with pytest.raises(ValueError, match=match): + validate_arrays(**data) + + def test_valid_arrays_pass(self): + validate_arrays(**self._data()) diff --git a/tests/unit/fitting/test_fitter.py b/tests/unit/fitting/test_fitter.py index 702f5e59..400052b0 100644 --- a/tests/unit/fitting/test_fitter.py +++ b/tests/unit/fitting/test_fitter.py @@ -7,8 +7,8 @@ import pytest import easyscience.fitting.fitter -from easyscience import AvailableMinimizers from easyscience import Fitter +from easyscience import Minimizers class TestFitter: @@ -24,9 +24,9 @@ def test_constructor(self, fitter: Fitter): assert fitter._fit_object == self.mock_fit_object assert fitter._fit_function == self.mock_fit_function assert fitter._dependent_dims is None - assert fitter._enum_current_minimizer is None # == AvailableMinimizers.LMFit_leastsq + assert fitter._enum_current_minimizer is None # == Minimizers.LMFit_leastsq assert fitter._minimizer is None - fitter._update_minimizer.assert_called_once_with(AvailableMinimizers.LMFit_leastsq) + fitter._update_minimizer.assert_called_once_with(Minimizers.LMFit_leastsq) def test_make_model(self, fitter: Fitter): # When @@ -54,19 +54,6 @@ def test_evaluate(self, fitter: Fitter): assert result == 'result' mock_minimizer.evaluate.assert_called_once_with('pars') - def test_convert_to_pars_obj(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.convert_to_pars_obj = MagicMock(return_value='obj') - fitter._minimizer = mock_minimizer - - # Then - obj = fitter.convert_to_pars_obj('pars') - - # Expect - assert obj == 'obj' - mock_minimizer.convert_to_pars_obj.assert_called_once_with('pars') - def test_initialize(self, fitter: Fitter): # When mock_fit_object = MagicMock() @@ -80,41 +67,31 @@ def test_initialize(self, fitter: Fitter): assert fitter._fit_function == mock_fit_function fitter._update_minimizer.count(2) - def test_create(self, fitter: Fitter, monkeypatch): + def test_switch_minimizer(self, fitter: Fitter): # When fitter._update_minimizer = MagicMock() - mock_string_to_enum = MagicMock(return_value=10) - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) # Then - fitter.create('great-minimizer') + fitter.switch_minimizer(Minimizers.Bumps) # Expect - mock_string_to_enum.assert_called_once_with('great-minimizer') - fitter._update_minimizer.assert_called_once_with(10) + fitter._update_minimizer.assert_called_once_with(Minimizers.Bumps) - def test_switch_minimizer(self, fitter: Fitter, monkeypatch): + def test_switch_minimizer_rejects_string(self, fitter: Fitter): # When - mock_minimizer = MagicMock() - fitter._minimizer = mock_minimizer - mock_string_to_enum = MagicMock(return_value=10) - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) - - # Then - fitter.switch_minimizer('great-minimizer') + fitter._update_minimizer = MagicMock() - # Expect - fitter._update_minimizer.count(2) - mock_string_to_enum.assert_called_once_with('great-minimizer') + # Then Expect + with pytest.raises(TypeError, match='Minimizers'): + fitter.switch_minimizer('Bumps') + fitter._update_minimizer.assert_not_called() def test_update_minimizer(self, monkeypatch): # When mock_fit_object = MagicMock() mock_fit_function = MagicMock() - mock_string_to_enum = MagicMock(return_value=10) mock_factory = MagicMock(return_value='minimizer') - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) monkeypatch.setattr(easyscience.fitting.fitter, 'factory', mock_factory) fitter = Fitter(mock_fit_object, mock_fit_function) @@ -130,20 +107,7 @@ def test_available_minimizers(self, fitter: Fitter): minimizers = fitter.available_minimizers # Then Expect - assert minimizers == [ - 'LMFit', - 'LMFit_leastsq', - 'LMFit_powell', - 'LMFit_cobyla', - 'LMFit_differential_evolution', - 'LMFit_scipy_least_squares', - 'Bumps', - 'Bumps_simplex', - 'Bumps_newton', - 'Bumps_lm', - 'DFO', - 'DFO_leastsq', - ] + assert minimizers == list(Minimizers) def test_minimizer(self, fitter: Fitter): # When diff --git a/tests/unit/fitting/test_multi_fitter.py b/tests/unit/fitting/test_multi_fitter.py index ac897653..885755da 100644 --- a/tests/unit/fitting/test_multi_fitter.py +++ b/tests/unit/fitting/test_multi_fitter.py @@ -1,25 +1,41 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +import logging from unittest.mock import MagicMock import numpy as np import pytest -from easyscience import ObjBase from easyscience import Parameter +from easyscience import global_object +from easyscience.base_classes import EasyList +from easyscience.base_classes import ModelBase from easyscience.fitting.fitter import Fitter from easyscience.fitting.multi_fitter import MultiFitter -class Line(ObjBase): - m: Parameter - c: Parameter - +class Line(ModelBase): def __init__(self, m_val: float, c_val: float): - m = Parameter('m', m_val) - c = Parameter('c', c_val) - super().__init__('line', m=m, c=c) + super().__init__() + self._m = Parameter(m_val, display_name='m') + self._c = Parameter(c_val, display_name='c') + + @property + def m(self) -> Parameter: + return self._m + + @m.setter + def m(self, value: float) -> None: + self._m.value = value + + @property + def c(self) -> Parameter: + return self._c + + @c.setter + def c(self, value: float) -> None: + self._c.value = value def __call__(self, x): return self.m.value * x + self.c.value @@ -142,6 +158,33 @@ def test_handles_single_dataset(self): assert np.allclose(results[0].y_calc, [1.1, 2.1, 3.1]) +# =================================================================== +# MultiFitter._fit_function_wrapper +# =================================================================== + + +class TestFitFunctionWrapper: + def test_fit_function_restored_with_multiple_datasets(self): + """Wrapping must not leave ``fit_function`` pointing at the last + dataset's function (regression: sampling on a 2+ dataset MultiFitter + silently swapped the user-visible fit function).""" + fit_objects = [Line(1.0, 0.5), Line(2.0, 1.5)] + mf = MultiFitter(fit_objects, fit_objects) + original = mf.fit_function + assert original is fit_objects[0] + + x = [np.array([0.0, 1.0, 2.0]), np.array([0.0, 1.0])] + mf._dependent_dims = [(3,), (2,)] + wrapped = mf._fit_function_wrapper(x, flatten=True) + + assert mf.fit_function is original + + # Each wrapped section still evaluates its own dataset's function. + y = wrapped(np.zeros(5)) + expected = np.hstack([fit_objects[0](x[0]), fit_objects[1](x[1])]) + assert np.allclose(y, expected) + + # =================================================================== # MultiFitter._precompute_reshaping with weights=None # =================================================================== @@ -165,3 +208,95 @@ def test_weights_all_none_returns_none(self): assert w_new is None assert len(dims) == 2 + + # =================================================================== + # The EasyList container + # =================================================================== + + def test_container_is_an_easy_list(self): + fit_objects = [Line(1.0, 0.5), Line(2.0, 1.5)] + + assert isinstance(MultiFitter(fit_objects, fit_objects).fit_object, EasyList) + + def test_sequence_contract(self): + fit_objects = [Line(1.0, 0.5), Line(2.0, 1.5)] + container = MultiFitter(fit_objects, fit_objects).fit_object + + assert container[0] is fit_objects[0] + assert container[1] is fit_objects[1] + assert container[-1] is fit_objects[1] + assert list(container[0:1]) == [fit_objects[0]] + assert len(container) == 2 + assert list(container) == fit_objects + assert fit_objects[0] in container + assert container[fit_objects[0].unique_name] is fit_objects[0] + + def test_get_free_parameters_concatenates_in_order(self): + fit_objects = [Line(1.0, 0.5), Line(2.0, 1.5)] + container = MultiFitter(fit_objects, fit_objects).fit_object + + assert container.get_free_parameters() == [ + *fit_objects[0].get_free_parameters(), + *fit_objects[1].get_free_parameters(), + ] + + def test_rejects_foreign_object(self): + with pytest.raises(TypeError, match='Items must be one of'): + MultiFitter([Line(1.0, 0.5), 'not a model'], [None, None]) + + def test_rejects_bare_parameter(self): + """The ModelBase-gated EasyList rejects bare parameters (a + Parameter is an EasyBase, but EasyList only harvests parameters + from ModelBase members).""" + model = Line(1.0, 0.5) + with pytest.raises(TypeError, match='Items must be one of'): + MultiFitter([model, Parameter(1.0, display_name='p')], [model, None]) + + def test_flattens_nested_list(self): + """Nested lists are flattened.""" + models = [Line(1.0, 0.5), Line(2.0, 1.5)] + + container = MultiFitter([models[0], [models[1]]], models).fit_object + + assert list(container) == models + + def test_accepts_fit_objects_as_a_tuple(self): + """Any sequence works, not just a list. + + easyreflectometry passes its models as a tuple; a container that + only flattened lists wrapped the whole tuple as a single item. + """ + models = (Line(1.0, 0.5), Line(2.0, 1.5)) + + container = MultiFitter(models, list(models)).fit_object + + assert list(container) == list(models) + + # =================================================================== + # Constructor defaults + # =================================================================== + + def test_default_arguments_do_not_crash(self): + """Both arguments default to None; construction must not index into them.""" + mf = MultiFitter() + + assert isinstance(mf.fit_object, EasyList) + assert len(mf.fit_object) == 0 + assert mf._fit_functions == [] + assert mf.fit_function is None + + def test_none_arguments_are_treated_as_empty(self): + mf = MultiFitter(None, None) + + assert len(mf.fit_object) == 0 + assert mf._fit_functions == [] + assert mf.fit_function is None + + def test_accepts_tuple_arguments(self): + """Any sequence works thanks to *-unpacking.""" + models = (Line(1.0, 0.5), Line(2.0, 1.5)) + mf = MultiFitter(models, models) + + assert list(mf.fit_object) == list(models) + assert mf._fit_functions == list(models) + assert mf.fit_function is models[0] diff --git a/tests/unit/fitting/test_reshaping.py b/tests/unit/fitting/test_reshaping.py new file mode 100644 index 00000000..aada7c91 --- /dev/null +++ b/tests/unit/fitting/test_reshaping.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for ``reshaping.py`` β€” the data reshaping and fit-function +wrapping shared by ``Fitter``, ``MultiFitter`` and ``Sampler``.""" + +import numpy as np +import pytest + +from easyscience.fitting.reshaping import inject_x +from easyscience.fitting.reshaping import inject_x_multi +from easyscience.fitting.reshaping import reshape_dataset +from easyscience.fitting.reshaping import reshape_datasets + + +def _grid(n: int, m: int): + """A vectorized ``(n, m, 2)`` coordinate grid and its ``(n, m)`` dependent values.""" + X, Y = np.meshgrid(np.linspace(0, 1, m), np.linspace(0, 1, n)) + x = np.stack((X, Y), axis=2) + return x, X + 2 * Y + + +def _sum_xy(x): + return x[..., 0] + 2 * x[..., 1] + + +class TestReshapeDataset: + def test_1d_dims_are_y_shape(self): + x = np.linspace(0, 1, 5) + x_fit, x_new, y_new, w_new, dims = reshape_dataset(x, 2 * x, np.ones(5), vectorized=False) + assert dims == (5,) + assert x_fit.shape == y_new.shape == w_new.shape == (5,) + + def test_vectorized_dims_are_y_shape_not_x_shape(self): + """For multi-dimensional coordinates the dependent dims must exclude + the coordinate-component axis: ``(2, 3, 2)`` x holds 6 observations.""" + x, y = _grid(2, 3) + _, x_new, y_new, _, dims = reshape_dataset(x, y, None, vectorized=True) + assert dims == (2, 3) + assert y_new.shape == (6,) + assert x_new.shape == (2, 3, 2) + + def test_non_vectorized_nd_dims_are_y_shape(self): + x = np.random.default_rng(0).random((6, 2)) + y = np.arange(6.0) + _, x_new, y_new, _, dims = reshape_dataset(x, y, None, vectorized=False) + assert dims == (6,) + assert x_new.shape == (6, 2) + + +class TestInjectX: + def test_injects_real_x_and_flattens(self): + x, y = _grid(2, 3) + wrapped = inject_x(_sum_xy, x, flatten=True) + np.testing.assert_allclose(wrapped(np.zeros(6)), y.flatten()) + + +class TestInjectXMulti: + def test_two_multidimensional_datasets_are_sliced_by_observation_count(self): + """Regression: slicing by the product of the x shape allocated twice as + many output positions as observations for vectorized grids, which + raised on the first dataset and silently clipped on the last.""" + x1, y1 = _grid(2, 3) + x2, y2 = _grid(3, 4) + x_fit, x_new, y_new, _, dims = reshape_datasets([x1, x2], [y1, y2], None, vectorized=True) + assert dims == [(2, 3), (3, 4)] + assert y_new.shape == (18,) + + wrapped = inject_x_multi([_sum_xy, _sum_xy], x_new, dims) + np.testing.assert_allclose(wrapped(x_fit), np.hstack([y1.flatten(), y2.flatten()])) + + def test_multidimensional_dataset_first_then_1d(self): + """Dataset boundaries hold regardless of order; a 2D dataset that is + not last used to raise a broadcast error.""" + x1, y1 = _grid(2, 3) + x2 = np.linspace(0, 1, 4) + y2 = 3 * x2 + x_fit, x_new, y_new, _, dims = reshape_datasets([x1, x2], [y1, y2], None, vectorized=True) + assert dims == [(2, 3), (4,)] + + wrapped = inject_x_multi([_sum_xy, lambda x: 3 * x], x_new, dims) + np.testing.assert_allclose(wrapped(x_fit), np.hstack([y1.flatten(), y2])) + + def test_weights_none_for_all_datasets(self): + x = np.linspace(0, 1, 3) + _, _, _, w_new, _ = reshape_datasets([x, x], [x, x], [None, None], vectorized=False) + assert w_new is None + + def test_weights_are_concatenated(self): + x = np.linspace(0, 1, 3) + _, _, _, w_new, _ = reshape_datasets( + [x, x], [x, x], [np.ones(3), 2 * np.ones(3)], vectorized=False + ) + np.testing.assert_array_equal(w_new, [1, 1, 1, 2, 2, 2]) + + +@pytest.mark.parametrize('vectorized', [False, True]) +def test_shape_mismatch_raises(vectorized): + x = np.linspace(0, 1, 4) + with pytest.raises(ValueError, match='shape of the x and y data must be the same'): + reshape_dataset(x, np.zeros(3), None, vectorized=vectorized) diff --git a/tests/unit/fitting/test_sampler.py b/tests/unit/fitting/test_sampler.py index 665c1d66..9671865f 100644 --- a/tests/unit/fitting/test_sampler.py +++ b/tests/unit/fitting/test_sampler.py @@ -13,34 +13,52 @@ import numpy as np import pytest -from easyscience import ObjBase from easyscience import Parameter +from easyscience.base_classes import ModelBase +from easyscience.fitting import Fitter from easyscience.fitting import Sampler from easyscience.fitting import SamplingResults -from easyscience.fitting.minimizers.minimizer_base import MINIMIZER_PARAMETER_PREFIX +from easyscience.fitting.engine_base import PARAMETER_PREFIX from easyscience.fitting.multi_fitter import MultiFitter from easyscience.fitting.sampler import _data_fingerprint from easyscience.fitting.sampler import load_chain -class AbsSin(ObjBase): - phase: Parameter - offset: Parameter - +class AbsSin(ModelBase): def __init__(self, offset_val: float, phase_val: float): - offset = Parameter('offset', offset_val) - phase = Parameter('phase', phase_val) - super().__init__('sin', offset=offset, phase=phase) + super().__init__() + self._offset = Parameter(offset_val, display_name='offset') + self._phase = Parameter(phase_val, display_name='phase') + + @property + def offset(self) -> Parameter: + return self._offset + + @offset.setter + def offset(self, value: float) -> None: + self._offset.value = value + + @property + def phase(self) -> Parameter: + return self._phase + + @phase.setter + def phase(self, value: float) -> None: + self._phase.value = value def __call__(self, x): return np.abs(np.sin(self.phase.value * x + self.offset.value)) -class _StubFitter: - """Duck-types the Fitter attributes checked by the Sampler constructor.""" +class _StubModel: + """Duck-types the model attribute checked by the Sampler constructor.""" + + def get_free_parameters(self): + return [] + - minimizer = None - fit_function = None +def _identity(x): + return x class _StubState: @@ -50,8 +68,9 @@ def __init__(self, labels): self.labels = list(labels) -def _bumps_fitter_and_data(): - """Build a 2-parameter BUMPS MultiFitter over a small sine model.""" +def _model_and_data(): + """Build a 2-parameter sine model and a small dataset to sample.""" + pytest.importorskip('bumps') ref_sin = AbsSin(0.2, np.pi) sp = AbsSin(0.354, 3.05) sp.offset.fixed = False @@ -59,12 +78,7 @@ def _bumps_fitter_and_data(): x = np.linspace(0, 5, 50) y = ref_sin(x) weights = np.ones_like(x) - f = MultiFitter([sp], [sp]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - return f, sp, x, y, weights + return sp, x, y, weights def _xyw(): @@ -93,68 +107,109 @@ def _make_state(ngen=6, npop=5, nvar=2, seed=7): class TestSamplerConstructorValidation: - def test_rejects_fitter_without_minimizer(self): + def test_rejects_fit_object_without_fit_parameters(self): x, y, w = _xyw() - with pytest.raises(TypeError, match='fitter must be a configured Fitter'): - Sampler(object(), [x], [y], [w]) + with pytest.raises(TypeError, match='fit_object must be an EasyScience model'): + Sampler(object(), [_identity], [x], [y], [w]) + + def test_rejects_non_callable_fit_function(self): + x, y, w = _xyw() + with pytest.raises(TypeError, match='fit_function must be callable'): + Sampler(_StubModel(), 'not-callable', x, y, w) + with pytest.raises(TypeError, match='fit_function must be callable'): + Sampler(_StubModel(), [_identity, None], [x, x], [y, y], [w, w]) + + def test_rejects_fit_function_structure_mismatch(self): + """One callable for a list of datasets (or vice versa) is an error: + multi-dataset sampling takes one fit function per dataset.""" + x, y, w = _xyw() + with pytest.raises(ValueError, match='fit_function must be a list of callables'): + Sampler(_StubModel(), _identity, [x], [y], [w]) + with pytest.raises(ValueError, match='fit_function must be a list of callables'): + Sampler(_StubModel(), [_identity], x, y, w) + + def test_rejects_fit_function_count_mismatch(self): + x, y, w = _xyw() + with pytest.raises(ValueError, match='one callable per dataset'): + Sampler(_StubModel(), [_identity], [x, x], [y, y], [w, w]) + + def test_rejects_list_of_fit_objects(self): + """Multiple datasets take one container object exposing all the + parameters, not a bare list of models.""" + sp_1 = AbsSin(0.1, 1.0) + sp_2 = AbsSin(0.2, 2.0) + x, y, w = _xyw() + with pytest.raises(TypeError, match='fit_object must be an EasyScience model'): + Sampler([sp_1, sp_2], [sp_1, sp_2], [x, x], [y, y], [w, w]) + + def test_requires_weights(self): + """Sampling has no default weighting, so weights are a required + argument rather than a None that only blows up at sample().""" + x, y, _ = _xyw() + with pytest.raises(TypeError, match='weights'): + Sampler(_StubModel(), [_identity], [x], [y]) def test_rejects_mixed_array_and_list(self): x, y, w = _xyw() with pytest.raises(ValueError, match='both be arrays or both be lists'): - Sampler(_StubFitter(), [x], y) + Sampler(_StubModel(), [_identity], [x], y, [w]) def test_rejects_dataset_count_mismatch(self): x, y, w = _xyw() with pytest.raises(ValueError, match='same number of datasets'): - Sampler(_StubFitter(), [x, x], [y]) + Sampler(_StubModel(), [_identity, _identity], [x, x], [y], [w, w]) def test_rejects_weights_structure_mismatch(self): x, y, w = _xyw() with pytest.raises(ValueError, match='weights must match the structure'): - Sampler(_StubFitter(), [x], [y], w) + Sampler(_StubModel(), [_identity], [x], [y], w) def test_rejects_weights_count_mismatch(self): x, y, w = _xyw() with pytest.raises(ValueError, match='weights must hold the same number'): - Sampler(_StubFitter(), [x], [y], [w, w]) + Sampler(_StubModel(), [_identity], [x], [y], [w, w]) def test_rejects_non_bool_vectorized(self): x, y, w = _xyw() with pytest.raises(TypeError, match='vectorized must be a bool'): - Sampler(_StubFitter(), [x], [y], [w], vectorized=1) + Sampler(_StubModel(), [_identity], [x], [y], [w], vectorized=1) def test_rejects_non_dict_sampler_kwargs(self): x, y, w = _xyw() with pytest.raises(TypeError, match='sampler_kwargs must be a dict'): - Sampler(_StubFitter(), [x], [y], [w], sampler_kwargs=[('init', 'random')]) + Sampler(_StubModel(), [_identity], [x], [y], [w], sampler_kwargs=[('init', 'random')]) def test_accepts_single_arrays(self): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), x, y, w) + sampler = Sampler(_StubModel(), _identity, x, y, w) assert sampler.results is None class TestSamplerDataBinding: def test_properties_expose_bound_data(self): x, y, w = _xyw() - f = _StubFitter() - sampler = Sampler(f, [x], [y], [w]) - assert sampler.fitter is f - np.testing.assert_array_equal(sampler.x[0], x) - np.testing.assert_array_equal(sampler.y[0], y) - np.testing.assert_array_equal(sampler.weights[0], w) - - def test_weights_property_none_when_unset(self): - x, y, _ = _xyw() - sampler = Sampler(_StubFitter(), [x], [y]) - assert sampler.weights is None + model = _StubModel() + sampler = Sampler(model, _identity, x, y, w) + assert sampler.fit_object is model + assert sampler.fit_function is _identity + np.testing.assert_array_equal(sampler.x, x) + np.testing.assert_array_equal(sampler.y, y) + np.testing.assert_array_equal(sampler.weights, w) + + def test_properties_keep_list_structure(self): + """Multi-dataset inputs come back as lists, in the order given.""" + x, y, w = _xyw() + sampler = Sampler(_StubModel(), [_identity, _identity], [x, 2 * x], [y, y], [w, w]) + assert sampler.fit_function == [_identity, _identity] + assert len(sampler.x) == 2 + np.testing.assert_array_equal(sampler.x[1], 2 * x) def test_inputs_are_copied(self): """Mutating the caller's arrays after construction must not change the bound data (nor the save() fingerprint derived from it).""" x, y, w = _xyw() y_original = y.copy() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) fingerprint_before = sampler._fingerprint() y[:] = 0.0 @@ -164,7 +219,7 @@ def test_inputs_are_copied(self): def test_bound_arrays_are_read_only(self): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), x, y, w) + sampler = Sampler(_StubModel(), _identity, x, y, w) with pytest.raises(ValueError, match='read-only'): sampler.x[0] = 99.0 @@ -172,8 +227,8 @@ def test_data_properties_have_no_setters(self): """Bound data is deliberately immutable β€” sample new data with a new Sampler, so a chain can never be extended against different data.""" x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) - for name in ('fitter', 'x', 'y', 'weights'): + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) + for name in ('fit_object', 'fit_function', 'x', 'y', 'weights'): with pytest.raises(AttributeError): setattr(sampler, name, None) @@ -181,20 +236,20 @@ def test_data_properties_have_no_setters(self): class TestSamplerPathValidation: def test_save_rejects_non_pathlike(self): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) with pytest.raises(TypeError, match='path must be a str or os.PathLike'): sampler.save(123) def test_save_accepts_pathlike(self, tmp_path): """A Path object passes validation; the empty sampler then raises RuntimeError.""" x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) with pytest.raises(RuntimeError, match='No chain state to save'): sampler.save(tmp_path / 'chain') def test_load_state_rejects_non_pathlike(self): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) with pytest.raises(TypeError, match='path must be a str or os.PathLike'): sampler.load_state(123) @@ -209,36 +264,30 @@ def test_load_chain_rejects_bad_skip(self, tmp_path, skip): class TestSamplerErrorPaths: - def test_sample_requires_bumps(self): - """sample() must raise RuntimeError if the minimizer is not BUMPS β€” - and must not mutate the fitter (no needless minimizer rebuild).""" - sp = AbsSin(0.354, 3.05) - f = MultiFitter([sp], [sp]) - + def test_sample_requires_bumps_package(self, monkeypatch): + """sample() must raise RuntimeError when the bumps package is not + installed.""" x, y, w = _xyw() - sampler = Sampler(f, [x], [y], [w]) - minimizer_before = f.minimizer - with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + sampler = Sampler(_StubModel(), _identity, x, y, w) + monkeypatch.setattr( + 'easyscience.fitting.minimizers.minimizers.bumps_engine_available', False + ) + with pytest.raises(RuntimeError, match='requires the bumps package'): sampler.sample(samples=10, burn=5, thin=1) - assert f.minimizer is minimizer_before - def test_fit_function_restored_on_error(self): - """fit_function must be restored even when the minimizer raises.""" - f, _, x, y, weights = _bumps_fitter_and_data() - sampler = Sampler(f, [x], [y], [weights]) - original_func = f.fit_function + def test_engine_argument_errors_propagate(self): + """Invalid ``samples`` is rejected by the engine (single source of + validation) and surfaces unchanged.""" + sp, x, y, weights = _model_and_data() + sampler = Sampler(sp, sp, x, y, weights) - # Invalid `samples` is rejected by the minimizer (single source of - # validation) *after* the fitter has been mutated for sampling. with pytest.raises(ValueError, match='samples must be a positive integer'): sampler.sample(samples=-1, burn=5, thin=1) - assert f.fit_function is original_func - def test_extend_requires_existing_state(self): """extend() before sample()/load_state() raises RuntimeError.""" x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) with pytest.raises(RuntimeError, match='No chain to extend'): sampler.extend(additional_samples=10) @@ -246,7 +295,7 @@ def test_extend_requires_existing_state(self): def test_save_raises_without_state(self, tmp_path): """save() before sample() raises RuntimeError.""" x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) with pytest.raises(RuntimeError, match='No chain state to save'): sampler.save(str(tmp_path / 'chain')) @@ -255,7 +304,7 @@ def test_sample_warns_when_replacing_existing_chain(self, monkeypatch, caplog): """sample() over an existing chain logs a replace warning; a fresh sampler does not.""" x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x], [y], [w]) + sampler = Sampler(_StubModel(), [_identity], [x], [y], [w]) dummy = SamplingResults( draws=np.zeros((1, 1)), param_names=['p'], logp=np.zeros(1), state=object() @@ -320,7 +369,7 @@ def test_missing_sidecar_falls_back_to_state_labels(self, tmp_path): assert names == ['P0', 'P1'] def test_fallback_strips_minimizer_prefix_from_labels(self, tmp_path): - self.state.labels = [f'{MINIMIZER_PARAMETER_PREFIX}a', f'{MINIMIZER_PARAMETER_PREFIX}b'] + self.state.labels = [f'{PARAMETER_PREFIX}a', f'{PARAMETER_PREFIX}b'] _, names, _ = load_chain(str(tmp_path / 'chain')) assert names == ['a', 'b'] @@ -345,74 +394,61 @@ class TestSamplerConstructorDataValidation: """Scalars, strings and empty/ragged data must be rejected at construction.""" def test_rejects_scalar_x(self): - _, y, _ = _xyw() + _, y, w = _xyw() with pytest.raises(ValueError, match='x must be an array of values, got a scalar'): - Sampler(_StubFitter(), 5.0, y) + Sampler(_StubModel(), _identity, 5.0, y, w) def test_rejects_scalar_dataset_in_list(self): - x, y, _ = _xyw() + x, y, w = _xyw() with pytest.raises(ValueError, match=r'y\[1\] must be an array of values'): - Sampler(_StubFitter(), [x, x], [y, 3.0]) + Sampler(_StubModel(), [_identity, _identity], [x, x], [y, 3.0], [w, w]) def test_rejects_string_data(self): - x, _, _ = _xyw() + x, _, w = _xyw() with pytest.raises(TypeError, match='y must hold numeric values'): - Sampler(_StubFitter(), x, 'abc') + Sampler(_StubModel(), _identity, x, 'abc', w) def test_rejects_non_numeric_object_array(self): _, y, _ = _xyw() with pytest.raises(TypeError, match='x must hold numeric values'): - Sampler(_StubFitter(), np.array([{}, {}], dtype=object), y) + Sampler(_StubModel(), _identity, np.array([{}, {}], dtype=object), y, np.ones(2)) def test_rejects_empty_array(self): with pytest.raises(ValueError, match='x must not be empty'): - Sampler(_StubFitter(), np.array([]), np.array([])) + Sampler(_StubModel(), _identity, np.array([]), np.array([]), np.array([])) def test_rejects_ragged_dataset(self): with pytest.raises(TypeError, match=r'x\[0\] could not be converted'): - Sampler(_StubFitter(), [[1.0, [2.0, 3.0]]], [np.zeros(3)]) + Sampler(_StubModel(), [_identity], [[1.0, [2.0, 3.0]]], [np.zeros(3)], [np.ones(3)]) def test_rejects_scalar_weights(self): x, y, _ = _xyw() with pytest.raises(ValueError, match='weights must be an array of values'): - Sampler(_StubFitter(), x, y, 2.0) + Sampler(_StubModel(), _identity, x, y, 2.0) - def test_weights_list_may_hold_none_entries(self): + def test_rejects_none_weight_entry(self): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), [x, x], [y, y], [w, None]) - assert sampler.weights[1] is None + with pytest.raises(TypeError, match=r'weights\[1\] must hold numeric values'): + Sampler(_StubModel(), [_identity, _identity], [x, x], [y, y], [w, None]) class TestDataFingerprint: def test_returns_none_on_unhashable_data(self): assert _data_fingerprint([object()], [], []) is None - def test_fingerprint_without_weights(self): - x, y, _ = _xyw() - sampler = Sampler(_StubFitter(), x, y) + def test_fingerprint_of_single_arrays(self): + x, y, w = _xyw() + sampler = Sampler(_StubModel(), _identity, x, y, w) assert isinstance(sampler._fingerprint(), str) -class TestSamplingResultsLegacyDict: - def test_to_legacy_dict_maps_fields(self): - state = object() - results = SamplingResults( - draws=np.ones((2, 1)), param_names=['p'], logp=np.zeros(2), state=state - ) - legacy = results.to_legacy_dict() - assert legacy['internal_bumps_object'] is state - assert legacy['param_names'] == ['p'] - np.testing.assert_array_equal(legacy['draws'], results.draws) - np.testing.assert_array_equal(legacy['logp'], results.logp) - - class TestSamplerRunEngine: """The ``_run`` tail: results construction, storage, and kwarg merging, - with the minimizer's sampling entry point stubbed out.""" + with the ``DreamSampler`` engine stubbed out.""" def test_run_stores_results_and_exposes_properties(self, monkeypatch): - f, _, x, y, weights = _bumps_fitter_and_data() - from easyscience.fitting.minimizers.minimizer_bumps import Bumps + sp, x, y, weights = _model_and_data() + from easyscience.fitting.samplers.sampler_bumps import DreamSampler canned = { 'draws': np.arange(8.0).reshape(4, 2), @@ -422,14 +458,13 @@ def test_run_stores_results_and_exposes_properties(self, monkeypatch): } captured = {} - def fake_mcmc_sample(self, **kwargs): + def fake_run(self, **kwargs): captured.update(kwargs) return dict(canned) - monkeypatch.setattr(Bumps, 'mcmc_sample', fake_mcmc_sample) + monkeypatch.setattr(DreamSampler, 'run', fake_run) - sampler = Sampler(f, [x], [y], [weights], sampler_kwargs={'trim': False}) - original_func = f.fit_function + sampler = Sampler(sp, sp, x, y, weights, sampler_kwargs={'trim': False}) results = sampler.sample(samples=100, burn=10, thin=2, sampler_kwargs={'init': 'lhs'}) assert isinstance(results, SamplingResults) @@ -443,8 +478,72 @@ def fake_mcmc_sample(self, **kwargs): assert captured['samples'] == 100 assert captured['burn'] == 10 assert captured['resume_state'] is None - # The fitter's fit function is restored after the run. - assert f.fit_function is original_func + + def test_run_binds_engine_to_model_and_wrapped_function(self, monkeypatch): + """The engine is built from the sampler's own model and a wrapped + fit function; no Fitter or minimizer is involved.""" + sp, x, y, weights = _model_and_data() + from easyscience.fitting.samplers.sampler_bumps import DreamSampler + + constructed = {} + original_init = DreamSampler.__init__ + + def spy_init(self, obj, fit_function): + constructed['obj'] = obj + constructed['fit_function'] = fit_function + original_init(self, obj, fit_function) + + canned = { + 'draws': np.zeros((2, 2)), + 'param_names': ['offset', 'phase'], + 'logp': np.zeros(2), + 'internal_bumps_object': object(), + } + monkeypatch.setattr(DreamSampler, '__init__', spy_init) + monkeypatch.setattr(DreamSampler, 'run', lambda self, **kwargs: dict(canned)) + + sampler = Sampler(sp, sp, x, y, weights) + results = sampler.sample(samples=10, burn=0, thin=1) + + assert results.param_names == ['offset', 'phase'] + assert constructed['obj'] is sp + assert callable(constructed['fit_function']) + assert constructed['fit_function'] is not sp + + +class TestSamplerFromFitter: + """``from_fitter`` mirrors direct construction and leaves the fitter alone.""" + + def test_from_plain_fitter(self): + sp, x, y, weights = _model_and_data() + f = Fitter(sp, sp) + + sampler = Sampler.from_fitter(f, x, y, weights, sampler_kwargs={'init': 'lhs'}) + + assert sampler.fit_object is sp + assert sampler.fit_function is sp + np.testing.assert_array_equal(sampler.x, x) + assert sampler._default_sampler_kwargs == {'init': 'lhs'} + + def test_from_multi_fitter(self): + sp_1 = AbsSin(0.1, 1.0) + sp_2 = AbsSin(0.2, 2.0) + x, y, w = _xyw() + f = MultiFitter([sp_1, sp_2], [sp_1, sp_2]) + + sampler = Sampler.from_fitter(f, [x, x], [y, y], [w, w]) + + # The fitter's container object exposes every model's parameters. + assert sampler.fit_object is f.fit_object + assert sampler.fit_function == [sp_1, sp_2] + assert f.fit_function is sp_1 # the fitter is untouched + + def test_multi_fitter_requires_list_data(self): + sp_1 = AbsSin(0.1, 1.0) + x, y, w = _xyw() + f = MultiFitter([sp_1], [sp_1]) + with pytest.raises(ValueError, match='fit_function must be a list of callables'): + Sampler.from_fitter(f, x, y, w) class TestSamplerExtendArithmetic: @@ -453,7 +552,7 @@ class TestSamplerExtendArithmetic: @staticmethod def _sampler_with_stub_state(monkeypatch, ngen=7, npop=5): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), x, y, w) + sampler = Sampler(_StubModel(), _identity, x, y, w) sampler._state = SimpleNamespace(Ngen=ngen, Npop=npop) captured = {} dummy = SamplingResults( @@ -493,7 +592,7 @@ class TestSamplerPersistenceRoundTrip: @staticmethod def _sampler_with_state(): x, y, w = _xyw() - sampler = Sampler(_StubFitter(), x, y, w) + sampler = Sampler(_StubModel(), _identity, x, y, w) state = _make_state() _draw = state.draw() sampler._state = state @@ -558,7 +657,7 @@ def test_load_state_populates_results(self, tmp_path, caplog): sampler.save(prefix) x, y, w = _xyw() - fresh = Sampler(_StubFitter(), x, y, w) + fresh = Sampler(_StubModel(), _identity, x, y, w) with caplog.at_level(logging.WARNING, logger='easyscience.fitting'): results = fresh.load_state(prefix) @@ -576,7 +675,7 @@ def test_load_state_warns_on_different_data(self, tmp_path, caplog): sampler.save(prefix) x, y, w = _xyw() - other = Sampler(_StubFitter(), x, 2.0 * y, w) + other = Sampler(_StubModel(), _identity, x, 2.0 * y, w) with caplog.at_level(logging.WARNING, logger='easyscience.fitting'): other.load_state(prefix) diff --git a/tests/unit/global_object/test_entry_list_comprehensive.py b/tests/unit/global_object/test_entry_list_comprehensive.py deleted file mode 100644 index 7a15ba2f..00000000 --- a/tests/unit/global_object/test_entry_list_comprehensive.py +++ /dev/null @@ -1,320 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from unittest.mock import MagicMock - -from easyscience.global_object.map import _EntryList - - -class TestEntryListComprehensive: - """Comprehensive tests for _EntryList class""" - - def test_inheritance(self): - """Test that _EntryList properly inherits from list""" - entry = _EntryList([1, 2, 3]) - - # Should have list functionality - assert len(entry) == 3 - assert entry[0] == 1 - assert 2 in entry - - # Can append like a normal list - entry.append(4) - assert len(entry) == 4 - - def test_init_with_list_data(self): - """Test initialization with existing list data""" - data = ['a', 'b', 'c'] - entry = _EntryList(data, my_type='created') - - assert len(entry) == 3 - assert entry[0] == 'a' - assert 'created' in entry.type - - def test_known_types_validation(self): - """Test validation of known types""" - entry = _EntryList() - known_types = {'argument', 'created', 'created_internal', 'returned'} - - # Test all known types - for known_type in known_types: - entry.type = known_type - assert known_type in entry.type - - # Test unknown type - entry.type = 'unknown_type' - assert 'unknown_type' not in entry.type - - def test_finalizer_attribute(self): - """Test finalizer attribute handling""" - entry = _EntryList() - - # Initially None - assert entry.finalizer is None - - # Can set to any value - mock_finalizer = MagicMock() - entry.finalizer = mock_finalizer - assert entry.finalizer is mock_finalizer - - def test_delitem_override(self): - """Test __delitem__ override""" - entry = _EntryList(['a', 'b', 'c']) - - # Should work like normal list deletion - del entry[1] - assert len(entry) == 2 - assert entry == ['a', 'c'] - - def test_repr_variations(self): - """Test various __repr__ scenarios""" - # Empty type, no finalizer - entry = _EntryList() - repr_str = str(entry) - assert 'Undefined' in repr_str - assert 'Without' in repr_str - - # Single type, no finalizer - entry.type = 'created' - repr_str = str(entry) - assert 'created' in repr_str - assert 'Without' in repr_str - - # Multiple types, no finalizer - entry.type = 'argument' - repr_str = str(entry) - assert 'created' in repr_str - assert 'argument' in repr_str - assert 'Without' in repr_str - - # With finalizer - entry.finalizer = MagicMock() - repr_str = str(entry) - assert 'Without' not in repr_str - - def test_type_setter_edge_cases(self): - """Test edge cases for type setter""" - entry = _EntryList() - - # Setting None should not crash - entry.type = None - assert None not in entry.type - - # Setting empty string - entry.type = '' - assert '' not in entry.type - - # Setting numeric value - entry.type = 123 - assert 123 not in entry.type - - def test_remove_type_edge_cases(self): - """Test edge cases for remove_type""" - entry = _EntryList() - entry.type = 'created' - entry.type = 'argument' - - # Remove type that exists - entry.remove_type('created') - assert 'created' not in entry.type - assert 'argument' in entry.type - - # Remove type that doesn't exist - entry.remove_type('returned') # Not in list - assert 'argument' in entry.type # Should be unchanged - - # Remove unknown type - entry.remove_type('unknown') - assert 'argument' in entry.type # Should be unchanged - - # Remove None - entry.remove_type(None) - assert 'argument' in entry.type # Should be unchanged - - def test_reset_type_variations(self): - """Test reset_type with various inputs""" - entry = _EntryList() - entry.type = 'created' - entry.type = 'argument' - - # Reset with valid type - entry.reset_type('returned') - assert entry.type == ['returned'] - - # Reset with invalid type - entry.type = 'created' # Add something first - entry.reset_type('invalid') - assert entry.type == [] # Should be empty since invalid - - # Reset with None - entry.type = 'created' - entry.reset_type(None) - assert entry.type == [] - - def test_boolean_properties_combinations(self): - """Test boolean properties with multiple types""" - entry = _EntryList() - - # Add multiple types - entry.type = 'created' - entry.type = 'argument' - entry.type = 'returned' - - # All should be true - assert entry.is_created - assert entry.is_argument - assert entry.is_returned - assert not entry.is_created_internal # Not added - - # Remove one - entry.remove_type('argument') - assert entry.is_created - assert not entry.is_argument - assert entry.is_returned - - def test_list_functionality_preserved(self): - """Test that list functionality is preserved""" - entry = _EntryList() - - # Basic list operations - entry.append('a') - entry.append('b') - entry.insert(1, 'x') - assert entry == ['a', 'x', 'b'] - - # List methods - assert entry.index('x') == 1 - assert entry.count('a') == 1 - - # Slicing - subset = entry[1:] - assert subset == ['x', 'b'] - - # Iteration - items = [item for item in entry] - assert items == ['a', 'x', 'b'] - - def test_type_persistence_during_list_operations(self): - """Test that type information persists during list operations""" - entry = _EntryList(my_type='created') - entry.type = 'argument' - - # Perform list operations - entry.append('item1') - entry.extend(['item2', 'item3']) - entry.sort() - - # Type information should be preserved - assert entry.is_created - assert entry.is_argument - - def test_equality_with_regular_list(self): - """Test equality comparison with regular list""" - entry = _EntryList(['a', 'b', 'c']) - regular_list = ['a', 'b', 'c'] - - # Should be equal in content - assert entry == regular_list - assert regular_list == entry - - # But not the same type - assert type(entry) != type(regular_list) - assert isinstance(entry, _EntryList) - assert isinstance(entry, list) - - def test_copy_behavior(self): - """Test copying behavior""" - import copy - - entry = _EntryList(['a', 'b'], my_type='created') - entry.finalizer = MagicMock() - - # Shallow copy - shallow_copy = copy.copy(entry) - assert shallow_copy == entry - assert shallow_copy.type == entry.type - assert shallow_copy.finalizer is entry.finalizer - - # Deep copy - deep_copy = copy.deepcopy(entry) - assert deep_copy == entry - assert deep_copy.type == entry.type - # Finalizer might be different in deep copy - - def test_memory_efficiency(self): - """Test that _EntryList doesn't consume excessive memory""" - # Create many entries - entries = [] - for i in range(1000): - entry = _EntryList([f'item_{i}'], my_type='created') - entries.append(entry) - - # Should not raise memory errors - assert len(entries) == 1000 - assert all(entry.is_created for entry in entries) - - def test_thread_safety_simulation(self): - """Test basic thread safety aspects (simulation)""" - import threading - import time - - entry = _EntryList() - results = [] - - def add_type(type_name, delay=0.001): - time.sleep(delay) - entry.type = type_name - results.append(type_name) - - # Simulate concurrent access - threads = [] - for i, type_name in enumerate(['created', 'argument', 'returned']): - thread = threading.Thread(target=add_type, args=(type_name,)) - threads.append(thread) - - for thread in threads: - thread.start() - - for thread in threads: - thread.join() - - # All types should be added (order might vary) - assert len(set(entry.type)) == 3 - assert all(t in entry.type for t in ['created', 'argument', 'returned']) - - def test_pickle_serialization(self): - """Test pickle serialization support""" - import pickle - - entry = _EntryList(['a', 'b', 'c'], my_type='created') - entry.type = 'argument' - - # Serialize - pickled = pickle.dumps(entry) - - # Deserialize - unpickled = pickle.loads(pickled) - - # Should preserve data and types - assert unpickled == entry - assert unpickled.type == entry.type - assert unpickled.is_created - assert unpickled.is_argument - - def test_subclassing(self): - """Test that _EntryList can be subclassed""" - - class CustomEntryList(_EntryList): - def __init__(self, *args, custom_attr=None, **kwargs): - super().__init__(*args, **kwargs) - self.custom_attr = custom_attr - - def custom_method(self): - return f'Custom: {len(self)} items' - - custom = CustomEntryList(['x', 'y'], my_type='created', custom_attr='test') - - assert custom.custom_attr == 'test' - assert custom.custom_method() == 'Custom: 2 items' - assert custom.is_created - assert len(custom) == 2 diff --git a/tests/unit/global_object/test_global_object.py b/tests/unit/global_object/test_global_object.py index 719fee5a..e2cd5e66 100644 --- a/tests/unit/global_object/test_global_object.py +++ b/tests/unit/global_object/test_global_object.py @@ -16,8 +16,7 @@ def test_init(self): # Expect assert global_object.log == GlobalObject().log - assert global_object.map == GlobalObject().map - assert global_object.stack == GlobalObject().stack + assert global_object.registry == GlobalObject().registry assert global_object.debug == GlobalObject().debug def test_generate_unique_name(self): @@ -33,13 +32,13 @@ def test_generate_unique_name_already_taken(self): global_object = GlobalObject() # Block the other_name_prefix_2 name keep_due_toweakref_1 = DescriptorBool( - name='test', value=True, unique_name='other_name_prefix_2' + display_name='test', value=True, unique_name='other_name_prefix_2' ) keep_due_toweakref_2 = DescriptorBool( - name='test', value=True, unique_name='other_name_prefix_a_3' + display_name='test', value=True, unique_name='other_name_prefix_a_3' ) keep_due_toweakref_3 = DescriptorBool( - name='test', value=True, unique_name='almost_other_name_prefix_3' + display_name='test', value=True, unique_name='almost_other_name_prefix_3' ) # Then @@ -50,10 +49,10 @@ def test_generate_unique_name_already_taken(self): @pytest.fixture def clear_global_map(self): - """Clear global map before and after each test""" - global_object.map._clear() + """Clear global registry before and after each test""" + global_object.registry.clear() yield - global_object.map._clear() + global_object.registry.clear() def test_singleton_behavior(self): """Test that GlobalObject is truly a singleton""" @@ -64,23 +63,9 @@ def test_singleton_behavior(self): # Then assert obj1 is obj2 assert id(obj1) == id(obj2) - assert obj1.map is obj2.map + assert obj1.registry is obj2.registry assert obj1.log is obj2.log - def test_instantiate_stack(self, clear_global_map): - """Test stack instantiation""" - # Given - global_obj = GlobalObject() - - # When - global_obj.instantiate_stack() - - # Then - assert global_obj.stack is not None - from easyscience.global_object.undo_redo import UndoStack - - assert isinstance(global_obj.stack, UndoStack) - def test_debug_property(self): """Test debug property access""" # Given @@ -94,7 +79,7 @@ def test_debug_property(self): assert global_obj.debug is global_obj2.debug def test_generate_unique_name_empty_map(self, clear_global_map): - """Test unique name generation with empty map""" + """Test unique name generation with empty registry""" # Given global_obj = GlobalObject() @@ -109,9 +94,9 @@ def test_generate_unique_name_with_gaps(self, clear_global_map): # Given global_obj = GlobalObject() # Create objects with non-sequential names - keep1 = DescriptorBool(name='test', value=True, unique_name='prefix_0') - keep2 = DescriptorBool(name='test', value=True, unique_name='prefix_2') - keep3 = DescriptorBool(name='test', value=True, unique_name='prefix_5') + keep1 = DescriptorBool(display_name='test', value=True, unique_name='prefix_0') + keep2 = DescriptorBool(display_name='test', value=True, unique_name='prefix_2') + keep3 = DescriptorBool(display_name='test', value=True, unique_name='prefix_5') # When name = global_obj.generate_unique_name('prefix') @@ -123,8 +108,8 @@ def test_generate_unique_name_non_numeric_suffix(self, clear_global_map): """Test that non-numeric suffixes are ignored""" # Given global_obj = GlobalObject() - keep1 = DescriptorBool(name='test', value=True, unique_name='prefix_abc') - keep2 = DescriptorBool(name='test', value=True, unique_name='prefix_1') + keep1 = DescriptorBool(display_name='test', value=True, unique_name='prefix_abc') + keep2 = DescriptorBool(display_name='test', value=True, unique_name='prefix_1') # When name = global_obj.generate_unique_name('prefix') @@ -136,8 +121,8 @@ def test_generate_unique_name_similar_prefixes(self, clear_global_map): """Test that similar but different prefixes don't interfere""" # Given global_obj = GlobalObject() - keep1 = DescriptorBool(name='test', value=True, unique_name='test_param_5') - keep2 = DescriptorBool(name='test', value=True, unique_name='test_parameter_10') + keep1 = DescriptorBool(display_name='test', value=True, unique_name='test_param_5') + keep2 = DescriptorBool(display_name='test', value=True, unique_name='test_parameter_10') # When name1 = global_obj.generate_unique_name('test_param') @@ -153,25 +138,14 @@ def test_integration_with_parameter_creation(self, clear_global_map): global_obj = GlobalObject() # When - param1 = Parameter(name='test1', value=1.0) - param2 = Parameter(name='test2', value=2.0) + param1 = Parameter(display_name='test1', value=1.0) + param2 = Parameter(display_name='test2', value=2.0) # Then - assert len(global_obj.map.vertices()) == 2 - assert param1.unique_name in global_obj.map.vertices() - assert param2.unique_name in global_obj.map.vertices() + assert len(global_obj.registry) == 2 + assert param1.unique_name in global_obj.registry + assert param2.unique_name in global_obj.registry # Test retrieval - retrieved1 = global_obj.map.get_item_by_key(param1.unique_name) + retrieved1 = global_obj.registry[param1.unique_name] assert retrieved1 is param1 - - def test_script_manager_access(self): - """Test that script manager is accessible""" - # Given - global_obj = GlobalObject() - - # Then - assert hasattr(global_obj, 'script') - from easyscience.global_object.hugger.hugger import ScriptManager - - assert isinstance(global_obj.script, ScriptManager) diff --git a/tests/unit/global_object/test_integration_comprehensive.py b/tests/unit/global_object/test_integration_comprehensive.py index 61268954..f5a83801 100644 --- a/tests/unit/global_object/test_integration_comprehensive.py +++ b/tests/unit/global_object/test_integration_comprehensive.py @@ -2,30 +2,40 @@ # SPDX-License-Identifier: BSD-3-Clause import gc -from unittest.mock import patch import pytest +from easy_test_models import CoefficientModel -from easyscience import ObjBase +from easyscience import ModelBase from easyscience import Parameter from easyscience import global_object from easyscience.global_object.global_object import GlobalObject from easyscience.variable import DescriptorBool +class Container(ModelBase): + """ModelBase holding arbitrary named children.""" + + def __init__(self, display_name=None, **children): + super().__init__(display_name=display_name) + self._children = children + + def __getattr__(self, key): + children = self.__dict__.get('_children', {}) + if key in children: + return children[key] + raise AttributeError(key) + + class TestGlobalObjectIntegration: """Integration tests for GlobalObject ecosystem""" @pytest.fixture def clear_all(self): """Clear everything before and after each test""" - global_object.map._clear() - if global_object.stack: - global_object.stack.clear() + global_object.registry.clear() yield - global_object.map._clear() - if global_object.stack: - global_object.stack.clear() + global_object.registry.clear() def test_parameter_lifecycle_integration(self, clear_all): """Test complete parameter lifecycle with global object integration""" @@ -33,78 +43,58 @@ def test_parameter_lifecycle_integration(self, clear_all): global_obj = GlobalObject() # When - Create parameter - param = Parameter(name='test_param', value=10.0, unit='m') + param = Parameter(display_name='test_param', value=10.0, unit='m') - # Then - Should be registered in global map - assert global_obj.map.is_known(param) - assert param.unique_name in global_obj.map.vertices() - assert 'created' in global_obj.map.find_type(param) + # Then - Should be registered in global registry + assert param.unique_name in global_obj.registry - # When - Modify parameter with undo/redo enabled - if not global_obj.stack: - global_obj.instantiate_stack() - global_obj.stack.enabled = True - - original_value = param.value + # When - Modify parameter param.value = 20.0 - # Then - Should be able to undo/redo - assert param.value == 20.0 - assert global_obj.stack.canUndo() - - global_obj.stack.undo() - assert param.value == original_value - - global_obj.stack.redo() + # Then assert param.value == 20.0 + assert global_obj.registry[param.unique_name] is param # When - Delete parameter param_name = param.unique_name - original_count = len(global_obj.map.vertices()) + original_count = len(global_obj.registry) del param gc.collect() - # Then - Should be removed from global map (via weak references) + # Then - Should be removed from global registry (via weak references) # Note: garbage collection timing can vary, so we check count decrease # or explicit cleanup - final_count = len(global_obj.map.vertices()) - if param_name in global_obj.map.vertices(): + final_count = len(global_obj.registry) + if param_name in global_obj.registry: # If still present, manually clean up for test consistency - global_obj.map.prune(param_name) - assert param_name not in global_obj.map.vertices() + global_obj.registry.discard(param_name) + assert param_name not in global_obj.registry - def test_objbase_parameter_relationship(self, clear_all): - """Test ObjBase containing parameters integration""" + def test_model_parameter_relationship(self, clear_all): + """Test a model containing parameters integration""" # Given global_obj = GlobalObject() - param1 = Parameter(name='length', value=10.0, unit='m') - param2 = Parameter(name='width', value=5.0, unit='m') + param1 = Parameter(display_name='length', value=10.0, unit='m') + param2 = Parameter(display_name='width', value=5.0, unit='m') - # When - Create ObjBase with parameters - obj = ObjBase(name='rectangle', length=param1, width=param2) + # When - Create a container with parameters + obj = Container(display_name='rectangle', length=param1, width=param2) - # Then - All should be in global map - assert global_obj.map.is_known(obj) - assert global_obj.map.is_known(param1) - assert global_obj.map.is_known(param2) + # Then - All should be in global registry + assert obj.unique_name in global_obj.registry + assert param1.unique_name in global_obj.registry + assert param2.unique_name in global_obj.registry - # Check relationships in map - obj_edges = global_obj.map.get_edges(obj) - assert param1.unique_name in obj_edges - assert param2.unique_name in obj_edges - - # When - Modify through ObjBase - if not global_obj.stack: - global_obj.instantiate_stack() - global_obj.stack.enabled = True + # Check that the container holds the parameters + assert obj.length is param1 + assert obj.width is param2 + # When - Modify through the container obj.length.value = 15.0 - # Then - Should be able to undo - assert global_obj.stack.canUndo() - global_obj.stack.undo() - assert obj.length.value == 10.0 + # Then - The change is seen through the shared parameter + assert param1.value == 15.0 def test_unique_name_generation_integration(self, clear_all): """Test unique name generation across different object types""" @@ -114,7 +104,7 @@ def test_unique_name_generation_integration(self, clear_all): # When - Create multiple objects of same type params = [] for i in range(5): - param = Parameter(name=f'param_{i}', value=float(i)) + param = Parameter(display_name=f'param_{i}', value=float(i)) params.append(param) # Then - Should have unique names @@ -126,174 +116,48 @@ def test_unique_name_generation_integration(self, clear_all): assert name == f'Parameter_{i}' # When - Create mixed object types - obj = ObjBase(name='test_obj') - desc = DescriptorBool(name='test_desc', value=True) + obj = Container(display_name='test_obj') + desc = DescriptorBool(display_name='test_desc', value=True) # Then - Should not interfere with each other's naming - assert obj.unique_name == 'ObjBase_0' + assert obj.unique_name == 'Container_0' assert desc.unique_name == 'DescriptorBool_0' - def test_map_vertex_type_management(self, clear_all): - """Test comprehensive vertex type management""" - # Given - global_obj = GlobalObject() - param = Parameter(name='test', value=1.0) - - # When - Check initial type - initial_types = global_obj.map.find_type(param) - assert 'created' in initial_types - - # When - Change type - global_obj.map.change_type(param, 'argument') - updated_types = global_obj.map.find_type(param) - assert 'created' in updated_types - assert 'argument' in updated_types - - # When - Reset type - global_obj.map.reset_type(param, 'returned') - final_types = global_obj.map.find_type(param) - assert final_types == ['returned'] - - # When - Filter by type - returned_objs = global_obj.map.returned_objs - assert param.unique_name in returned_objs - - created_objs = global_obj.map.created_objs - assert param.unique_name not in created_objs - @pytest.mark.skip(reason='Weak reference timing can be inconsistent in tests') def test_weak_reference_cleanup_integration(self, clear_all): - """Test that weak references properly clean up the map""" + """Test that weak references properly clean up the registry""" # Given global_obj = GlobalObject() # When - Create objects - param1 = Parameter(name='temp1', value=1.0) - param2 = Parameter(name='temp2', value=2.0) - obj = ObjBase(name='temp_obj', param1=param1, param2=param2) + param1 = Parameter(display_name='temp1', value=1.0) + param2 = Parameter(display_name='temp2', value=2.0) + obj = Container(display_name='temp_obj', param1=param1, param2=param2) param1_name = param1.unique_name param2_name = param2.unique_name obj_name = obj.unique_name # Then - All should be present - assert global_obj.map.is_known(param1) - assert global_obj.map.is_known(param2) - assert global_obj.map.is_known(obj) + assert param1_name in global_obj.registry + assert param2_name in global_obj.registry + assert obj_name in global_obj.registry # When - Delete objects del param1 gc.collect() # Then - param1 should be removed - assert param1_name not in global_obj.map.vertices() - assert param2_name in global_obj.map.vertices() - assert obj_name in global_obj.map.vertices() + assert param1_name not in global_obj.registry + assert param2_name in global_obj.registry + assert obj_name in global_obj.registry # When - Delete remaining objects del param2, obj gc.collect() - # Then - Map should be empty - assert len(global_obj.map.vertices()) == 0 - - def test_complex_undo_redo_scenario(self, clear_all): - """Test complex undo/redo scenario with multiple objects""" - # Given - global_obj = GlobalObject() - if not global_obj.stack: - global_obj.instantiate_stack() - global_obj.stack.enabled = True - - # Create a complex object structure - length = Parameter(name='length', value=10.0, unit='m') - width = Parameter(name='width', value=5.0, unit='m') - height = Parameter(name='height', value=3.0, unit='m') - - box = ObjBase(name='box', length=length, width=width, height=height) - - # When - Perform multiple operations in a macro - global_obj.stack.beginMacro('Resize box') - - length.value = 15.0 - width.value = 7.0 - height.value = 4.0 - - global_obj.stack.endMacro() - - # Then - All changes should be applied - assert box.length.value == 15.0 - assert box.width.value == 7.0 - assert box.height.value == 4.0 - - # When - Undo macro - assert global_obj.stack.canUndo() - assert global_obj.stack.undoText() == 'Resize box' - - global_obj.stack.undo() - - # Then - All changes should be reverted - assert box.length.value == 10.0 - assert box.width.value == 5.0 - assert box.height.value == 3.0 - - # When - Redo macro - assert global_obj.stack.canRedo() - global_obj.stack.redo() - - # Then - All changes should be reapplied - assert box.length.value == 15.0 - assert box.width.value == 7.0 - assert box.height.value == 4.0 - - def test_map_path_finding_integration(self, clear_all): - """Test map path finding with real object relationships""" - # Given - global_obj = GlobalObject() - - # Create a hierarchy: container -> sub_container -> parameter - param = Parameter(name='value', value=42.0) - sub_container = ObjBase(name='sub', value=param) - main_container = ObjBase(name='main', sub=sub_container) - - # When - Find path from main to parameter - path = global_obj.map.find_path(main_container.unique_name, param.unique_name) - - # Then - Should find the path through the hierarchy - assert len(path) >= 2 - assert path[0] == main_container.unique_name - assert path[-1] == param.unique_name - - # When - Find reverse path - reverse_path = global_obj.map.reverse_route(param.unique_name, main_container.unique_name) - - # Then - Should be the reverse - assert reverse_path[0] == param.unique_name - assert reverse_path[-1] == main_container.unique_name - - def test_map_connectivity_with_isolated_objects(self, clear_all): - """Test map connectivity detection""" - # Given - global_obj = GlobalObject() - - # When - Create connected objects - param1 = Parameter(name='connected1', value=1.0) - param2 = Parameter(name='connected2', value=2.0) - container = ObjBase(name='container', p1=param1, p2=param2) - - # Then - Map should be connected - # TODO: Depending on implementation, connectivity might vary - # assert global_obj.map.is_connected() - - # When - Create isolated object - isolated = Parameter(name='isolated', value=99.0) - # Remove its automatic connection by clearing edges - # (In real usage, isolated objects would be rare) - - # Then - Map might still be connected depending on implementation - # This tests the connectivity algorithm - connectivity = global_obj.map.is_connected() - assert isinstance(connectivity, bool) + # Then - registry should be empty + assert len(global_obj.registry) == 0 def test_error_handling_integration(self, clear_all): """Test error handling across the global object system""" @@ -301,17 +165,17 @@ def test_error_handling_integration(self, clear_all): global_obj = GlobalObject() # When - Try to get non-existent object - with pytest.raises(ValueError, match='Item not in map'): - global_obj.map.get_item_by_key('non_existent') + with pytest.raises(KeyError): + global_obj.registry['non_existent'] # When - Try to add object with duplicate name - param1 = Parameter(name='test', value=1.0) + param1 = Parameter(display_name='test', value=1.0) param1_name = param1.unique_name - # Create another with same unique name (should fail in add_vertex) + # Create another with same unique name (should fail in add) with pytest.raises(ValueError, match='already exists'): # This simulates what would happen if we tried to add duplicate - global_obj.map.add_vertex(param1) # param1 already added during creation + global_obj.registry.add(param1) # param1 already added during creation def test_memory_pressure_simulation(self, clear_all): """Test system behavior under memory pressure""" @@ -321,11 +185,11 @@ def test_memory_pressure_simulation(self, clear_all): # When - Create many objects objects = [] for i in range(100): - param = Parameter(name=f'param_{i}', value=float(i)) - obj = ObjBase(name=f'obj_{i}', param=param) + param = Parameter(display_name=f'param_{i}', value=float(i)) + obj = Container(display_name=f'obj_{i}', param=param) objects.append((param, obj)) - initial_vertex_count = len(global_obj.map.vertices()) + initial_vertex_count = len(global_obj.registry) assert initial_vertex_count == 200 # 100 params + 100 objs # When - Delete half the objects @@ -337,16 +201,16 @@ def test_memory_pressure_simulation(self, clear_all): # Force garbage collection gc.collect() - # Then - Map should have fewer vertices - current_vertex_count = len(global_obj.map.vertices()) + # Then - registry should have fewer vertices + current_vertex_count = len(global_obj.registry) assert current_vertex_count <= initial_vertex_count # When - Delete all remaining objects del objects gc.collect() - # Then - Map should be mostly empty (might have some remaining references) - final_vertex_count = len(global_obj.map.vertices()) + # Then - registry should be mostly empty (might have some remaining references) + final_vertex_count = len(global_obj.registry) assert final_vertex_count < initial_vertex_count def test_serialization_integration_with_global_state(self, clear_all): @@ -355,64 +219,34 @@ def test_serialization_integration_with_global_state(self, clear_all): global_obj = GlobalObject() # Create objects - param = Parameter(name='test_param', value=123.45, unit='kg') - obj = ObjBase(name='test_obj', param=param) + param = Parameter(display_name='test_param', value=123.45, unit='kg') + obj = CoefficientModel(display_name='test_obj', coefficients=[param]) - original_vertex_count = len(global_obj.map.vertices()) + original_vertex_count = len(global_obj.registry) # When - Serialize objects - param_dict = param.as_dict() - obj_dict = obj.as_dict() + param_dict = param.to_dict() + obj_dict = obj.to_dict() # Clear global state - global_obj.map._clear() - assert len(global_obj.map.vertices()) == 0 + global_obj.registry.clear() + assert len(global_obj.registry) == 0 # When - Deserialize objects new_param = Parameter.from_dict(param_dict) - new_obj = ObjBase.from_dict(obj_dict) + new_obj = CoefficientModel.from_dict(obj_dict) - # Then - Should be registered in global map again - assert len(global_obj.map.vertices()) >= 2 - assert global_obj.map.is_known(new_param) - assert global_obj.map.is_known(new_obj) + # Then - Should be registered in global registry again + assert len(global_obj.registry) >= 2 + assert new_param.unique_name in global_obj.registry + assert new_obj.unique_name in global_obj.registry # Objects should be functionally equivalent - assert new_param.name == param.name + assert new_param.display_name == param.display_name assert new_param.value == param.value assert new_param.unit == param.unit - - def test_debug_mode_integration(self, clear_all): - """Test debug mode behavior across the system""" - # Given - global_obj = GlobalObject() - original_debug = global_obj.debug - - try: - # When - Enable debug mode - global_obj.debug = True - - if not global_obj.stack: - global_obj.instantiate_stack() - global_obj.stack.enabled = True - - # Create and modify objects - param = Parameter(name='debug_test', value=1.0) - - # This should trigger debug output in property_stack decorator - with patch('builtins.print') as mock_print: - param.value = 2.0 - - # Should have printed debug info (if debug mode works) - # Note: This depends on the debug implementation details - - # Test that operations still work normally - assert param.value == 2.0 - assert global_obj.stack.canUndo() - - finally: - # Restore original debug state - global_obj.debug = original_debug + assert new_obj.display_name == 'test_obj' + assert new_obj.coefficients[0].value == param.value def test_concurrent_access_simulation(self, clear_all): """Simulate concurrent access patterns""" @@ -423,12 +257,15 @@ def test_concurrent_access_simulation(self, clear_all): global_obj = GlobalObject() results = [] errors = [] + # The registry holds weak references, so keep the objects alive + params = [] def create_objects(thread_id, count=10): """Create objects in a thread""" try: for i in range(count): - param = Parameter(name=f'thread_{thread_id}_param_{i}', value=float(i)) + param = Parameter(display_name=f'thread_{thread_id}_param_{i}', value=float(i)) + params.append(param) results.append(param.unique_name) time.sleep(0.001) # Small delay to encourage race conditions except Exception as e: @@ -451,6 +288,6 @@ def create_objects(thread_id, count=10): assert len(results) == 15 # 3 threads * 5 objects each assert len(set(results)) == 15 # All unique names should be unique - # All objects should be in the map + # All objects should be in the registry for unique_name in results: - assert unique_name in global_obj.map.vertices() + assert unique_name in global_obj.registry diff --git a/tests/unit/global_object/test_map.py b/tests/unit/global_object/test_map.py deleted file mode 100644 index c68aa96f..00000000 --- a/tests/unit/global_object/test_map.py +++ /dev/null @@ -1,757 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import gc -from unittest.mock import MagicMock - -import pytest - -from easyscience import ObjBase -from easyscience import Parameter -from easyscience import global_object -from easyscience.global_object.map import Map -from easyscience.global_object.map import _EntryList - - -class TestEntryList: - """Test the _EntryList helper class""" - - def test_init_default(self): - """Test _EntryList initialization with defaults""" - entry = _EntryList() - assert entry._type == [] - assert entry.finalizer is None - - def test_init_with_known_type(self): - """Test _EntryList initialization with known type""" - entry = _EntryList(my_type='created') - assert 'created' in entry._type - - def test_init_with_unknown_type(self): - """Test _EntryList initialization with unknown type""" - entry = _EntryList(my_type='unknown') - assert entry._type == [] - - def test_repr(self): - """Test string representation""" - entry = _EntryList() - repr_str = str(entry) - assert 'Undefined' in repr_str - assert 'finalizer' in repr_str - - entry.type = 'created' - repr_str = str(entry) - assert 'created' in repr_str - assert 'finalizer' in repr_str - - def test_type_property_getter(self): - """Test type property getter""" - entry = _EntryList() - assert entry.type == [] - - def test_type_property_setter_valid(self): - """Test type property setter with valid types""" - entry = _EntryList() - entry.type = 'created' - assert 'created' in entry.type - - entry.type = 'argument' - assert 'argument' in entry.type - assert 'created' in entry.type # Should be additive - - def test_type_property_setter_invalid(self): - """Test type property setter with invalid type""" - entry = _EntryList() - entry.type = 'invalid' - assert entry.type == [] - - def test_type_property_setter_duplicate(self): - """Test type property setter doesn't add duplicates""" - entry = _EntryList() - entry.type = 'created' - entry.type = 'created' # Try to add again - assert entry.type.count('created') == 1 - - def test_remove_type(self): - """Test removing a type""" - entry = _EntryList() - entry.type = 'created' - entry.type = 'argument' - - entry.remove_type('created') - assert 'created' not in entry.type - assert 'argument' in entry.type - - def test_remove_type_not_present(self): - """Test removing a type that's not present""" - entry = _EntryList() - entry.type = 'created' - entry.remove_type('argument') # Not present - assert 'created' in entry.type - - def test_reset_type(self): - """Test resetting types""" - entry = _EntryList() - entry.type = 'created' - entry.type = 'argument' - - entry.reset_type('returned') - assert entry.type == ['returned'] - - def test_reset_type_default_none(self): - """Test resetting types with no default""" - entry = _EntryList() - entry.type = 'created' - - entry.reset_type() - assert entry.type == [] - - def test_boolean_properties(self): - """Test boolean property helpers""" - entry = _EntryList() - - # Test all false initially - assert not entry.is_argument - assert not entry.is_created - assert not entry.is_created_internal - assert not entry.is_returned - - # Test each property - entry.type = 'argument' - assert entry.is_argument - - entry.type = 'created' - assert entry.is_created - - entry.type = 'created_internal' - assert entry.is_created_internal - - entry.type = 'returned' - assert entry.is_returned - - -class TestMap: - @pytest.fixture - def clear(self): - global_object.map._clear() - - @pytest.fixture - def base_object(self): - return ObjBase(name='test') - - @pytest.fixture - def parameter_object(self): - return Parameter(name='test2', value=2) - - def test_add_vertex(self, clear, base_object, parameter_object): - # When Then Expect - assert len(global_object.map._store) == 2 - assert len(global_object.map._Map__type_dict) == 2 - - def test_clear(self, clear, base_object): - # When - assert len(global_object.map._store) == 1 - assert len(global_object.map._Map__type_dict) == 1 - # Then - global_object.map._clear() - # Expect - assert len(global_object.map._store) == 0 - assert global_object.map._Map__type_dict == {} - - def test_weakref(self, clear): - # When - test_obj = ObjBase(name='test') - assert len(global_object.map._store) == 1 - assert len(global_object.map._Map__type_dict) == 1 - # Then - del test_obj - gc.collect() - # Expect - assert len(global_object.map._store) == 0 - assert len(global_object.map._Map__type_dict) == 0 - - def test_vertices(self, clear, base_object, parameter_object): - # When Then Expect - assert global_object.map.vertices() == [ - base_object.unique_name, - parameter_object.unique_name, - ] - - def test_get_item_by_key(self, clear, base_object, parameter_object): - # When Then Expect - assert global_object.map.get_item_by_key(base_object.unique_name) == base_object - assert global_object.map.get_item_by_key(parameter_object.unique_name) == parameter_object - - @pytest.mark.parametrize('cls, kwargs', [(ObjBase, {}), (Parameter, {'value': 2.0})]) - def test_identical_unique_names_exception(self, clear, cls, kwargs): - # When - test_obj = cls(name='test', unique_name='test', **kwargs) - # Then Expect - with pytest.raises(ValueError): - test_obj2 = cls(name='test2', unique_name='test', **kwargs) - - def test_unique_name_change_still_in_map(self, clear, base_object, parameter_object): - # When - assert global_object.map.get_item_by_key('ObjBase_0') == base_object - assert global_object.map.get_item_by_key('Parameter_0') == parameter_object - # Then - base_object.unique_name = 'test3' - parameter_object.unique_name = 'test4' - # Expect - assert global_object.map.get_item_by_key('ObjBase_0') == base_object - assert global_object.map.get_item_by_key('Parameter_0') == parameter_object - assert global_object.map.get_item_by_key('test3') == base_object - assert global_object.map.get_item_by_key('test4') == parameter_object - - def test_add_vertex_duplicate_name_error(self, clear, base_object): - """Test that adding vertex with duplicate name raises error""" - # When/Then - with pytest.raises(ValueError, match='already exists'): - # Try to add another object with same unique_name - duplicate_obj = ObjBase(name='duplicate', unique_name=base_object.unique_name) - - def test_add_vertex_with_object_type(self, clear): - """Test adding vertex with specific object type""" - # Given - obj = ObjBase(name='test') - - # When - Object is automatically added during construction - # Then - assert global_object.map.is_known(obj) - assert 'created' in global_object.map.find_type(obj) - - def test_is_known_object(self, clear, base_object): - """Test is_known method""" - # When/Then - assert global_object.map.is_known(base_object) is True - - # Test with unknown object - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - assert global_object.map.is_known(unknown_obj) is False - - def test_find_type_known_object(self, clear, base_object): - """Test find_type method""" - # When/Then - types = global_object.map.find_type(base_object) - assert isinstance(types, list) - assert 'created' in types - - def test_find_type_unknown_object(self, clear): - """Test find_type with unknown object""" - # Given - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - - # When/Then - result = global_object.map.find_type(unknown_obj) - assert result is None - - def test_returned_objs_access_safe_under_modification(self, clear): - """Ensure accessing returned_objs doesn't raise when entries change size during iteration.""" - objs = [ObjBase(name=f'race_{i}') for i in range(8)] - # Mark all as returned - for o in objs: - global_object.map.change_type(o, 'returned') - - # Repeatedly access returned_objs while deleting objects and forcing GC to - # try to trigger concurrent modification. This used to raise RuntimeError. - for _ in range(200): - _ = global_object.map.returned_objs # should not raise - if _ and objs: - # delete one object and collect to trigger finalizer/prune - del objs[0] - gc.collect() - # If we got here without exceptions, consider the access safe - assert True - - def test_reset_type(self, clear, base_object): - """Test resetting object type""" - # Given - original_types = global_object.map.find_type(base_object) - - # When - global_object.map.reset_type(base_object, 'argument') - - # Then - new_types = global_object.map.find_type(base_object) - assert new_types == ['argument'] - assert new_types != original_types - - def test_change_type(self, clear, base_object): - """Test changing object type""" - # Given - original_types = global_object.map.find_type(base_object) - original_count = len(original_types) - - # When - global_object.map.change_type(base_object, 'argument') - - # Then - new_types = global_object.map.find_type(base_object) - assert 'argument' in new_types - assert len(new_types) >= original_count # Should be at least the same or more - - def test_add_edge(self, clear, base_object, parameter_object): - """Test adding edges between objects""" - # When - global_object.map.add_edge(base_object, parameter_object) - - # Then - edges = global_object.map.get_edges(base_object) - assert parameter_object.unique_name in edges - - def test_add_edge_unknown_start_object(self, clear, parameter_object): - """Test adding edge with unknown start object""" - # Given - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - - # When/Then - with pytest.raises(AttributeError, match='Start object not in map'): - global_object.map.add_edge(unknown_obj, parameter_object) - - def test_get_edges(self, clear, base_object, parameter_object): - """Test getting edges for an object""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - edges = global_object.map.get_edges(base_object) - - # Then - assert isinstance(edges, list) - assert parameter_object.unique_name in edges - - def test_get_edges_unknown_object(self, clear): - """Test getting edges for unknown object""" - # Given - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - - # When/Then - with pytest.raises(AttributeError): - global_object.map.get_edges(unknown_obj) - - def test_prune_vertex_from_edge(self, clear, base_object, parameter_object): - """Test removing edge between objects""" - # Given - global_object.map.add_edge(base_object, parameter_object) - assert parameter_object.unique_name in global_object.map.get_edges(base_object) - - # When - global_object.map.prune_vertex_from_edge(base_object, parameter_object) - - # Then - edges = global_object.map.get_edges(base_object) - assert parameter_object.unique_name not in edges - - def test_prune_vertex_from_edge_none_child(self, clear, base_object): - """Test pruning edge with None child""" - # When/Then - Should not raise error - global_object.map.prune_vertex_from_edge(base_object, None) - - def test_prune_vertex(self, clear, base_object): - """Test pruning a vertex completely""" - # Given - unique_name = base_object.unique_name - assert global_object.map.is_known(base_object) - - # When - global_object.map.prune(unique_name) - - # Then - assert not global_object.map.is_known(base_object) - assert unique_name not in global_object.map.vertices() - - def test_edges_generation(self, clear, base_object, parameter_object): - """Test edge generation""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - edges = global_object.map.edges() - - # Then - assert isinstance(edges, list) - expected_edge = {base_object.unique_name, parameter_object.unique_name} - assert expected_edge in edges - - def test_type_filtering_properties(self, clear): - """Test type filtering properties""" - # Given - obj1 = ObjBase(name='obj1') # 'created' type - obj2 = Parameter(name='obj2', value=1) # 'created' type - - global_object.map.change_type(obj1, 'argument') - global_object.map.change_type(obj2, 'returned') - - # When/Then - argument_objs = global_object.map.argument_objs - created_objs = global_object.map.created_objs - returned_objs = global_object.map.returned_objs - - assert obj1.unique_name in argument_objs - assert obj1.unique_name in created_objs # Should still be there - assert obj2.unique_name in created_objs - assert obj2.unique_name in returned_objs - - def test_find_path_simple(self, clear, base_object, parameter_object): - """Test finding path between objects""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - path = global_object.map.find_path(base_object.unique_name, parameter_object.unique_name) - - # Then - assert path == [base_object.unique_name, parameter_object.unique_name] - - def test_find_path_same_vertex(self, clear, base_object): - """Test finding path to same vertex""" - # When - path = global_object.map.find_path(base_object.unique_name, base_object.unique_name) - - # Then - assert path == [base_object.unique_name] - - def test_find_path_no_path(self, clear, base_object, parameter_object): - """Test finding path when no path exists""" - # When - No edge added - path = global_object.map.find_path(base_object.unique_name, parameter_object.unique_name) - - # Then - assert path == [] - - def test_find_all_paths(self, clear, base_object, parameter_object): - """Test finding all paths between objects""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - paths = global_object.map.find_all_paths( - base_object.unique_name, parameter_object.unique_name - ) - - # Then - assert len(paths) == 1 - assert paths[0] == [base_object.unique_name, parameter_object.unique_name] - - def test_reverse_route_with_start(self, clear, base_object, parameter_object): - """Test reverse route with specified start""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - route = global_object.map.reverse_route( - parameter_object.unique_name, base_object.unique_name - ) - - # Then - assert route == [parameter_object.unique_name, base_object.unique_name] - - def test_reverse_route_without_start(self, clear, base_object, parameter_object): - """Test reverse route without specified start""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - route = global_object.map.reverse_route(parameter_object.unique_name) - - # Then - assert len(route) >= 1 - assert route[0] == parameter_object.unique_name - - def test_is_connected_single_vertex(self, clear, base_object): - """Test connectivity with single vertex""" - # When/Then - assert global_object.map.is_connected() is True - - def test_is_connected_multiple_vertices(self, clear, base_object, parameter_object): - """Test connectivity with multiple connected vertices""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When/Then - assert global_object.map.is_connected() is True - - def test_map_repr(self, clear, base_object, parameter_object): - """Test map string representation""" - # When - repr_str = str(global_object.map) - - # Then - assert 'Map object' in repr_str - assert '2' in repr_str # Should show vertex count - - def test_get_item_by_key_not_found(self, clear): - """Test getting item by non-existent key""" - # When/Then - with pytest.raises(ValueError, match='Item not in map'): - global_object.map.get_item_by_key('non_existent') - - def test_clear_with_finalizers(self, clear): - """Test clearing map properly calls finalizers""" - # Given - obj = ObjBase(name='test') - original_count = len(global_object.map.vertices()) - - # When - global_object.map._clear() - - # Then - assert len(global_object.map.vertices()) == 0 - - def test_map_initialization(self): - """Test Map initialization""" - # When - test_map = Map() - - # Then - assert len(test_map.vertices()) == 0 - assert test_map.edges() == [] - - def test_vertices_retry_on_runtime_error(self, clear): - """Test that vertices() retries when RuntimeError occurs during iteration. - - This tests the thread-safety fix for WeakValueDictionary modification - during iteration (e.g., by garbage collection). - """ - # Given - test_map = Map() - - # Create a mock _store that raises RuntimeError on first iteration attempt - call_count = 0 - original_store = test_map._store - - class MockWeakValueDict: - def __init__(self): - self.data = {} - self.iteration_count = 0 - - def __iter__(self): - self.iteration_count += 1 - if self.iteration_count == 1: - # First iteration raises RuntimeError (simulates GC interference) - raise RuntimeError('dictionary changed size during iteration') - # Subsequent iterations succeed - return iter(self.data) - - def __len__(self): - return len(self.data) - - mock_store = MockWeakValueDict() - test_map._store = mock_store - - # When - vertices = test_map.vertices() - - # Then - assert vertices == [] - assert mock_store.iteration_count == 2 # Should have retried once - - def test_add_vertex_cleans_stale_type_dict_entry(self, clear): - """Test that add_vertex cleans up stale __type_dict entries. - - This can happen when a weak reference was collected but the finalizer - hasn't run yet, and a new object is created with the same unique_name. - """ - # Given - test_map = Map() - - # Manually add a stale entry to __type_dict (simulating GC collected but finalizer not run) - stale_name = 'StaleObject_0' - test_map._Map__type_dict[stale_name] = _EntryList() - - # Create a mock object with the same unique_name - mock_obj = MagicMock() - mock_obj.unique_name = stale_name - - # When - Adding the object should clean up the stale entry first - test_map.add_vertex(mock_obj, 'created') - - # Then - Object should be added successfully - assert stale_name in test_map._store - assert stale_name in test_map._Map__type_dict - assert test_map._Map__type_dict[stale_name].type == ['created'] - - def test_prune_key_not_in_store(self, clear): - """Test that prune handles case when key is not in _store. - - This defensive check prevents KeyError when the weak reference has - already been garbage collected but __type_dict entry remains. - """ - # Given - test_map = Map() - - # Manually add entry to __type_dict without corresponding _store entry - orphan_key = 'OrphanObject_0' - test_map._Map__type_dict[orphan_key] = _EntryList() - - # When - Pruning should not raise error - test_map.prune(orphan_key) - - # Then - Entry should be removed from __type_dict - assert orphan_key not in test_map._Map__type_dict - - def test_prune_key_in_both_dicts(self, clear, base_object): - """Test that prune removes key from both _store and __type_dict.""" - # Given - unique_name = base_object.unique_name - assert unique_name in global_object.map._store - assert unique_name in global_object.map._Map__type_dict - - # When - global_object.map.prune(unique_name) - - # Then - assert unique_name not in global_object.map._Map__type_dict - # Note: _store entry may or may not exist depending on weak ref state - - def test_prune_nonexistent_key(self, clear): - """Test that prune handles nonexistent key gracefully.""" - # When/Then - Should not raise error - global_object.map.prune('nonexistent_key') - - def test_reset_type_unknown_object(self, clear): - """Test reset_type with object not in map.""" - # Given - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - - # When/Then - Should not raise error - global_object.map.reset_type(unknown_obj, 'argument') - - def test_change_type_unknown_object(self, clear): - """Test change_type with object not in map.""" - # Given - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - - # When/Then - Should not raise error - global_object.map.change_type(unknown_obj, 'argument') - - def test_find_path_start_not_in_graph(self, clear): - """Test find_path when start vertex is not in graph.""" - # When - path = global_object.map.find_path('nonexistent', 'also_nonexistent') - - # Then - assert path == [] - - def test_find_all_paths_start_not_in_graph(self, clear): - """Test find_all_paths when start vertex is not in graph.""" - # When - paths = global_object.map.find_all_paths('nonexistent', 'also_nonexistent') - - # Then - assert paths == [] - - def test_find_isolated_vertices(self, clear, base_object, parameter_object): - """Test finding isolated vertices (vertices with no outgoing edges).""" - # Given - No edges added, both objects are isolated - - # When - isolated = global_object.map.find_isolated_vertices() - - # Then - assert base_object.unique_name in isolated - assert parameter_object.unique_name in isolated - - def test_find_isolated_vertices_with_edges(self, clear, base_object, parameter_object): - """Test finding isolated vertices when some have edges.""" - # Given - global_object.map.add_edge(base_object, parameter_object) - - # When - isolated = global_object.map.find_isolated_vertices() - - # Then - # base_object has an edge, so it's not isolated - assert base_object.unique_name not in isolated - # parameter_object has no outgoing edges, so it's isolated - assert parameter_object.unique_name in isolated - - def test_prune_vertex_from_edge_edge_not_exists(self, clear, base_object, parameter_object): - """Test pruning edge that doesn't exist.""" - # Given - No edge added between objects - - # When/Then - Should not raise error - global_object.map.prune_vertex_from_edge(base_object, parameter_object) - - def test_prune_vertex_from_edge_parent_not_in_map(self, clear, parameter_object): - """Test pruning edge when parent is not in map.""" - # Given - unknown_obj = MagicMock() - unknown_obj.unique_name = 'unknown' - - # When/Then - Should not raise error (vertex1 not in type_dict) - global_object.map.prune_vertex_from_edge(unknown_obj, parameter_object) - - def test_created_internal_property(self, clear): - """Test created_internal property.""" - # Given - obj = ObjBase(name='internal_obj') - global_object.map.change_type(obj, 'created_internal') - - # When - internal_objs = global_object.map.created_internal - - # Then - assert obj.unique_name in internal_objs - - def test_clear_empties_both_dicts(self, clear, base_object, parameter_object): - """Test that _clear() properly empties both _store and __type_dict.""" - # Given - assert len(global_object.map._store) == 2 - assert len(global_object.map._Map__type_dict) == 2 - - # When - global_object.map._clear() - - # Then - assert len(global_object.map._store) == 0 - assert len(global_object.map._Map__type_dict) == 0 - - def test_entry_list_delitem(self): - """Test _EntryList __delitem__ method.""" - # Given - entry = _EntryList() - entry.append('item1') - entry.append('item2') - entry.append('item3') - - # When - del entry[1] - - # Then - assert len(entry) == 2 - assert 'item2' not in entry - assert 'item1' in entry - assert 'item3' in entry - - def test_entry_list_repr_with_finalizer(self): - """Test _EntryList repr when finalizer is set.""" - # Given - entry = _EntryList() - entry.type = 'created' - entry.finalizer = MagicMock() # Non-None finalizer - - # When - repr_str = str(entry) - - # Then - assert 'created' in repr_str - assert 'With a finalizer' in repr_str - - def test_entry_list_remove_type_unknown(self): - """Test removing a type that's not in known types.""" - # Given - entry = _EntryList() - entry.type = 'created' - - # When - Try to remove unknown type - entry.remove_type('unknown_type') - - # Then - Should not change anything - assert 'created' in entry.type diff --git a/tests/unit/global_object/test_object_registry.py b/tests/unit/global_object/test_object_registry.py new file mode 100644 index 00000000..ccf60d3e --- /dev/null +++ b/tests/unit/global_object/test_object_registry.py @@ -0,0 +1,262 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import gc + +import pytest + +from easyscience import Parameter +from easyscience import global_object +from easyscience.base_classes import EasyBase +from easyscience.global_object.object_registry import ObjectRegistry + + +class _Named: + """Minimal weak-referenceable object that can be registered in an ObjectRegistry.""" + + def __init__(self, unique_name): + self.unique_name = unique_name + + +class TestObjectRegistry: + @pytest.fixture + def clear(self): + global_object.registry.clear() + + @pytest.fixture + def base_object(self): + return EasyBase(display_name='test') + + @pytest.fixture + def parameter_object(self): + return Parameter(display_name='test2', value=2) + + def test_add(self, clear, base_object, parameter_object): + # When Then Expect + assert global_object.registry.objects() == [base_object, parameter_object] + + def test_clear(self, clear, base_object): + # When + assert global_object.registry.names() == [base_object.unique_name] + # Then + global_object.registry.clear() + # Expect + assert global_object.registry.names() == [] + assert global_object.registry.objects() == [] + + def test_weakref(self, clear): + # When + test_obj = EasyBase(display_name='test') + assert global_object.registry.names() == [test_obj.unique_name] + # Then + del test_obj + gc.collect() + # Expect + assert global_object.registry.names() == [] + + def test_names(self, clear, base_object, parameter_object): + # When Then Expect + assert global_object.registry.names() == [ + base_object.unique_name, + parameter_object.unique_name, + ] + + def test_getitem(self, clear, base_object, parameter_object): + # When Then Expect + assert global_object.registry[base_object.unique_name] == base_object + assert global_object.registry[parameter_object.unique_name] == parameter_object + + def test_getitem_not_found(self, clear): + """Test getting item by non-existent key""" + # When/Then + with pytest.raises(KeyError): + global_object.registry['non_existent'] + + def test_contains_len_iter(self, clear, base_object, parameter_object): + # When Then Expect + assert base_object.unique_name in global_object.registry + assert 'non_existent' not in global_object.registry + assert len(global_object.registry) == 2 + assert list(global_object.registry) == [ + base_object.unique_name, + parameter_object.unique_name, + ] + + def test_iter_survives_collection_mid_loop(self): + """Iteration uses a snapshot, so collecting an object mid-loop is safe.""" + # Given + test_registry = ObjectRegistry() + objs = [_Named('a'), _Named('b')] + for obj in objs: + test_registry.add(obj) + del obj + + # When + seen = [] + for name in test_registry: + seen.append(name) + objs.clear() + gc.collect() + + # Then + assert seen == ['a', 'b'] + assert len(test_registry) == 0 + + @pytest.mark.parametrize( + 'cls, kwargs', [(EasyBase, {'display_name': 'test'}), (Parameter, {'value': 2.0})] + ) + def test_identical_unique_names_exception(self, clear, cls, kwargs): + # When + test_obj = cls(unique_name='test', **kwargs) + # Then Expect + with pytest.raises(ValueError): + test_obj2 = cls(unique_name='test', **kwargs) + + def test_add_duplicate_name_error(self, clear, base_object): + """Test that adding an object with a duplicate name raises error""" + # When/Then + with pytest.raises(ValueError, match='already exists'): + # Try to add another object with same unique_name + duplicate_obj = EasyBase(display_name='duplicate', unique_name=base_object.unique_name) + + def test_unique_name_change_replaces_registry_entry( + self, clear, base_object, parameter_object + ): + # When + assert global_object.registry['EasyBase_0'] == base_object + assert global_object.registry['Parameter_0'] == parameter_object + # Then + base_object.unique_name = 'test3' + parameter_object.unique_name = 'test4' + # Expect + assert 'EasyBase_0' not in global_object.registry + assert 'Parameter_0' not in global_object.registry + assert global_object.registry['test3'] == base_object + assert global_object.registry['test4'] == parameter_object + + def test_unique_name_change_to_taken_name_keeps_old_entry( + self, clear, base_object, parameter_object + ): + # When Then + with pytest.raises(ValueError, match='already exists'): + base_object.unique_name = parameter_object.unique_name + # Expect + assert base_object.unique_name == 'EasyBase_0' + assert global_object.registry['EasyBase_0'] is base_object + assert global_object.registry['Parameter_0'] is parameter_object + + def test_unique_name_change_to_same_name(self, clear, base_object): + # When + base_object.unique_name = 'EasyBase_0' + # Then + assert global_object.registry['EasyBase_0'] is base_object + assert len(global_object.registry) == 1 + + def test_unique_name_reusable_after_rename(self, clear): + # When + first = EasyBase(unique_name='original') + first.unique_name = 'renamed' + # Then + second = EasyBase(unique_name='original') + # Expect + assert second.unique_name == 'original' + assert global_object.registry['original'] is second + assert global_object.registry['renamed'] is first + + def test_discard(self, clear, base_object): + """Test discarding a name completely""" + # Given + unique_name = base_object.unique_name + assert unique_name in global_object.registry + + # When + global_object.registry.discard(unique_name) + + # Then + assert unique_name not in global_object.registry + assert base_object not in global_object.registry.objects() + + def test_discard_nonexistent_key(self, clear): + """Test that discard handles nonexistent key gracefully.""" + # When/Then - Should not raise error + global_object.registry.discard('nonexistent_key') + + def test_registry_repr(self, clear, base_object, parameter_object): + """Test registry string representation""" + # When + repr_str = str(global_object.registry) + + # Then + assert 'ObjectRegistry object' in repr_str + assert '2' in repr_str # Should show entry count + + def test_registry_initialization(self): + """Test ObjectRegistry initialization""" + # When + test_registry = ObjectRegistry() + + # Then + assert len(test_registry) == 0 + + def test_discard_then_reuse_name(self): + """Discarding a reused name removes the new object, even after the old one is collected.""" + # Given + test_registry = ObjectRegistry() + obj_a = _Named('n') + test_registry.add(obj_a) + test_registry.discard('n') + obj_b = _Named('n') + test_registry.add(obj_b) + + # When + del obj_a + gc.collect() + test_registry.discard('n') + + # Then + assert 'n' not in test_registry + assert obj_b not in test_registry.objects() + + def test_clear_then_reuse_name(self): + """Discarding a name reused after clearing removes the new object.""" + # Given + test_registry = ObjectRegistry() + obj_a = _Named('n') + test_registry.add(obj_a) + test_registry.clear() + obj_b = _Named('n') + test_registry.add(obj_b) + + # When + del obj_a + gc.collect() + test_registry.discard('n') + + # Then + assert 'n' not in test_registry + assert obj_b not in test_registry.objects() + + def test_objects_contains_live_objects_only(self): + """Objects that have been collected are not returned.""" + # Given + test_registry = ObjectRegistry() + kept = _Named('kept') + released = _Named('released') + test_registry.add(kept) + test_registry.add(released) + + # When + del released + gc.collect() + + # Then + assert test_registry.objects() == [kept] + + def test_objects_lists_renamed_object_once(self, clear, base_object): + """A renamed object drops its old name, so it is returned once.""" + # When + base_object.unique_name = 'renamed' + + # Then + assert global_object.registry.objects() == [base_object] + assert global_object.registry.names() == ['renamed'] diff --git a/tests/unit/global_object/test_undo_redo.py b/tests/unit/global_object/test_undo_redo.py deleted file mode 100644 index 0171a0e6..00000000 --- a/tests/unit/global_object/test_undo_redo.py +++ /dev/null @@ -1,449 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import math - -import numpy as np -import pytest - -from easyscience import DescriptorNumber -from easyscience import Fitter -from easyscience import ObjBase -from easyscience import Parameter -from easyscience.base_classes import CollectionBase -from easyscience.variable import DescriptorBool -from easyscience.variable import DescriptorStr - - -def createSingleObjs(idx): - alphabet = 'abcdefghijklmnopqrstuvwxyz' - reps = math.floor(idx / len(alphabet)) + 1 - name = alphabet[idx % len(alphabet)] * reps - if idx % 2: - return Parameter(name, idx, unit='m/s') - else: - return DescriptorNumber(name, idx, unit='m/s') - - -def createParam(option): - return pytest.param(option, id=option[0]) - - -def doUndoRedo(obj, attr, future, additional=''): - from easyscience import global_object - - global_object.stack.enabled = True - e = False - - def getter(_obj, _attr): - value = getattr(_obj, _attr) - if additional: - value = getattr(value, additional) - return value - - try: - previous = getter(obj, attr) - if attr == 'unit' and hasattr(obj, 'convert_unit'): - obj.convert_unit(future) - else: - setattr(obj, attr, future) - assert getter(obj, attr) == future - assert global_object.stack.canUndo() - global_object.stack.undo() - assert getter(obj, attr) == previous - assert global_object.stack.canRedo() - global_object.stack.redo() - assert getter(obj, attr) == future - except Exception as err: - e = err - finally: - global_object.stack.enabled = False - return e - - -@pytest.mark.parametrize( - 'test', - [ - createParam(option) - for option in [ - ('value', 500), - ('error', 5), - ('unit', 'km/s'), - ('display_name', 'boom'), - ] - ], -) -def test_DescriptorNumberUndoRedo(test): - obj = DescriptorNumber('DescriptorNumber', 1, unit='m/s') - attr = test[0] - value = test[1] - - e = doUndoRedo(obj, attr, value) - assert not e - - -def test_DescriptorBoolUndoRedo(): - obj = DescriptorBool('DescriptorBool', False) - attr = 'value' - value = True - - e = doUndoRedo(obj, attr, value) - assert not e - - obj = DescriptorBool('DescriptorBool', True) - attr = 'value' - value = False - - e = doUndoRedo(obj, attr, value) - assert not e - - -def test_DescriptorStrUndoRedo(): - obj = DescriptorStr('DescriptorStr', 'Foo') - attr = 'value' - value = 'Bar' - - e = doUndoRedo(obj, attr, value) - assert not e - - -@pytest.mark.parametrize( - 'test', - [ - createParam(option) - for option in [ - ('value', 500), - ('error', 5), - ('unit', 'km/s'), - ('display_name', 'boom'), - ('fixed', False), - ('max', 505), - ('min', -1), - ] - ], -) -def test_ParameterUndoRedo(test): - obj = Parameter('Parameter', 1, unit='m/s') - attr = test[0] - value = test[1] - - e = doUndoRedo(obj, attr, value) - assert not e - - -def test_Parameter_Bounds_UndoRedo(): - from easyscience import global_object - - global_object.stack.enabled = True - parameter = Parameter('test', 1) - assert parameter.min == -np.inf - assert parameter.max == np.inf - - parameter.min = 0 - parameter.max = 2 - assert parameter.min == 0 - assert parameter.max == 2 - - global_object.stack.undo() - global_object.stack.undo() - assert parameter.min == -np.inf - assert parameter.max == np.inf - - -def test_ObjBaseUndoRedo(): - objs = {obj.name: obj for obj in [createSingleObjs(idx) for idx in range(5)]} - name = 'test' - obj = ObjBase(name, **objs) - name2 = 'best' - - # Test name - # assert not doUndoRedo(obj, 'name', name2) - - # Test setting value - for b_obj in objs.values(): - e = doUndoRedo(obj, b_obj.name, b_obj.value + 1, 'value') - assert not e - - -def test_CollectionBaseUndoRedo(): - objs = [createSingleObjs(idx) for idx in range(5)] - name = 'test' - obj = CollectionBase(name, *objs) - name2 = 'best' - - # assert not doUndoRedo(obj, 'name', name2) - - from easyscience import global_object - - global_object.stack.enabled = True - - original_length = len(obj) - p = Parameter('slip_in', 50) - idx = 2 - obj.insert(idx, p) - assert len(obj) == original_length + 1 - objs.insert(idx, p) - for item, obj_r in zip(obj, objs): - assert item == obj_r - - # Test inserting items - global_object.stack.undo() - assert len(obj) == original_length - _ = objs.pop(idx) - for item, obj_r in zip(obj, objs): - assert item == obj_r - global_object.stack.redo() - assert len(obj) == original_length + 1 - objs.insert(idx, p) - for item, obj_r in zip(obj, objs): - assert item == obj_r - - # Test Del Items - del obj[idx] - del objs[idx] - assert len(obj) == original_length - for item, obj_r in zip(obj, objs): - assert item == obj_r - global_object.stack.undo() - assert len(obj) == original_length + 1 - objs.insert(idx, p) - for item, obj_r in zip(obj, objs): - assert item == obj_r - del objs[idx] - global_object.stack.redo() - assert len(obj) == original_length - for item, obj_r in zip(obj, objs): - assert item == obj_r - - # Test Place Item - old_item = objs[idx] - objs[idx] = p - obj[idx] = p - assert len(obj) == original_length - for item, obj_r in zip(obj, objs): - assert item == obj_r - global_object.stack.undo() - for i in range(len(obj)): - if i == idx: - item = old_item - else: - item = objs[i] - assert obj[i] == item - global_object.stack.redo() - for item, obj_r in zip(obj, objs): - assert item == obj_r - - global_object.stack.enabled = False - - -def test_UndoRedoMacros(): - items = [createSingleObjs(idx) for idx in range(5)] - offset = 5 - undo_text = 'test_macro' - from easyscience import global_object - - global_object.stack.enabled = True - global_object.stack.beginMacro(undo_text) - values = [item.value for item in items] - - for item, value in zip(items, values): - item.value = value + offset - global_object.stack.endMacro() - - for item, old_value in zip(items, values): - assert item.value == old_value + offset - assert global_object.stack.undoText() == undo_text - - global_object.stack.undo() - - for item, old_value in zip(items, values): - assert item.value == old_value - assert global_object.stack.redoText() == undo_text - - global_object.stack.redo() - for item, old_value in zip(items, values): - assert item.value == old_value + offset - - -@pytest.mark.parametrize('fit_engine', ['LMFit', 'Bumps', 'DFO']) -def test_fittingUndoRedo(fit_engine): - m_value = 6 - c_value = 2 - x = np.linspace(-5, 5, 100) - weights = np.ones_like(x) - dy = np.random.rand(*x.shape) - - class Line(ObjBase): - def __init__(self, m: Parameter, c: Parameter): - super(Line, self).__init__('basic_line', m=m, c=c) - - @classmethod - def default(cls): - m = Parameter('m', m_value) - c = Parameter('c', c_value) - return cls(m=m, c=c) - - @classmethod - def from_pars(cls, m_value: float, c_value: float): - m = Parameter('m', m_value) - c = Parameter('c', c_value) - return cls(m=m, c=c) - - def __call__(self, x: np.ndarray) -> np.ndarray: - return self.m.value * x + self.c.value - - l1 = Line.default() - m_sp = 4 - c_sp = -3 - - l2 = Line.from_pars(m_sp, c_sp) - l2.m.fixed = False - l2.c.fixed = False - - y = l1(x) + 0.125 * (dy - 0.5) - - f = Fitter(l2, l2) - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') - - from easyscience import global_object - - global_object.stack.enabled = True - res = f.fit(x, y, weights=weights) - - # assert l1.c.value == pytest.approx(l2.c.value, rel=l2.c.error * 3) - # assert l1.m.value == pytest.approx(l2.m.value, rel=l2.m.error * 3) - assert global_object.stack.undoText() == 'Fitting routine' - - global_object.stack.undo() - assert l2.m.value == m_sp - assert l2.c.value == c_sp - assert global_object.stack.redoText() == 'Fitting routine' - - global_object.stack.redo() - assert l2.m.value == res.p[f'p{l2.m.unique_name}'] - assert l2.c.value == res.p[f'p{l2.c.unique_name}'] - - -# TODO: Check if this test is needed -# @pytest.mark.parametrize('math_funcs', [pytest.param([Parameter.__add__, float.__add__], id='Addition'), -# pytest.param([Parameter.__sub__, float.__sub__], id='Subtraction')]) -# def test_parameter_maths_basic(math_funcs): -# a = 1.0 -# b = 2.0 -# sa = 0.1 -# sb = 0.2 - -# p_fun = math_funcs[0] -# f_fun = math_funcs[1] - -# result_value = f_fun(a, b) -# result_error = (sa ** 2 + sb ** 2) ** 0.5 - -# from easyscience import global_object -# global_object.stack.enabled = True - -# # Perform basic test -# p1 = Parameter('a', a) -# p2 = Parameter('b', b) - -# p1 = p_fun(p1, p2) - -# assert p1.value == result_value -# global_object.stack.undo() -# assert p1.value == a -# global_object.stack.redo() -# assert p1.value == result_value - -# # Perform basic + error -# p1 = Parameter('a', a, error=sa) -# p2 = Parameter('b', b, error=sb) -# p1 = p_fun(p1, p2) -# assert p1.value == result_value -# assert p1.error == result_error -# global_object.stack.undo() -# assert p1.value == a -# assert p1.error == sa -# global_object.stack.redo() -# assert p1.value == result_value -# assert p1.error == result_error - -# # Perform basic + units -# p1 = Parameter('a', a, error=sa, units='m/s') -# p2 = Parameter('b', b, error=sb, units='m/s') -# p1 = p_fun(p1, p2) -# assert p1.value == result_value -# assert p1.error == result_error -# assert str(p1.unit) == 'meter / second' -# global_object.stack.undo() -# assert p1.value == a -# assert p1.error == sa -# assert str(p1.unit) == 'meter / second' -# global_object.stack.redo() -# assert p1.value == result_value -# assert p1.error == result_error -# assert str(p1.unit) == 'meter / second' - - -# @pytest.mark.parametrize('math_funcs', [pytest.param([Parameter.__imul__, float.__mul__, -# 'meter ** 2 / second ** 2'], id='Multiplication'), -# pytest.param([Parameter.__itruediv__, float.__truediv__, -# 'dimensionless'], id='Division')]) -# def test_parameter_maths_advanced(math_funcs): -# a = 4.0 -# b = 2.0 -# sa = 0.1 -# sb = 0.2 -# unit = 'meter / second' - -# p_fun = math_funcs[0] -# f_fun = math_funcs[1] -# u_str = math_funcs[2] - -# result_value = f_fun(a, b) -# result_error = ((sa / a) ** 2 + (sb / b) ** 2) ** 0.5 * result_value - -# from easyscience import global_object -# global_object.stack.enabled = True - -# # Perform basic test -# p1 = Parameter('a', a) -# p2 = Parameter('b', b) - -# p1 = p_fun(p1, p2) -# assert p1.value == result_value -# global_object.stack.undo() -# assert p1.value == a -# global_object.stack.redo() -# assert p1.value == result_value - -# # Perform basic + error -# p1 = Parameter('a', a, error=sa) -# p2 = Parameter('b', b, error=sb) -# p1 = p_fun(p1, p2) -# assert p1.value == result_value -# assert p1.error == result_error -# global_object.stack.undo() -# assert p1.value == a -# assert p1.error == sa -# global_object.stack.redo() -# assert p1.value == result_value -# assert p1.error == result_error - -# # Perform basic + units -# p1 = Parameter('a', a, error=sa, units=unit) -# p2 = Parameter('b', b, error=sb, units=unit) -# p1 = p_fun(p1, p2) -# assert p1.value == result_value -# assert p1.error == result_error -# assert str(p1.unit) == u_str -# global_object.stack.undo() -# assert p1.value == a -# assert p1.error == sa -# assert str(p1.unit) == unit -# global_object.stack.redo() -# assert p1.value == result_value -# assert p1.error == result_error -# assert str(p1.unit) == u_str diff --git a/tests/unit/global_object/test_undo_redo_comprehensive.py b/tests/unit/global_object/test_undo_redo_comprehensive.py deleted file mode 100644 index 05264c1b..00000000 --- a/tests/unit/global_object/test_undo_redo_comprehensive.py +++ /dev/null @@ -1,705 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from unittest.mock import MagicMock -from unittest.mock import patch - -import pytest - -from easyscience.global_object.undo_redo import CommandHolder -from easyscience.global_object.undo_redo import DictStack -from easyscience.global_object.undo_redo import DictStackReCreate -from easyscience.global_object.undo_redo import FunctionStack -from easyscience.global_object.undo_redo import NotarizedDict -from easyscience.global_object.undo_redo import PropertyStack -from easyscience.global_object.undo_redo import UndoCommand -from easyscience.global_object.undo_redo import UndoStack -from easyscience.global_object.undo_redo import dict_stack_deco -from easyscience.global_object.undo_redo import property_stack - - -class MockUndoCommand(UndoCommand): - """Mock implementation for testing""" - - def __init__(self, obj, text=None): - super().__init__(obj) - if text: - self.text = text - self.undo_called = False - self.redo_called = False - - def undo(self): - self.undo_called = True - - def redo(self): - self.redo_called = True - - -class TestUndoCommand: - """Test abstract UndoCommand base class""" - - def test_init(self): - """Test UndoCommand initialization""" - obj = MagicMock() - cmd = MockUndoCommand(obj) - - assert cmd._obj is obj - assert cmd._text is None - - def test_text_property(self): - """Test text property getter/setter""" - cmd = MockUndoCommand(MagicMock()) - - # Test getter - assert cmd.text is None - - # Test setter - cmd.text = 'test text' - assert cmd.text == 'test text' - - def test_abstract_methods(self): - """Test that abstract methods are implemented""" - cmd = MockUndoCommand(MagicMock()) - - # Should not raise NotImplementedError - cmd.undo() - cmd.redo() - assert cmd.undo_called - assert cmd.redo_called - - -class TestCommandHolder: - """Test CommandHolder class""" - - def test_init_default(self): - """Test CommandHolder initialization""" - holder = CommandHolder() - assert len(holder) == 0 - assert holder.text == '' - assert not holder.is_macro - - def test_init_with_text(self): - """Test CommandHolder initialization with text""" - holder = CommandHolder('test text') - assert holder._text == 'test text' - - def test_append_and_pop(self): - """Test appending and popping commands""" - holder = CommandHolder() - cmd = MockUndoCommand(MagicMock()) - - holder.append(cmd) - assert len(holder) == 1 - assert holder.current is cmd - - popped = holder.pop() - assert popped is cmd - assert len(holder) == 0 - - def test_iteration(self): - """Test iteration over commands""" - holder = CommandHolder() - cmd1 = MockUndoCommand(MagicMock(), 'cmd1') - cmd2 = MockUndoCommand(MagicMock(), 'cmd2') - - holder.append(cmd1) - holder.append(cmd2) - - commands = list(holder) - assert len(commands) == 2 - assert cmd2 in commands # Last appended should be first (LIFO) - - def test_is_macro_property(self): - """Test is_macro property""" - holder = CommandHolder() - assert not holder.is_macro - - holder.append(MockUndoCommand(MagicMock())) - assert not holder.is_macro - - holder.append(MockUndoCommand(MagicMock())) - assert holder.is_macro - - def test_text_property_with_commands(self): - """Test text property with commands""" - holder = CommandHolder() - cmd = MockUndoCommand(MagicMock(), 'command text') - holder.append(cmd) - - assert holder.text == 'command text' - - def test_text_property_override(self): - """Test text property with override""" - holder = CommandHolder('override text') - cmd = MockUndoCommand(MagicMock(), 'command text') - holder.append(cmd) - - assert holder.text == 'override text' - - -class TestUndoStack: - """Test UndoStack class""" - - @pytest.fixture - def stack(self): - """Create a fresh UndoStack for each test""" - return UndoStack() - - def test_init_default(self, stack): - """Test UndoStack initialization""" - assert stack.enabled is False - assert len(stack.history) == 0 - assert len(stack.future) == 0 - assert not stack._macro_running - assert not stack._command_running - - def test_init_with_max_history(self): - """Test UndoStack initialization with max history""" - stack = UndoStack(max_history=5) - assert stack._max_history == 5 - assert stack.history.maxlen == 5 - assert stack.future.maxlen == 5 - - def test_enabled_property(self, stack): - """Test enabled property getter/setter""" - assert not stack.enabled - - stack.enabled = True - assert stack.enabled - - stack.enabled = False - assert not stack.enabled - - def test_enabled_setter_with_running_macro(self, stack): - """Test that setting enabled=False ends running macro""" - stack.enabled = True - stack.beginMacro('test') - assert stack._macro_running - - stack.enabled = False - assert not stack._macro_running - - def test_force_state(self, stack): - """Test force_state method""" - stack.force_state(True) - assert stack.enabled - - stack.force_state(False) - assert not stack.enabled - - def test_push_disabled_stack(self, stack): - """Test pushing command to disabled stack""" - cmd = MockUndoCommand(MagicMock()) - - stack.push(cmd) - - assert cmd.redo_called - assert len(stack.history) == 0 - - def test_push_enabled_stack(self, stack): - """Test pushing command to enabled stack""" - stack.enabled = True - cmd = MockUndoCommand(MagicMock()) - - stack.push(cmd) - - assert cmd.redo_called - assert len(stack.history) == 1 - assert len(stack.future) == 0 - - def test_push_with_macro(self, stack): - """Test pushing command during macro""" - stack.enabled = True - stack.beginMacro('test macro') - - cmd1 = MockUndoCommand(MagicMock()) - cmd2 = MockUndoCommand(MagicMock()) - - stack.push(cmd1) - stack.push(cmd2) - - assert len(stack.history) == 1 # Only the macro holder - assert len(stack.history[0]) == 2 # Two commands in the macro - - def test_push_clears_future(self, stack): - """Test that push clears future stack""" - stack.enabled = True - cmd1 = MockUndoCommand(MagicMock()) - cmd2 = MockUndoCommand(MagicMock()) - - stack.push(cmd1) - stack.undo() - assert len(stack.future) == 1 - - stack.push(cmd2) - assert len(stack.future) == 0 - - def test_pop(self, stack): - """Test popping last command""" - stack.enabled = True - cmd = MockUndoCommand(MagicMock()) - - stack.push(cmd) - popped = stack.pop() - - assert popped is cmd - assert len(stack.history) == 0 - - def test_pop_from_macro(self, stack): - """Test popping from macro""" - stack.enabled = True - stack.beginMacro('test') - - cmd1 = MockUndoCommand(MagicMock()) - cmd2 = MockUndoCommand(MagicMock()) - - stack.push(cmd1) - stack.push(cmd2) - - popped = stack.pop() - assert popped is cmd2 - assert len(stack.history[0]) == 1 # One command left in macro - - def test_clear(self, stack): - """Test clearing the stack""" - stack.enabled = True - stack.push(MockUndoCommand(MagicMock())) - stack.beginMacro('test') - - stack.clear() - - assert len(stack.history) == 0 - assert len(stack.future) == 0 - assert not stack._macro_running - - def test_undo_success(self, stack): - """Test successful undo operation""" - stack.enabled = True - cmd = MockUndoCommand(MagicMock()) - - stack.push(cmd) - stack.undo() - - assert cmd.undo_called - assert len(stack.history) == 0 - assert len(stack.future) == 1 - - def test_undo_cannot_undo(self, stack): - """Test undo when cannot undo""" - assert not stack.canUndo() - stack.undo() # Should not raise error - - def test_undo_with_exception(self, stack): - """Test undo with exception in command""" - stack.enabled = True - - class FailingCommand(UndoCommand): - def undo(self): - raise Exception('test error') - - def redo(self): - pass - - cmd = FailingCommand(MagicMock()) - stack.push(cmd) - - # Should not raise, just print error - stack.undo() - - def test_redo_success(self, stack): - """Test successful redo operation""" - stack.enabled = True - cmd = MockUndoCommand(MagicMock()) - - stack.push(cmd) - stack.undo() - stack.redo() - - assert cmd.redo_called - assert len(stack.history) == 1 - assert len(stack.future) == 0 - - def test_redo_cannot_redo(self, stack): - """Test redo when cannot redo""" - assert not stack.canRedo() - stack.redo() # Should not raise error - - def test_macro_operations(self, stack): - """Test macro begin/end operations""" - stack.enabled = True - - stack.beginMacro('test macro') - assert stack._macro_running - - cmd1 = MockUndoCommand(MagicMock()) - cmd2 = MockUndoCommand(MagicMock()) - stack.push(cmd1) - stack.push(cmd2) - - stack.endMacro() - assert not stack._macro_running - - # Undo should undo both commands - stack.undo() - assert cmd1.undo_called - assert cmd2.undo_called - - def test_macro_nested_error(self, stack): - """Test error on nested macro""" - stack.enabled = True - stack.beginMacro('test1') - - with pytest.raises(AssertionError, match='already running'): - stack.beginMacro('test2') - - def test_end_macro_without_begin_error(self, stack): - """Test error on ending macro without begin""" - stack.enabled = True - - with pytest.raises(AssertionError, match='not running'): - stack.endMacro() - - def test_can_undo_redo_states(self, stack): - """Test canUndo/canRedo state checking""" - assert not stack.canUndo() - assert not stack.canRedo() - - stack.enabled = True - cmd = MockUndoCommand(MagicMock()) - stack.push(cmd) - - assert stack.canUndo() - assert not stack.canRedo() - - stack.undo() - assert not stack.canUndo() - assert stack.canRedo() - - def test_can_undo_redo_during_macro(self, stack): - """Test canUndo/canRedo during macro""" - stack.enabled = True - stack.push(MockUndoCommand(MagicMock())) - - stack.beginMacro('test') - assert not stack.canUndo() # Cannot undo during macro - assert not stack.canRedo() # Cannot redo during macro - - def test_undo_redo_text(self, stack): - """Test undo/redo text retrieval""" - stack.enabled = True - cmd = MockUndoCommand(MagicMock(), 'test command') - - stack.push(cmd) - assert stack.undoText() == 'test command' - assert stack.redoText() == '' - - stack.undo() - assert stack.undoText() == '' - assert stack.redoText() == 'test command' - - -class TestPropertyStack: - """Test PropertyStack command""" - - def test_init(self): - """Test PropertyStack initialization""" - parent = MagicMock() - func = MagicMock() - old_value = 10 - new_value = 20 - - cmd = PropertyStack(parent, func, old_value, new_value) - - assert cmd._parent is parent - assert cmd._set_func is func - assert cmd._old_value == old_value - assert cmd._new_value == new_value - assert '10' in cmd.text and '20' in cmd.text - - def test_init_with_custom_text(self): - """Test PropertyStack with custom text""" - cmd = PropertyStack(MagicMock(), MagicMock(), 10, 20, 'custom text') - assert cmd.text == 'custom text' - - def test_undo_redo(self): - """Test PropertyStack undo/redo""" - parent = MagicMock() - func = MagicMock() - - cmd = PropertyStack(parent, func, 10, 20) - - cmd.undo() - func.assert_called_once_with(parent, 10) - - func.reset_mock() - cmd.redo() - func.assert_called_once_with(parent, 20) - - -class TestFunctionStack: - """Test FunctionStack command""" - - def test_init(self): - """Test FunctionStack initialization""" - parent = MagicMock() - set_func = MagicMock() - unset_func = MagicMock() - - cmd = FunctionStack(parent, set_func, unset_func) - - assert cmd._parent is parent - assert cmd._old_fn is set_func - assert cmd._new_fn is unset_func - - def test_undo_redo(self): - """Test FunctionStack undo/redo""" - set_func = MagicMock() - unset_func = MagicMock() - - cmd = FunctionStack(MagicMock(), set_func, unset_func) - - cmd.undo() - unset_func.assert_called_once() - - cmd.redo() - set_func.assert_called_once() - - -class TestNotarizedDict: - """Test NotarizedDict class""" - - def test_init(self): - """Test NotarizedDict initialization""" - nd = NotarizedDict(a=1, b=2) - - assert nd['a'] == 1 - assert nd['b'] == 2 - assert not nd._stack_enabled - - def test_setitem_without_stack(self): - """Test setitem without stack enabled""" - nd = NotarizedDict() - nd['key'] = 'value' - - assert nd['key'] == 'value' - - def test_delitem_without_stack(self): - """Test delitem without stack enabled""" - nd = NotarizedDict(key='value') - del nd['key'] - - assert 'key' not in nd - - def test_reorder_without_stack(self): - """Test reorder without stack enabled""" - nd = NotarizedDict(a=1, b=2) - nd.reorder(b=2, a=1) - - assert list(nd.keys()) == ['b', 'a'] - - def test_repr(self): - """Test string representation""" - nd = NotarizedDict(a=1) - repr_str = repr(nd) - - assert 'NotarizedDict' in repr_str - assert "'a': 1" in repr_str - - -class TestDictStack: - """Test DictStack command""" - - def test_init_deletion(self): - """Test DictStack initialization for deletion""" - nd = NotarizedDict(key='value') - cmd = DictStack(nd, 'key') - - assert cmd._deletion is True - assert cmd._creation is False - assert cmd._key == 'key' - assert cmd._old_value == 'value' - assert cmd._index == 0 - - def test_init_creation(self): - """Test DictStack initialization for creation""" - nd = NotarizedDict() - cmd = DictStack(nd, 'key', 'value') - - assert cmd._creation is True - assert cmd._deletion is False - assert cmd._key == 'key' - assert cmd._new_value == 'value' - - def test_init_modification(self): - """Test DictStack initialization for modification""" - nd = NotarizedDict(key='old_value') - cmd = DictStack(nd, 'key', 'new_value') - - assert not cmd._creation - assert not cmd._deletion - assert cmd._old_value == 'old_value' - assert cmd._new_value == 'new_value' - - def test_undo_redo_creation(self): - """Test undo/redo for creation""" - nd = NotarizedDict() - cmd = DictStack(nd, 'key', 'value') - - # Simulate the initial operation (like UndoStack.push() does) - cmd.redo() - assert nd.data['key'] == 'value' - - # After creation, undo should delete the key - cmd.undo() - assert 'key' not in nd.data - - # After undo, redo should create/set new value - cmd.redo() - assert nd.data['key'] == 'value' - - def test_undo_redo_modification(self): - """Test undo/redo for modification""" - nd = NotarizedDict() - nd['key'] = 'old_value' - cmd = DictStack(nd, 'key', 'new_value') - - # Simulate the initial operation (like UndoStack.push() does) - cmd.redo() - assert nd.data['key'] == 'new_value' - - # After modification, undo should revert to old value - cmd.undo() - assert nd.data['key'] == 'old_value' - - # After undo, redo should set new value - cmd.redo() - assert nd.data['key'] == 'new_value' - - def test_undo_redo_deletion(self): - """Test undo/redo for deletion""" - nd = NotarizedDict(key='value') - cmd = DictStack(nd, 'key') - - # Simulate the initial operation (like UndoStack.push() does) - cmd.redo() - assert 'key' not in nd.data - - # After deletion, undo should restore - cmd.undo() - assert nd.data['key'] == 'value' - - # After undo, redo should delete - cmd.redo() - assert 'key' not in nd.data - - -class TestDictStackReCreate: - """Test DictStackReCreate command""" - - def test_init(self): - """Test DictStackReCreate initialization""" - nd = NotarizedDict(a=1, b=2) - new_dict = {'c': 3, 'd': 4} - - cmd = DictStackReCreate(nd, **new_dict) - - assert cmd._old_value == {'a': 1, 'b': 2} - assert cmd._new_value == new_dict - - def test_undo_redo(self): - """Test undo/redo operations""" - nd = NotarizedDict(a=1, b=2) - old_data = nd.data.copy() - new_dict = {'c': 3, 'd': 4} - - cmd = DictStackReCreate(nd, **new_dict) - - cmd.undo() - assert nd.data == old_data - - cmd.redo() - assert nd.data == new_dict - - -class TestPropertyStackDecorator: - """Test property_stack decorator""" - - def test_decorator_without_args(self): - """Test decorator applied without arguments""" - - @property_stack - def test_func(obj, value): - obj.value = value - - assert hasattr(test_func, 'func') - - def test_decorator_with_string_arg(self): - """Test decorator applied with string argument""" - - @property_stack('Custom text') - def test_func(obj, value): - obj.value = value - - assert hasattr(test_func, 'func') - - def test_decorator_no_change_no_push(self): - """Test decorator doesn't push when value unchanged""" - - class TestObj: - def __init__(self): - self.value = 10 - - @property_stack - def value(obj, new_value): - obj.value = new_value - - obj = TestObj() - - with patch('easyscience.global_object') as mock_global: - mock_stack = MagicMock() - mock_global.stack = mock_stack - - value(obj, 10) # Same value - - # Should NOT have pushed anything since value didn't change - mock_stack.push.assert_not_called() - - -class TestDictStackDecorator: - """Test dict_stack_deco decorator""" - - def test_decorator_with_stack_disabled(self): - """Test decorator when stack is disabled""" - nd = NotarizedDict() - nd._stack_enabled = False - - @dict_stack_deco - def test_func(obj, key, value): - obj[key] = value - - # Should call the function directly - with patch('easyscience.global_object') as mock_global: - mock_stack = MagicMock() - mock_global.stack = mock_stack - test_func(nd, 'key', 'value') - mock_stack.push.assert_not_called() - - def test_decorator_with_stack_enabled(self): - """Test decorator when stack is enabled""" - nd = NotarizedDict() - nd._stack_enabled = True - - @dict_stack_deco - def test_func(obj, key, value): - pass # Don't actually modify - - with patch('easyscience.global_object') as mock_global: - mock_stack = MagicMock() - mock_global.stack = mock_stack - - test_func(nd, 'key', 'value') - - # Should push a DictStack command - mock_stack.push.assert_called_once() - args = mock_stack.push.call_args[0] - assert isinstance(args[0], DictStack) diff --git a/tests/unit/io/test_serializer_component.py b/tests/unit/io/test_serializer_component.py index c4e21f49..08cc3405 100644 --- a/tests/unit/io/test_serializer_component.py +++ b/tests/unit/io/test_serializer_component.py @@ -19,7 +19,6 @@ '@module': DescriptorNumber.__module__, '@class': DescriptorNumber.__name__, '@version': easyscience.__version__, - 'name': 'test', 'value': 1.0, 'variance': None, 'unit': 'dimensionless', @@ -34,7 +33,6 @@ '@module': Parameter.__module__, '@class': Parameter.__name__, '@version': easyscience.__version__, - 'name': 'test', 'unit': 'km', 'value': 1.0, 'variance': 0.0, @@ -71,7 +69,7 @@ def check_dict(check, item): @pytest.mark.parametrize(**skip_dict) @pytest.mark.parametrize(**dp_param_dict) -def test_variable_as_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +def test_variable_to_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} obj = dp_cls(**data_dict) @@ -84,7 +82,7 @@ def test_variable_as_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber if not isinstance(skip, list): skip = [skip] - enc = obj.as_dict(skip=skip) + enc = obj.to_dict(skip=skip) expected_keys = set(dp_kwargs.keys()) obtained_keys = set(enc.keys()) diff --git a/tests/unit/io/test_serializer_dict.py b/tests/unit/io/test_serializer_dict.py deleted file mode 100644 index 1ddf21ef..00000000 --- a/tests/unit/io/test_serializer_dict.py +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from copy import deepcopy -from typing import Type - -import pytest - -from easyscience import DescriptorNumber -from easyscience import ObjBase -from easyscience import global_object -from easyscience.io.serializer_dict import SerializerDict - -from .test_serializer_component import check_dict -from .test_serializer_component import dp_param_dict -from .test_serializer_component import skip_dict - - -def recursive_remove(d, remove_keys: list) -> dict: - """ - Remove keys from a dictionary. - """ - if not isinstance(remove_keys, list): - remove_keys = [remove_keys] - if isinstance(d, dict): - dd = {} - for k in d.keys(): - if k not in remove_keys: - dd[k] = recursive_remove(d[k], remove_keys) - return dd - else: - return d - - -######################################################################################################################## -# TESTING ENCODING -######################################################################################################################## -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_SerializerDict(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} - - obj = dp_cls(**data_dict) - - dp_kwargs = deepcopy(dp_kwargs) - - if isinstance(skip, str): - del dp_kwargs[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc = obj.encode(skip=skip, encoder=SerializerDict) - - expected_keys = set(dp_kwargs.keys()) - obtained_keys = set(enc.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(dp_kwargs, enc) - - -######################################################################################################################## -# TESTING DECODING -######################################################################################################################## -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_SerializerDict_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} - - obj = dp_cls(**data_dict) - - enc = obj.encode(encoder=SerializerDict) - global_object.map._clear() - dec = dp_cls.decode(enc, decoder=SerializerDict) - - for k in data_dict.keys(): - if hasattr(obj, k) and hasattr(dec, k): - assert getattr(obj, k) == getattr(dec, k) - else: - raise AttributeError(f'{k} not found in decoded object') - - -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_SerializerDict_from_dict(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} - - obj = dp_cls(**data_dict) - - enc = obj.encode(encoder=SerializerDict) - global_object.map._clear() - dec = dp_cls.from_dict(enc) - - for k in data_dict.keys(): - if hasattr(obj, k) and hasattr(dec, k): - assert getattr(obj, k) == getattr(dec, k) - else: - raise AttributeError(f'{k} not found in decoded object') - - -def test_group_encode(): - d0 = DescriptorNumber('a', 0) - d1 = DescriptorNumber('b', 1) - - from easyscience.base_classes import CollectionBase - - b = CollectionBase('test', d0, d1) - d = b.as_dict() - assert isinstance(d['data'], list) - - -def test_group_encode2(): - d0 = DescriptorNumber('a', 0) - d1 = DescriptorNumber('b', 1) - - from easyscience.base_classes import CollectionBase - - b = ObjBase('outer', b=CollectionBase('test', d0, d1)) - d = b.as_dict() - assert isinstance(d['b'], dict) diff --git a/tests/unit/models/__init__.py b/tests/unit/job/__init__.py similarity index 99% rename from tests/unit/models/__init__.py rename to tests/unit/job/__init__.py index b1990ec8..4e798e20 100644 --- a/tests/unit/models/__init__.py +++ b/tests/unit/job/__init__.py @@ -1,3 +1,2 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - diff --git a/tests/unit/job/test_job.py b/tests/unit/job/test_job.py new file mode 100644 index 00000000..950b77cc --- /dev/null +++ b/tests/unit/job/test_job.py @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np +import pytest + +from easyscience import global_object +from easyscience.base_classes import ModelBase +from easyscience.job import AnalysisBase +from easyscience.job import ExperimentBase +from easyscience.job import JobBase +from easyscience.job import TheoreticalModelBase + + +@pytest.fixture(autouse=True) +def clear(): + global_object.registry.clear() + yield + global_object.registry.clear() + + +class Analysis(AnalysisBase): + def calculate_theory(self, x, **kwargs): + return super().calculate_theory(x, **kwargs) + + def fit(self, x, y, e, **kwargs): + return super().fit(x, y, e, **kwargs) + + +class Job(JobBase): + @JobBase.theoretical_model.setter + def theoretical_model(self, theory): + JobBase.theoretical_model.fset(self, theory) + + @JobBase.experiment.setter + def experiment(self, experiment): + JobBase.experiment.fset(self, experiment) + + @JobBase.analysis.setter + def analysis(self, analysis): + JobBase.analysis.fset(self, analysis) + + def calculate_theory(self, *args, **kwargs): + return super().calculate_theory(*args, **kwargs) + + def fit(self, *args, **kwargs): + return super().fit(*args, **kwargs) + + +@pytest.mark.parametrize('cls', [Analysis, ExperimentBase, Job, TheoreticalModelBase]) +def test_job_classes_are_model_bases(cls): + obj = cls(display_name='name', unique_name=f'{cls.__name__}_unique') + + assert isinstance(obj, ModelBase) + assert obj.display_name == 'name' + assert obj.unique_name == f'{cls.__name__}_unique' + assert obj.get_all_variables() == [] + + +class TestAnalysisBase: + def test_defaults(self): + analysis = Analysis(display_name='analysis') + + assert analysis.minimizer is None + assert str(analysis) == 'Analysis: analysis' + + def test_arguments_are_keyword_only(self): + with pytest.raises(TypeError): + Analysis('analysis') + + def test_minimizer_setter(self): + analysis = Analysis() + minimizer = object() + + analysis.minimizer = minimizer + + assert analysis.minimizer is minimizer + + def test_abstract_methods_raise(self): + analysis = Analysis() + x = np.array([1.0]) + + with pytest.raises(NotImplementedError, match='calculate_theory'): + analysis.calculate_theory(x) + with pytest.raises(NotImplementedError, match='fit'): + analysis.fit(x, x, x) + + def test_cannot_instantiate_abstract_base(self): + with pytest.raises(TypeError): + AnalysisBase() + + +class TestExperimentBase: + def test_str(self): + assert str(ExperimentBase(display_name='experiment')) == 'Experiment: experiment' + + def test_str_falls_back_to_unique_name(self): + experiment = ExperimentBase(unique_name='exp_1') + + assert str(experiment) == 'Experiment: exp_1' + + +class TestTheoreticalModelBase: + def test_str_not_implemented(self): + with pytest.raises(NotImplementedError): + str(TheoreticalModelBase()) + + +class TestJobBase: + def test_defaults(self): + job = Job(display_name='job') + + assert job.theoretical_model is None + assert job.experiment is None + assert job.analysis is None + + @pytest.mark.parametrize( + ('attribute', 'value_factory'), + [ + ('theoretical_model', TheoreticalModelBase), + ('experiment', ExperimentBase), + ('analysis', Analysis), + ], + ) + def test_base_setters_are_abstract(self, attribute, value_factory): + job = Job() + + with pytest.raises(NotImplementedError, match='setter not implemented'): + setattr(job, attribute, value_factory()) + + def test_abstract_methods_raise(self): + job = Job() + + with pytest.raises(NotImplementedError, match='calculate_theory'): + job.calculate_theory() + with pytest.raises(NotImplementedError, match='fit'): + job.fit() + + def test_cannot_instantiate_abstract_base(self): + with pytest.raises(TypeError): + JobBase() diff --git a/tests/unit/legacy/test_collection_base.py b/tests/unit/legacy/test_collection_base.py deleted file mode 100644 index 152a0a37..00000000 --- a/tests/unit/legacy/test_collection_base.py +++ /dev/null @@ -1,155 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -"""Tests for easyscience.legacy.collection_base.CollectionBase. - -These tests cover methods not exercised by the existing -tests/unit/base_classes/test_collection_base.py suite. -""" - -import logging - -import pytest - -from easyscience import DescriptorNumber -from easyscience import Parameter -from easyscience import global_object -from easyscience.legacy.collection_base import CollectionBase - - -@pytest.fixture(autouse=True) -def _clear_map(): - global_object.map._clear() - yield - global_object.map._clear() - - -@pytest.fixture -def setup_pars(): - return { - 'name': 'test', - 'par1': Parameter('p1', 0.1, fixed=True), - 'des1': DescriptorNumber('d1', 0.1), - 'par2': Parameter('p2', 0.2), - 'des2': DescriptorNumber('d2', 0.2), - 'par3': Parameter('p3', 0.3), - } - - -# --------------------------------------------------------------------------- -# Deprecation warning on instantiation -# --------------------------------------------------------------------------- - -def test_instantiation_emits_deprecation_warning(caplog: "pytest.LogCaptureFixture"): - """Instantiating CollectionBase should log a deprecation warning.""" - with caplog.at_level(logging.WARNING, logger='easyscience.legacy'): - CollectionBase('test') - assert len(caplog.records) >= 1 - messages = [r.message for r in caplog.records] - combined = ' '.join(messages) - assert 'deprecated' in combined.lower() - - -# --------------------------------------------------------------------------- -# sort -# --------------------------------------------------------------------------- - -def test_sort_ascending(setup_pars): - """Sort by parameter value in ascending order.""" - name = setup_pars.pop('name') - coll = CollectionBase(name, **setup_pars) - coll.sort(mapping=lambda item: item.value, reverse=False) - values = [item.value for item in coll] - assert values == sorted(values) - - -def test_sort_descending(setup_pars): - """Sort by parameter value in descending order.""" - name = setup_pars.pop('name') - coll = CollectionBase(name, **setup_pars) - coll.sort(mapping=lambda item: item.value, reverse=True) - values = [item.value for item in coll] - assert values == sorted(values, reverse=True) - - -# --------------------------------------------------------------------------- -# data property -# --------------------------------------------------------------------------- - -def test_data_property(setup_pars): - """The data property returns a tuple of stored items.""" - name = setup_pars.pop('name') - coll = CollectionBase(name, **setup_pars) - d = coll.data - assert isinstance(d, tuple) - assert len(d) == 5 - # Items should be the same objects - for item_from_data, item_from_iter in zip(d, coll): - assert item_from_data is item_from_iter - - -# --------------------------------------------------------------------------- -# __setitem__ with an EasyScience object (not a Number) -# --------------------------------------------------------------------------- - -def test_setitem_with_easyscience_object(setup_pars): - """Replace an item at an index with another EasyScience object.""" - name = setup_pars.pop('name') - coll = CollectionBase(name, **setup_pars) - - n_before = len(coll) - old_item = coll[0] - new_item = Parameter('replacement', 99.0) - - coll[0] = new_item - - assert len(coll) == n_before - assert coll[0].name == 'replacement' - assert coll[0].value == 99.0 - # Old item should be removed from the graph - assert old_item.unique_name not in global_object.map.get_edges(coll) - - -# --------------------------------------------------------------------------- -# __getitem__ with duplicate names returns a new CollectionBase -# --------------------------------------------------------------------------- - -def test_getitem_duplicate_names_returns_collection(setup_pars): - """When multiple items share the same name, __getitem__ returns a sub-collection.""" - name = setup_pars.pop('name') - # Add two items with the same display name - p1 = Parameter('same_name', 1.0) - p2 = Parameter('same_name', 2.0) - coll = CollectionBase(name, p1, p2) - - result = coll['same_name'] - assert isinstance(result, CollectionBase) - assert len(result) == 2 - - -def test_getitem_nonexistent_name_raises(setup_pars): - """Looking up a nonexistent name raises IndexError.""" - name = setup_pars.pop('name') - coll = CollectionBase(name, **setup_pars) - - with pytest.raises(IndexError, match='Given index does not exist'): - _ = coll['nonexistent'] - - -# --------------------------------------------------------------------------- -# insert method (called by MutableSequence.append, but test edge case) -# --------------------------------------------------------------------------- - -def test_insert_at_specific_index(setup_pars): - """Insert an item at a specific index (not just via append).""" - name = setup_pars.pop('name') - coll = CollectionBase(name, **setup_pars) - - n_before = len(coll) - new_item = Parameter('inserted', 42.0) - - coll.insert(2, new_item) - - assert len(coll) == n_before + 1 - assert coll[2].name == 'inserted' - assert coll[2].value == 42.0 diff --git a/tests/unit/legacy/test_dict.py b/tests/unit/legacy/test_dict.py deleted file mode 100644 index b1990ec8..00000000 --- a/tests/unit/legacy/test_dict.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - diff --git a/tests/unit/legacy/test_json.py b/tests/unit/legacy/test_json.py deleted file mode 100644 index b1990ec8..00000000 --- a/tests/unit/legacy/test_json.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - diff --git a/tests/unit/legacy/test_obj_base.py b/tests/unit/legacy/test_obj_base.py deleted file mode 100644 index fc33c424..00000000 --- a/tests/unit/legacy/test_obj_base.py +++ /dev/null @@ -1,119 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -"""Tests for easyscience.legacy.obj_base.ObjBase. - -These tests cover methods not exercised by the existing -tests/unit/base_classes/test_obj_base.py suite. -""" - -import logging -from typing import ClassVar - -import pytest - -from easyscience import Parameter -from easyscience import global_object -from easyscience.legacy.obj_base import ObjBase - - -@pytest.fixture(autouse=True) -def _clear_map(): - global_object.map._clear() - yield - global_object.map._clear() - - -# --------------------------------------------------------------------------- -# Deprecation warning on instantiation -# --------------------------------------------------------------------------- - -def test_instantiation_emits_deprecation_warning(caplog: "pytest.LogCaptureFixture"): - """Instantiating ObjBase should log a deprecation warning.""" - with caplog.at_level(logging.WARNING, logger='easyscience.legacy'): - ObjBase('test') - assert len(caplog.records) >= 1 - messages = [r.message for r in caplog.records] - combined = ' '.join(messages) - assert 'deprecated' in combined.lower() - - -# --------------------------------------------------------------------------- -# __repr__ -# --------------------------------------------------------------------------- - -def test_repr(): - """Verify __repr__ returns the expected format.""" - obj = ObjBase('my_name') - r = repr(obj) - assert "ObjBase" in r - assert "my_name" in r - - -# --------------------------------------------------------------------------- -# __setattr__ with interface bindings regeneration -# --------------------------------------------------------------------------- - -def test_setattr_with_interface_calls_generate_bindings(): - """When replacing an attribute that has an interface, generate_bindings is called.""" - from unittest.mock import Mock - - class A(ObjBase): - a: ClassVar[Parameter] - - def __init__(self, a: Parameter): - super().__init__('A', a=a) - - p1 = Parameter('a', 1.0) - a = A(p1) - - # Attach a mock interface - mock_iface = Mock() - a.interface = mock_iface - - # Replace the parameter β€” should trigger generate_bindings via __setattr__ - p2 = Parameter('a', 2.0) - a.a = p2 - - mock_iface.generate_bindings.assert_called() - - -# --------------------------------------------------------------------------- -# __setattr__ without annotation (the else branch) -# --------------------------------------------------------------------------- - -def test_setattr_without_annotation(): - """Setting a non-annotated BasedBase/DescriptorBase attribute updates graph.""" - - class A(ObjBase): - def __init__(self, p: Parameter): - super().__init__('A', p=p) - - p1 = Parameter('p', 1.0) - a = A(p1) - - graph = global_object.map - edges_before = set(graph.get_edges(a)) - - # Replace the parameter with a new one - p2 = Parameter('p', 2.0) - a.p = p2 - - edges_after = set(graph.get_edges(a)) - assert edges_before != edges_after - assert p2.unique_name in edges_after - assert p1.unique_name not in edges_after - - -# --------------------------------------------------------------------------- -# __setter with DescriptorBase path -# --------------------------------------------------------------------------- - -def test_setter_sets_descriptor_value(): - """When setting a Descriptor via the logged property, the descriptor's value is updated.""" - from easyscience import DescriptorNumber - - d = DescriptorNumber('d1', 0.5) - obj = ObjBase('test', d1=d) - obj.d1 = 3.14 - assert obj.d1.value == 3.14 diff --git a/tests/unit/legacy/test_xml.py b/tests/unit/legacy/test_xml.py deleted file mode 100644 index b1990ec8..00000000 --- a/tests/unit/legacy/test_xml.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - diff --git a/tests/unit/models/test_polynomial.py b/tests/unit/models/test_polynomial.py deleted file mode 100644 index 5376d03e..00000000 --- a/tests/unit/models/test_polynomial.py +++ /dev/null @@ -1,192 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import numpy as np -import pytest - -from easyscience import global_object -from easyscience.base_classes import CollectionBase -from easyscience.models.polynomial import Polynomial -from easyscience.variable import Parameter - - -@pytest.fixture -def clear(): - """Clear global object map before each test.""" - global_object.map._clear() - - -poly_test_cases = ( - (1.0,), - ( - 1.0, - 2.0, - ), - (1.0, 2.0, 3.0), - (-1.0, -2.0, -3.0), - (0.72, 6.48, -0.48), -) - - -@pytest.mark.parametrize('coo', poly_test_cases) -def test_Polynomial_pars(clear, coo): - poly = Polynomial(coefficients=coo) - - vals = {coo.value for coo in poly.coefficients} - assert len(vals.difference(set(coo))) == 0 - - x = np.linspace(0, 10, 100) - y = np.polyval(coo, x) - assert np.allclose(poly(x), y) - - -def test_Polynomial_default_initialization(clear): - """Test Polynomial with no coefficients.""" - poly = Polynomial(name='test_poly') - - assert poly.name == 'test_poly' - assert len(poly.coefficients) == 0 - - # Test that calling the polynomial with empty coefficients works - x = np.array([1, 2, 3]) - result = poly(x) - assert len(result) == len(x) - - -def test_Polynomial_with_Parameter_objects(clear): - """Test Polynomial with Parameter objects as coefficients.""" - p0 = Parameter('c0', value=1.0) - p1 = Parameter('c1', value=2.0) - p2 = Parameter('c2', value=3.0) - - poly = Polynomial(coefficients=[p0, p1, p2]) - - assert len(poly.coefficients) == 3 - assert poly.coefficients[0].name == 'c0' - assert poly.coefficients[1].name == 'c1' - assert poly.coefficients[2].name == 'c2' - assert poly.coefficients[0].value == 1.0 - assert poly.coefficients[1].value == 2.0 - assert poly.coefficients[2].value == 3.0 - - # Test evaluation - coefficients passed directly to polyval - # polyval([1.0, 2.0, 3.0], x) = 1.0*x^2 + 2.0*x + 3.0 - x = np.array([0, 1, 2]) - expected = np.polyval([1.0, 2.0, 3.0], x) - assert np.allclose(poly(x), expected) - - -def test_Polynomial_with_mixed_coefficients(clear): - """Test Polynomial with mixed float and Parameter coefficients.""" - p0 = Parameter('c0', value=5.0) - - poly = Polynomial(coefficients=[p0, 2.0, 1.0]) - - assert len(poly.coefficients) == 3 - assert poly.coefficients[0].name == 'c0' - assert poly.coefficients[1].name == 'c1' - assert poly.coefficients[2].name == 'c2' - - # polyval([5.0, 2.0, 1.0], x) = 5.0*x^2 + 2.0*x + 1.0 - x = np.array([1, 2, 3]) - expected = np.polyval([5.0, 2.0, 1.0], x) - assert np.allclose(poly(x), expected) - - -def test_Polynomial_with_CollectionBase(clear): - """Test Polynomial initialized with a CollectionBase.""" - collection = CollectionBase('coeffs') - collection.append(Parameter('c0', value=1.0)) - collection.append(Parameter('c1', value=2.0)) - collection.append(Parameter('c2', value=3.0)) - - poly = Polynomial(coefficients=collection) - - assert poly.coefficients is collection - assert len(poly.coefficients) == 3 - - # polyval([1.0, 2.0, 3.0], x) = 1.0*x^2 + 2.0*x + 3.0 - x = np.array([0, 1, 2]) - expected = np.polyval([1.0, 2.0, 3.0], x) - assert np.allclose(poly(x), expected) - - -def test_Polynomial_invalid_coefficient_type(clear): - """Test that invalid coefficient types raise TypeError.""" - with pytest.raises(TypeError, match='Coefficients must be floats or Parameters'): - Polynomial(coefficients=[1.0, 'invalid', 3.0]) - - with pytest.raises(TypeError, match='Coefficients must be floats or Parameters'): - Polynomial(coefficients=[1, 2, 3]) # integers, not floats - - -def test_Polynomial_invalid_coefficients_type(clear): - """Test that invalid coefficients argument type raises TypeError.""" - # String is iterable, so it will iterate over characters and fail with first error - with pytest.raises(TypeError, match='Coefficients must be floats or Parameters'): - Polynomial(coefficients='invalid') - - # Integer is not iterable, so it will fail with second error - with pytest.raises(TypeError, match='coefficients must be a list or a CollectionBase'): - Polynomial(coefficients=42) - - -def test_Polynomial_repr_no_coefficients(clear): - """Test __repr__ with no coefficients.""" - poly = Polynomial(name='empty') - - repr_str = repr(poly) - assert 'Polynomial(empty, )' == repr_str - - -def test_Polynomial_repr_one_coefficient(clear): - """Test __repr__ with one coefficient.""" - poly = Polynomial(coefficients=[5.0]) - - repr_str = repr(poly) - assert 'Polynomial(polynomial, 5.0)' == repr_str - - -def test_Polynomial_repr_two_coefficients(clear): - """Test __repr__ with two coefficients.""" - poly = Polynomial(coefficients=[3.0, 2.0]) - - repr_str = repr(poly) - assert 'Polynomial(polynomial, 2.0x + 3.0)' == repr_str - - -def test_Polynomial_repr_three_coefficients(clear): - """Test __repr__ with three coefficients.""" - poly = Polynomial(coefficients=[1.0, 2.0, 3.0]) - - repr_str = repr(poly) - assert 'Polynomial(polynomial, 3.0x^2 + 2.0x + 1.0)' == repr_str - - -def test_Polynomial_repr_with_zero_coefficients(clear): - """Test __repr__ with some zero coefficients.""" - poly = Polynomial(coefficients=[1.0, 0.0, 3.0, 0.0, 5.0]) - - repr_str = repr(poly) - # Zero coefficients for higher powers should be excluded - assert 'Polynomial(polynomial, 5.0x^4 + 3.0x^2 + 0.0x + 1.0)' == repr_str - - -def test_Polynomial_repr_negative_coefficients(clear): - """Test __repr__ with negative coefficients.""" - poly = Polynomial(coefficients=[-1.0, -2.0, -3.0]) - - repr_str = repr(poly) - assert 'Polynomial(polynomial, -3.0x^2 + -2.0x + -1.0)' == repr_str - - -def test_Polynomial_call_with_args_kwargs(clear): - """Test that __call__ accepts *args and **kwargs.""" - poly = Polynomial(coefficients=[1.0, 2.0, 3.0]) - - x = np.array([0, 1, 2]) - # These additional args/kwargs should be ignored - result = poly(x, 'extra_arg', extra_kwarg='value') - # polyval([1.0, 2.0, 3.0], x) = 1.0*x^2 + 2.0*x + 3.0 - expected = np.polyval([1.0, 2.0, 3.0], x) - assert np.allclose(result, expected) diff --git a/tests/unit/variable/test_descriptor_any_type.py b/tests/unit/variable/test_descriptor_any_type.py deleted file mode 100644 index 5dc28b58..00000000 --- a/tests/unit/variable/test_descriptor_any_type.py +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import numpy as np -import pytest - -from easyscience import global_object -from easyscience.variable import DescriptorAnyType - - -class TestDescriptorAnyType: - @pytest.fixture - def descriptor(self): - descriptor = DescriptorAnyType( - name='name', - value='string', - description='description', - url='url', - display_name='display_name', - parent=None, - ) - return descriptor - - @pytest.fixture - def clear(self): - global_object.map._clear() - - def test_init(self, descriptor: DescriptorAnyType): - # When Then Expect - assert descriptor._value == 'string' - - # From super - assert descriptor._name == 'name' - assert descriptor._description == 'description' - assert descriptor._url == 'url' - assert descriptor._display_name == 'display_name' - - def test_value(self, descriptor: DescriptorAnyType): - # When Then Expect - assert descriptor.value == 'string' - - @pytest.mark.parametrize( - 'value', [True, 'new_string', 1.0, np.array([1, 2, 3]), {'key': 'value'}] - ) - def test_set_value(self, descriptor: DescriptorAnyType, value): - # When Then - descriptor.value = value - - # Expect - if isinstance(value, np.ndarray): - assert np.array_equal(descriptor._value, value) - else: - assert descriptor._value == value - - @pytest.mark.parametrize( - 'value, expected', - [ - (True, 'True'), - ('new_string', "'new_string'"), - (1.0, '1.0'), - (np.array([1, 2, 3]), 'array([1, 2, 3])'), - ({'key': 'value'}, "{'key': 'value'}"), - ], - ) - def test_repr(self, descriptor: DescriptorAnyType, value, expected): - # Set the descriptor value - descriptor.value = value - - # When Then - repr_str = str(descriptor) - - print(repr_str) - - # Expect - assert repr_str == f"" - - def test_copy(self, descriptor: DescriptorAnyType): - # When Then - descriptor_copy = descriptor.__copy__() - - # Expect - assert type(descriptor_copy) == DescriptorAnyType - assert descriptor_copy._value == descriptor._value diff --git a/tests/unit/variable/test_descriptor_array.py b/tests/unit/variable/test_descriptor_array.py index 73bb0857..a7520c63 100644 --- a/tests/unit/variable/test_descriptor_array.py +++ b/tests/unit/variable/test_descriptor_array.py @@ -16,34 +16,30 @@ class TestDescriptorArray: @pytest.fixture def descriptor(self): descriptor = DescriptorArray( - name='name', value=[[1.0, 2.0], [3.0, 4.0]], unit='m', variance=[[0.1, 0.2], [0.3, 0.4]], description='description', url='url', display_name='display_name', - parent=None, ) return descriptor @pytest.fixture def descriptor_dimensionless(self): descriptor = DescriptorArray( - name='name', value=[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], unit='dimensionless', variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], description='description', url='url', display_name='display_name', - parent=None, ) return descriptor @pytest.fixture def clear(self): - global_object.map._clear() + global_object.registry.clear() def test_init(self, descriptor: DescriptorArray): # When Then Expect @@ -52,7 +48,6 @@ def test_init(self, descriptor: DescriptorArray): assert np.array_equal(descriptor._array.variances, np.array([[0.1, 0.2], [0.3, 0.4]])) # From super - assert descriptor._name == 'name' assert descriptor._description == 'description' assert descriptor._url == 'url' assert descriptor._display_name == 'display_name' @@ -60,14 +55,12 @@ def test_init(self, descriptor: DescriptorArray): def test_init_sc_unit(self): # When Then descriptor = DescriptorArray( - name='name', value=[[1.0, 2.0], [3.0, 4.0]], unit=sc.units.Unit('m'), variance=[[0.1, 0.2], [0.3, 0.4]], description='description', url='url', display_name='display_name', - parent=None, ) # Expect @@ -79,14 +72,12 @@ def test_init_sc_unit_unknown(self): # When Then Expect with pytest.raises(UnitError): DescriptorArray( - name='name', value=[[1.0, 2.0], [3.0, 4.0]], unit='unknown', variance=[[0.1, 0.2], [0.3, 0.4]], description='description', url='url', display_name='display_name', - parent=None, ) @pytest.mark.parametrize('value', [True, 'string']) @@ -96,14 +87,12 @@ def test_init_value_type_exception(self, value): # Then Expect with pytest.raises(TypeError): DescriptorArray( - name='name', value=value, unit='m', variance=[[0.1, 0.2], [0.3, 0.4]], description='description', url='url', display_name='display_name', - parent=None, ) def test_init_variance_exception(self): @@ -112,14 +101,12 @@ def test_init_variance_exception(self): # Then Expect with pytest.raises(ValueError): DescriptorArray( - name='name', value=[[1.0, 2.0], [3.0, 4.0]], unit='m', variance=variance, description='description', url='url', display_name='display_name', - parent=None, ) # test from_scipp @@ -127,7 +114,7 @@ def test_from_scipp(self): # When full_value = sc.array(dims=['row', 'column'], values=[[1, 2], [3, 4]], unit='m') # Then - descriptor = DescriptorArray.from_scipp(name='name', full_value=full_value) + descriptor = DescriptorArray.from_scipp(display_name='name', full_value=full_value) # Expect assert np.array_equal(descriptor._array.values, [[1, 2], [3, 4]]) @@ -185,6 +172,14 @@ def test_set_variance(self, descriptor: DescriptorArray): assert np.array_equal(descriptor.variance, np.array([[0.2, 0.3], [0.4, 0.5]])) assert np.array_equal(descriptor.error, np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]]))) + def test_set_variance_none(self, descriptor: DescriptorArray): + # When Then + descriptor.variance = None + + # Expect + assert descriptor.variance is None + assert descriptor.error is None + def test_error(self, descriptor: DescriptorArray): # When Then Expect assert np.array_equal(descriptor.error, np.sqrt(np.array([[0.1, 0.2], [0.3, 0.4]]))) @@ -213,7 +208,7 @@ def test_repr(self, descriptor: DescriptorArray): # Expect assert ( repr_str - == "" + == "" ) def test_copy(self, descriptor: DescriptorArray): @@ -232,7 +227,9 @@ def test_copy(self, descriptor: DescriptorArray): ) def test_base_unit(self, unit_string, expected): # When - descriptor = DescriptorArray(name='name', value=[[1.0, 2.0], [3.0, 4.0]], unit=unit_string) + descriptor = DescriptorArray( + display_name='name', value=[[1.0, 2.0], [3.0, 4.0]], unit=unit_string + ) # Then base_unit = descriptor._base_unit() @@ -244,36 +241,47 @@ def test_base_unit(self, unit_string, expected): 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test + name', [[3.0, 4.0], [5.0, 6.0]], 'm', [[0.11, 0.21], [0.31, 0.41]] + [[3.0, 4.0], [5.0, 6.0]], + unit='m', + variance=[[0.11, 0.21], [0.31, 0.41]], + display_name='test + name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test + name', [[1.01, 2.01], [3.01, 4.01]], - 'm', - [[0.1010, 0.2010], [0.3010, 0.4010]], + unit='m', + variance=[[0.1010, 0.2010], [0.3010, 0.4010]], + display_name='test + name', ), True, ), ( - DescriptorArray('test', [[2.0, 3.0], [4.0, -5.0]], 'cm', [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray( - 'test + name', + [[2.0, 3.0], [4.0, -5.0]], + unit='cm', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', + ), + DescriptorArray( [[1.02, 2.03], [3.04, 3.95]], - 'm', - [[0.1001, 0.2002], [0.3003, 0.4004]], + unit='m', + variance=[[0.1001, 0.2002], [0.3003, 0.4004]], + display_name='test + name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm'), + DescriptorArray([[2, 3], [4, -5]], unit='cm', display_name='test'), DescriptorArray( - 'test + name', [[1.02, 2.03], [3.04, 3.95]], 'm', [[0.1, 0.2], [0.3, 0.4]] + [[1.02, 2.03], [3.04, 3.95]], + unit='m', + variance=[[0.1, 0.2], [0.3, 0.4]], + display_name='test + name', ), False, ), @@ -296,7 +304,6 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn result = descriptor + test # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -308,19 +315,19 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ( 1, DescriptorArray( - 'test', [[2.0, 3.0], [4.0, 5.0], [6.0, 7.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ], @@ -341,39 +348,47 @@ def test_addition_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test + name', [[3.0, 4.0], [5.0, 6.0]], 'm', [[0.11, 0.21], [0.31, 0.41]] + [[3.0, 4.0], [5.0, 6.0]], + unit='m', + variance=[[0.11, 0.21], [0.31, 0.41]], + display_name='test + name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test + name', [[101.0, 201.0], [301.0, 401.0]], - 'cm', - [[1010.0, 2010.0], [3010.0, 4010.0]], + unit='cm', + variance=[[1010.0, 2010.0], [3010.0, 4010.0]], + display_name='test + name', ), True, ), ( - DescriptorArray('test', [[2.0, 3.0], [4.0, -5.0]], 'cm', [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray( - 'test + name', + [[2.0, 3.0], [4.0, -5.0]], + unit='cm', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', + ), + DescriptorArray( [[102.0, 203.0], [304.0, 395.0]], - 'cm', - [[1001.0, 2002.0], [3003.0, 4004.0]], + unit='cm', + variance=[[1001.0, 2002.0], [3003.0, 4004.0]], + display_name='test + name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm'), + DescriptorArray([[2, 3], [4, -5]], unit='cm', display_name='test'), DescriptorArray( - 'test + name', [[102.0, 203.0], [304.0, 395.0]], - 'cm', - [[1000.0, 2000.0], [3000.0, 4000.0]], + unit='cm', + variance=[[1000.0, 2000.0], [3000.0, 4000.0]], + display_name='test + name', ), False, ), @@ -396,7 +411,6 @@ def test_reverse_addition(self, descriptor: DescriptorArray, test, expected, rai result = test + descriptor # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -408,19 +422,19 @@ def test_reverse_addition(self, descriptor: DescriptorArray, test, expected, rai ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ( 1, DescriptorArray( - 'test', [[2.0, 3.0], [4.0, 5.0], [6.0, 7.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ], @@ -441,39 +455,47 @@ def test_reverse_addition_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test + name', [[-1.0, 0.0], [1.0, 2.0]], 'm', [[0.11, 0.21], [0.31, 0.41]] + [[-1.0, 0.0], [1.0, 2.0]], + unit='m', + variance=[[0.11, 0.21], [0.31, 0.41]], + display_name='test + name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test + name', [[0.99, 1.99], [2.99, 3.99]], - 'm', - [[0.1010, 0.2010], [0.3010, 0.4010]], + unit='m', + variance=[[0.1010, 0.2010], [0.3010, 0.4010]], + display_name='test + name', ), True, ), ( - DescriptorArray('test', [[2.0, 3.0], [4.0, -5.0]], 'cm', [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray( - 'test + name', + [[2.0, 3.0], [4.0, -5.0]], + unit='cm', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', + ), + DescriptorArray( [[0.98, 1.97], [2.96, 4.05]], - 'm', - [[0.1001, 0.2002], [0.3003, 0.4004]], + unit='m', + variance=[[0.1001, 0.2002], [0.3003, 0.4004]], + display_name='test + name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm'), + DescriptorArray([[2, 3], [4, -5]], unit='cm', display_name='test'), DescriptorArray( - 'test + name', [[0.98, 1.97], [2.96, 4.05]], - 'm', - [[0.100, 0.200], [0.300, 0.400]], + unit='m', + variance=[[0.100, 0.200], [0.300, 0.400]], + display_name='test + name', ), False, ), @@ -496,7 +518,6 @@ def test_subtraction(self, descriptor: DescriptorArray, test, expected, raises_w result = descriptor - test # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -508,19 +529,19 @@ def test_subtraction(self, descriptor: DescriptorArray, test, expected, raises_w ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[-1.0, -1.0], [-1.0, 9.0], [-1, 14.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ( 1, DescriptorArray( - 'test', [[0.0, 1.0], [2.0, 3.0], [4.0, 5.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ], @@ -541,39 +562,47 @@ def test_subtraction_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test + name', [[1.0, 0.0], [-1.0, -2.0]], 'm', [[0.11, 0.21], [0.31, 0.41]] + [[1.0, 0.0], [-1.0, -2.0]], + unit='m', + variance=[[0.11, 0.21], [0.31, 0.41]], + display_name='test + name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test + name', [[-99.0, -199.0], [-299.0, -399.0]], - 'cm', - [[1010.0, 2010.0], [3010.0, 4010.0]], + unit='cm', + variance=[[1010.0, 2010.0], [3010.0, 4010.0]], + display_name='test + name', ), True, ), ( - DescriptorArray('test', [[2.0, 3.0], [4.0, -5.0]], 'cm', [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray( - 'test + name', + [[2.0, 3.0], [4.0, -5.0]], + unit='cm', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', + ), + DescriptorArray( [[-98.0, -197.0], [-296.0, -405.0]], - 'cm', - [[1001.0, 2002.0], [3003.0, 4004.0]], + unit='cm', + variance=[[1001.0, 2002.0], [3003.0, 4004.0]], + display_name='test + name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm'), + DescriptorArray([[2, 3], [4, -5]], unit='cm', display_name='test'), DescriptorArray( - 'test + name', [[-98.0, -197.0], [-296.0, -405.0]], - 'cm', - [[1000.0, 2000.0], [3000.0, 4000.0]], + unit='cm', + variance=[[1000.0, 2000.0], [3000.0, 4000.0]], + display_name='test + name', ), False, ), @@ -598,7 +627,6 @@ def test_reverse_subtraction( result = test - descriptor # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -610,19 +638,19 @@ def test_reverse_subtraction( ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[1.0, 1.0], [1.0, -9.0], [1.0, -14.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ( 1, DescriptorArray( - 'test', [[0.0, -1.0], [-2.0, -3.0], [-4.0, -5.0]], - 'dimensionless', - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + unit='dimensionless', + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + display_name='test', ), ), ], @@ -643,69 +671,80 @@ def test_reverse_subtraction_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test * name', [[2.0, 4.0], [6.0, 8.0]], 'm^2', [[0.41, 0.84], [1.29, 1.76]] + [[2.0, 4.0], [6.0, 8.0]], + unit='m^2', + variance=[[0.41, 0.84], [1.29, 1.76]], + display_name='test * name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test * name', [[0.01, 0.02], [0.03, 0.04]], - 'm^2', - [[0.00101, 0.00402], [0.00903, 0.01604]], + unit='m^2', + variance=[[0.00101, 0.00402], [0.00903, 0.01604]], + display_name='test * name', ), True, ), ( - DescriptorNumber('test', 1, 'kg', 10), + DescriptorNumber(1, unit='kg', variance=10, display_name='test'), DescriptorArray( - 'test * name', [[1.0, 2.0], [3.0, 4.0]], 'kg*m', [[10.1, 40.2], [90.3, 160.4]] + [[1.0, 2.0], [3.0, 4.0]], + unit='kg*m', + variance=[[10.1, 40.2], [90.3, 160.4]], + display_name='test * name', ), True, ), ( - DescriptorArray('test', [[2.0, 3.0], [4.0, -5.0]], 'cm', [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray( - 'test * name', + [[2.0, 3.0], [4.0, -5.0]], + unit='cm', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', + ), + DescriptorArray( [[0.02, 0.06], [0.12, -0.2]], - 'm^2', - [[0.00014, 0.00098], [0.00318, 0.0074]], + unit='m^2', + variance=[[0.00014, 0.00098], [0.00318, 0.0074]], + display_name='test * name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm'), + DescriptorArray([[2, 3], [4, -5]], unit='cm', display_name='test'), DescriptorArray( - 'test * name', [[0.02, 0.06], [0.12, -0.2]], - 'm^2', - [ + unit='m^2', + variance=[ [0.1 * 2**2 * 1e-4, 0.2 * 3**2 * 1e-4], [0.3 * 4**2 * 1e-4, 0.4 * 5**2 * 1e-4], ], + display_name='test * name', ), False, ), ( [[2.0, 3.0], [4.0, -5.0]], DescriptorArray( - 'test * name', [[2.0, 6.0], [12.0, -20.0]], - 'm', - [[0.1 * 2**2, 0.2 * 3**2], [0.3 * 4**2, 0.4 * 5**2]], + unit='m', + variance=[[0.1 * 2**2, 0.2 * 3**2], [0.3 * 4**2, 0.4 * 5**2]], + display_name='test * name', ), False, ), ( 2.0, DescriptorArray( - 'test * name', [[2.0, 4.0], [6.0, 8.0]], - 'm', - [[0.1 * 2**2, 0.2 * 2**2], [0.3 * 2**2, 0.4 * 2**2]], + unit='m', + variance=[[0.1 * 2**2, 0.2 * 2**2], [0.3 * 2**2, 0.4 * 2**2]], + display_name='test * name', ), False, ), @@ -731,7 +770,6 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise result = descriptor * test # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -743,19 +781,19 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[2.0, 6.0], [12.0, -20.0], [30.0, -48.0]], - 'dimensionless', - [[0.4, 1.8], [4.8, 10.0], [18.0, 38.4]], + unit='dimensionless', + variance=[[0.4, 1.8], [4.8, 10.0], [18.0, 38.4]], + display_name='test', ), ), ( 1.5, DescriptorArray( - 'test', [[1.5, 3.0], [4.5, 6.0], [7.5, 9.0]], - 'dimensionless', - [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + unit='dimensionless', + variance=[[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + display_name='test', ), ), ], @@ -776,66 +814,80 @@ def test_multiplication_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test * name', [[2.0, 4.0], [6.0, 8.0]], 'm^2', [[0.41, 0.84], [1.29, 1.76]] + [[2.0, 4.0], [6.0, 8.0]], + unit='m^2', + variance=[[0.41, 0.84], [1.29, 1.76]], + display_name='test * name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test * name', [[100.0, 200.0], [300.0, 400.0]], - 'cm^2', - [[101000.0, 402000.0], [903000.0, 1604000.0]], + unit='cm^2', + variance=[[101000.0, 402000.0], [903000.0, 1604000.0]], + display_name='test * name', ), True, ), ( - DescriptorNumber('test', 1, 'kg', 10), + DescriptorNumber(1, unit='kg', variance=10, display_name='test'), DescriptorArray( - 'test * name', [[1.0, 2.0], [3.0, 4.0]], 'kg*m', [[10.1, 40.2], [90.3, 160.4]] + [[1.0, 2.0], [3.0, 4.0]], + unit='kg*m', + variance=[[10.1, 40.2], [90.3, 160.4]], + display_name='test * name', ), True, ), ( - DescriptorArray('test', [[2.0, 3.0], [4.0, -5.0]], 'cm', [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray( - 'test * name', + [[2.0, 3.0], [4.0, -5.0]], + unit='cm', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', + ), + DescriptorArray( [[200.0, 600.0], [1200.0, -2000.0]], - 'cm^2', - [[14000.0, 98000.0], [318000.0, 740000.0]], + unit='cm^2', + variance=[[14000.0, 98000.0], [318000.0, 740000.0]], + display_name='test * name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm'), + DescriptorArray([[2, 3], [4, -5]], unit='cm', display_name='test'), DescriptorArray( - 'test * name', [[200.0, 600.0], [1200.0, -2000.0]], - 'cm^2', - [[0.1 * 2**2 * 1e4, 0.2 * 3**2 * 1e4], [0.3 * 4**2 * 1e4, 0.4 * 5**2 * 1e4]], + unit='cm^2', + variance=[ + [0.1 * 2**2 * 1e4, 0.2 * 3**2 * 1e4], + [0.3 * 4**2 * 1e4, 0.4 * 5**2 * 1e4], + ], + display_name='test * name', ), False, ), ( [[2.0, 3.0], [4.0, -5.0]], DescriptorArray( - 'test * name', [[2.0, 6.0], [12.0, -20.0]], - 'm', - [[0.1 * 2**2, 0.2 * 3**2], [0.3 * 4**2, 0.4 * 5**2]], + unit='m', + variance=[[0.1 * 2**2, 0.2 * 3**2], [0.3 * 4**2, 0.4 * 5**2]], + display_name='test * name', ), False, ), ( 2.0, DescriptorArray( - 'test * name', [[2.0, 4.0], [6.0, 8.0]], - 'm', - [[0.1 * 2**2, 0.2 * 2**2], [0.3 * 2**2, 0.4 * 2**2]], + unit='m', + variance=[[0.1 * 2**2, 0.2 * 2**2], [0.3 * 2**2, 0.4 * 2**2]], + display_name='test * name', ), False, ), @@ -863,7 +915,6 @@ def test_reverse_multiplication( result = test * descriptor # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -875,19 +926,19 @@ def test_reverse_multiplication( ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[2.0, 6.0], [12.0, -20.0], [30.0, -48.0]], - 'dimensionless', - [[0.4, 1.8], [4.8, 10.0], [18.0, 38.4]], + unit='dimensionless', + variance=[[0.4, 1.8], [4.8, 10.0], [18.0, 38.4]], + display_name='test', ), ), ( 1.5, DescriptorArray( - 'test', [[1.5, 3.0], [4.5, 6.0], [7.5, 9.0]], - 'dimensionless', - [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + unit='dimensionless', + variance=[[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + display_name='test', ), ), ], @@ -908,12 +959,11 @@ def test_reverse_multiplication_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'name / test', [[1.0 / 2.0, 2.0 / 2.0], [3.0 / 2.0, 4.0 / 2.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [ (0.1 + 0.01 * 1.0**2 / 2.0**2) / 2.0**2, (0.2 + 0.01 * 2.0**2 / 2.0**2) / 2.0**2, @@ -923,16 +973,16 @@ def test_reverse_multiplication_dimensionless( (0.4 + 0.01 * 4.0**2 / 2.0**2) / 2.0**2, ], ], + display_name='name / test', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'name / test', [[100.0, 200.0], [300.0, 400.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [ (0.1 + 10 * 1.0**2 / 1.0**2) / 1.0**2 * 1e4, (0.2 + 10 * 2.0**2 / 1.0**2) / 1.0**2 * 1e4, @@ -942,16 +992,16 @@ def test_reverse_multiplication_dimensionless( (0.4 + 10 * 4.0**2 / 1.0**2) / 1.0**2 * 1e4, ], ], + display_name='name / test', ), True, ), ( - DescriptorNumber('test', 1, 'kg', 10), + DescriptorNumber(1, unit='kg', variance=10, display_name='test'), DescriptorArray( - 'name / test', [[1.0, 2.0], [3.0, 4.0]], - 'm/kg', - [ + unit='m/kg', + variance=[ [ (0.1 + 10 * 1.0**2 / 1.0**2) / 1.0**2, (0.2 + 10 * 2.0**2 / 1.0**2) / 1.0**2, @@ -961,18 +1011,21 @@ def test_reverse_multiplication_dimensionless( (0.4 + 10 * 4.0**2 / 1.0**2) / 1.0**2, ], ], + display_name='name / test', ), True, ), ( DescriptorArray( - 'test', [[2.0, 3.0], [4.0, -5.0]], 'cm^2', [[1.0, 2.0], [3.0, 4.0]] + [[2.0, 3.0], [4.0, -5.0]], + unit='cm^2', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', ), DescriptorArray( - 'name / test', [[1 / 2 * 1e4, 2 / 3 * 1e4], [3.0 / 4.0 * 1e4, -4.0 / 5.0 * 1e4]], - '1/m', - [ + unit='1/m', + variance=[ [ (0.1 + 1.0 * 1.0**2 / 2.0**2) / 2.0**2 * 1e8, (0.2 + 2.0 * 2.0**2 / 3.0**2) / 3.0**2 * 1e8, @@ -982,39 +1035,40 @@ def test_reverse_multiplication_dimensionless( (0.4 + 4.0 * 4.0**2 / 5.0**2) / 5.0**2 * 1e8, ], ], + display_name='name / test', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm^2'), + DescriptorArray([[2, 3], [4, -5]], unit='cm^2', display_name='test'), DescriptorArray( - 'name / test', [[1 / 2 * 1e4, 2 / 3 * 1e4], [3.0 / 4.0 * 1e4, -4.0 / 5.0 * 1e4]], - '1/m', - [ + unit='1/m', + variance=[ [(0.1) / 2.0**2 * 1e8, (0.2) / 3.0**2 * 1e8], [(0.3) / 4.0**2 * 1e8, (0.4) / 5.0**2 * 1e8], ], + display_name='name / test', ), False, ), ( [[2.0, 3.0], [4.0, -5.0]], DescriptorArray( - 'name / name', [[0.5, 2.0 / 3.0], [3.0 / 4.0, -4 / 5]], - 'm', - [[0.1 / 2**2, 0.2 / 3.0**2], [0.3 / 4**2, 0.4 / 5.0**2]], + unit='m', + variance=[[0.1 / 2**2, 0.2 / 3.0**2], [0.3 / 4**2, 0.4 / 5.0**2]], + display_name='name / name', ), False, ), ( 2.0, DescriptorArray( - 'name / test', [[0.5, 1.0], [3.0 / 2.0, 2.0]], - 'm', - [[0.1 / 2.0**2, 0.2 / 2.0**2], [0.3 / 2.0**2, 0.4 / 2.0**2]], + unit='m', + variance=[[0.1 / 2.0**2, 0.2 / 2.0**2], [0.3 / 2.0**2, 0.4 / 2.0**2]], + display_name='name / test', ), False, ), @@ -1040,7 +1094,6 @@ def test_division(self, descriptor: DescriptorArray, test, expected, raises_warn result = descriptor / test # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.allclose(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -1052,27 +1105,27 @@ def test_division(self, descriptor: DescriptorArray, test, expected, raises_warn ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[1.0 / 2.0, 2.0 / 3.0], [3.0 / 4.0, -4.0 / 5.0], [5.0 / 6.0, -6.0 / 8.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [0.1 / 2.0**2, 0.2 / 3.0**2], [0.3 / 4.0**2, 0.4 / 5.0**2], [0.5 / 6.0**2, 0.6 / 8.0**2], ], + display_name='test', ), ), ( 2, DescriptorArray( - 'test', [[1.0 / 2.0, 2.0 / 2.0], [3.0 / 2.0, 4.0 / 2.0], [5.0 / 2.0, 6.0 / 2.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [0.1 / 2.0**2, 0.2 / 2.0**2], [0.3 / 2.0**2, 0.4 / 2.0**2], [0.5 / 2.0**2, 0.6 / 2.0**2], ], + display_name='test', ), ), ], @@ -1093,75 +1146,77 @@ def test_division_dimensionless( 'test, expected, raises_warning', [ ( - DescriptorNumber('test', 2, 'm', 0.01), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), DescriptorArray( - 'test / name', [[2.0, 1.0], [2.0 / 3.0, 0.5]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [0.41, 0.0525], [ (0.01 + 0.3 * 2**2 / 3.0**2) / 3.0**2, (0.01 + 0.4 * 2**2 / 4.0**2) / 4.0**2, ], ], + display_name='test / name', ), True, ), ( - DescriptorNumber('test', 1, 'cm', 10), + DescriptorNumber(1, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test / name', [[1.0 / 100.0, 1.0 / 200.0], [1.0 / 300.0, 1.0 / 400.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [1.01e-3, (1e-3 + 0.2 * 0.01**2 / 2**2) / 2**2], [ (1e-3 + 0.3 * 0.01**2 / 3**2) / 3**2, (1e-3 + 0.4 * 0.01**2 / 4**2) / 4**2, ], ], + display_name='test / name', ), True, ), ( - DescriptorNumber('test', 1, 'kg', 10), + DescriptorNumber(1, unit='kg', variance=10, display_name='test'), DescriptorArray( - 'test / name', [[1.0, 0.5], [1.0 / 3.0, 0.25]], - 'kg/m', - [ + unit='kg/m', + variance=[ [10.1, (10 + 0.2 * 1 / 2**2) / 2**2], [(10 + 0.3 * 1 / 3**2) / 3**2, (10 + 0.4 * 1 / 4**2) / 4**2], ], + display_name='test / name', ), True, ), ( DescriptorArray( - 'test', [[2.0, 3.0], [4.0, -5.0]], 'cm^2', [[1.0, 2.0], [3.0, 4.0]] + [[2.0, 3.0], [4.0, -5.0]], + unit='cm^2', + variance=[[1.0, 2.0], [3.0, 4.0]], + display_name='test', ), DescriptorArray( - 'test / name', [[2e-4, 1.5e-4], [4.0 / 3.0 * 1e-4, -1.25e-4]], - 'm', - [ + unit='m', + variance=[ [1.4e-8, 6.125e-9], [ (3.0e-8 + 0.3 * (0.0004) ** 2 / 3**2) / 3**2, (4.0e-8 + 0.4 * (0.0005) ** 2 / 4**2) / 4**2, ], ], + display_name='test / name', ), False, ), ( - DescriptorArray('test', [[2, 3], [4, -5]], 'cm^2'), + DescriptorArray([[2, 3], [4, -5]], unit='cm^2', display_name='test'), DescriptorArray( - 'test / name', [[2e-4, 1.5e-4], [4.0 / 3.0 * 1e-4, -1.25e-4]], - 'm', - [ + unit='m', + variance=[ [ (0.1 * 2.0**2 / 1.0**2) / 1.0**2 * 1e-8, (0.2 * 3.0**2 / 2.0**2) / 2.0**2 * 1e-8, @@ -1171,32 +1226,33 @@ def test_division_dimensionless( (0.4 * 5.0**2 / 4.0**2) / 4.0**2 * 1e-8, ], ], + display_name='test / name', ), False, ), ( [[2.0, 3.0], [4.0, -5.0]], DescriptorArray( - 'test / name', [[2, 1.5], [4.0 / 3.0, -1.25]], - '1/m', - [ + unit='1/m', + variance=[ [0.1 * 2**2 / 1**4, 0.2 * 3.0**2 / 2.0**4], [0.3 * 4**2 / 3**4, 0.4 * 5.0**2 / 4.0**4], ], + display_name='test / name', ), False, ), ( 2.0, DescriptorArray( - 'test / name', [[2, 1.0], [2.0 / 3.0, 0.5]], - '1/m', - [ + unit='1/m', + variance=[ [0.1 * 2**2 / 1**4, 0.2 * 2.0**2 / 2.0**4], [0.3 * 2**2 / 3**4, 0.4 * 2.0**2 / 4.0**4], ], + display_name='test / name', ), False, ), @@ -1222,7 +1278,6 @@ def test_reverse_division(self, descriptor: DescriptorArray, test, expected, rai result = test / descriptor # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.allclose(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -1234,27 +1289,27 @@ def test_reverse_division(self, descriptor: DescriptorArray, test, expected, rai ( [[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray( - 'test', [[2.0 / 1.0, 3.0 / 2.0], [4.0 / 3.0, -5.0 / 4.0], [6.0 / 5.0, -8.0 / 6.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [0.1 * 2.0**2, 0.2 * 3.0**2 / 2**4], [0.3 * 4.0**2 / 3.0**4, 0.4 * 5.0**2 / 4**4], [0.5 * 6.0**2 / 5**4, 0.6 * 8.0**2 / 6**4], ], + display_name='test', ), ), ( 2, DescriptorArray( - 'test', [[2.0, 1.0], [2.0 / 3.0, 0.5], [2.0 / 5.0, 1.0 / 3.0]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [0.1 * 2.0**2, 0.2 / 2**2], [0.3 * 2**2 / 3**4, 0.4 * 2**2 / 4**4], [0.5 * 2**2 / 5**4, 0.6 * 2**2 / 6**4], ], + display_name='test', ), ), ], @@ -1276,12 +1331,12 @@ def test_reverse_division_dimensionless( [ [[2.0, 3.0], [4.0, -5.0], [6.0, 0.0]], 0.0, - DescriptorNumber('test', 0, 'cm', 10), + DescriptorNumber(0, unit='cm', variance=10, display_name='test'), DescriptorArray( - 'test', [[1.5, 0.0], [4.5, 6.0], [7.5, 9.0]], - 'dimensionless', - [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + unit='dimensionless', + variance=[[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + display_name='test', ), ], ids=['list', 'number', 'DescriptorNumber', 'DescriptorArray'], @@ -1293,10 +1348,10 @@ def test_division_exception(self, descriptor_dimensionless: DescriptorArray, tes # Also test reverse division where `self` is a DescriptorArray with a zero zero_descriptor = DescriptorArray( - 'test', [[1.5, 0.0], [4.5, 6.0], [7.5, 0.0]], - 'dimensionless', - [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + unit='dimensionless', + variance=[[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]], + display_name='test', ) with pytest.raises(ZeroDivisionError): test / zero_descriptor @@ -1305,39 +1360,39 @@ def test_division_exception(self, descriptor_dimensionless: DescriptorArray, tes 'test, expected', [ ( - DescriptorNumber('test', 2, 'dimensionless'), + DescriptorNumber(2, unit='dimensionless', display_name='test'), DescriptorArray( - 'test ** name', [[1.0, 4.0], [9.0, 16.0]], - 'm^2', - [[4 * 0.1 * 1, 4 * 0.2 * 2**2], [4 * 0.3 * 3**2, 4 * 0.4 * 4**2]], + unit='m^2', + variance=[[4 * 0.1 * 1, 4 * 0.2 * 2**2], [4 * 0.3 * 3**2, 4 * 0.4 * 4**2]], + display_name='test ** name', ), ), ( - DescriptorNumber('test', 3, 'dimensionless'), + DescriptorNumber(3, unit='dimensionless', display_name='test'), DescriptorArray( - 'test ** name', [[1.0, 8.0], [27, 64.0]], - 'm^3', - [[9 * 0.1, 9 * 0.2 * 2**4], [9 * 0.3 * 3**4, 9 * 0.4 * 4**4]], + unit='m^3', + variance=[[9 * 0.1, 9 * 0.2 * 2**4], [9 * 0.3 * 3**4, 9 * 0.4 * 4**4]], + display_name='test ** name', ), ), ( - DescriptorNumber('test', 0.0, 'dimensionless'), + DescriptorNumber(0.0, unit='dimensionless', display_name='test'), DescriptorArray( - 'test ** name', [[1.0, 1.0], [1.0, 1.0]], - 'dimensionless', - [[0.0, 0.0], [0.0, 0.0]], + unit='dimensionless', + variance=[[0.0, 0.0], [0.0, 0.0]], + display_name='test ** name', ), ), ( 0.0, DescriptorArray( - 'test ** name', [[1.0, 1.0], [1.0, 1.0]], - 'dimensionless', - [[0.0, 0.0], [0.0, 0.0]], + unit='dimensionless', + variance=[[0.0, 0.0], [0.0, 0.0]], + display_name='test ** name', ), ), ], @@ -1353,7 +1408,6 @@ def test_power(self, descriptor: DescriptorArray, test, expected): result = descriptor**test # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -1363,25 +1417,25 @@ def test_power(self, descriptor: DescriptorArray, test, expected): 'test, expected', [ ( - DescriptorNumber('test', 0.1, 'dimensionless'), + DescriptorNumber(0.1, unit='dimensionless', display_name='test'), DescriptorArray( - 'test ** name', [[1, 2**0.1], [3**0.1, 4**0.1], [5**0.1, 6**0.1]], - 'dimensionless', - [ + unit='dimensionless', + variance=[ [0.1**2 * 0.1 * 1, 0.1**2 * 0.2 * 2 ** (-1.8)], [0.1**2 * 0.3 * 3 ** (-1.8), 0.1**2 * 0.4 * 4 ** (-1.8)], [0.1**2 * 0.5 * 5 ** (-1.8), 0.1**2 * 0.6 * 6 ** (-1.8)], ], + display_name='test ** name', ), ), ( - DescriptorNumber('test', 2.0, 'dimensionless'), + DescriptorNumber(2.0, unit='dimensionless', display_name='test'), DescriptorArray( - 'test ** name', [[1.0, 4.0], [9.0, 16.0], [25.0, 36.0]], - 'dimensionless', - [[0.4, 3.2], [10.8, 25.6], [50.0, 86.4]], + unit='dimensionless', + variance=[[0.4, 3.2], [10.8, 25.6], [50.0, 86.4]], + display_name='test ** name', ), ), ], @@ -1392,7 +1446,6 @@ def test_power_dimensionless(self, descriptor_dimensionless: DescriptorArray, te result = descriptor_dimensionless**test # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.allclose(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -1401,13 +1454,16 @@ def test_power_dimensionless(self, descriptor_dimensionless: DescriptorArray, te @pytest.mark.parametrize( 'test, exception', [ - (DescriptorNumber('test', 2, 'm'), UnitError), - (DescriptorNumber('test', 2, 'dimensionless', 10), ValueError), - (DescriptorNumber('test', np.nan, 'dimensionless'), UnitError), - (DescriptorNumber('test', np.nan, 'dimensionless'), UnitError), - (DescriptorNumber('test', 1.5, 'dimensionless'), UnitError), + (DescriptorNumber(2, unit='m', display_name='test'), UnitError), + ( + DescriptorNumber(2, unit='dimensionless', variance=10, display_name='test'), + ValueError, + ), + (DescriptorNumber(np.nan, unit='dimensionless', display_name='test'), UnitError), + (DescriptorNumber(np.nan, unit='dimensionless', display_name='test'), UnitError), + (DescriptorNumber(1.5, unit='dimensionless', display_name='test'), UnitError), ( - DescriptorNumber('test', 0.5, 'dimensionless'), + DescriptorNumber(0.5, unit='dimensionless', display_name='test'), UnitError, ), # Square roots are not legal ], @@ -1430,7 +1486,10 @@ def test_power_exception(self, descriptor: DescriptorArray, test, exception): @pytest.mark.parametrize( 'test', - [DescriptorNumber('test', 2, 's'), DescriptorArray('test', [[1, 2], [3, 4]], 's')], + [ + DescriptorNumber(2, unit='s', display_name='test'), + DescriptorArray([[1, 2], [3, 4]], unit='s', display_name='test'), + ], ids=['add_array_to_unit', 'incompatible_units'], ) def test_addition_exception(self, descriptor: DescriptorArray, test): @@ -1442,7 +1501,10 @@ def test_addition_exception(self, descriptor: DescriptorArray, test): @pytest.mark.parametrize( 'test', - [DescriptorNumber('test', 2, 's'), DescriptorArray('test', [[1, 2], [3, 4]], 's')], + [ + DescriptorNumber(2, unit='s', display_name='test'), + DescriptorArray([[1, 2], [3, 4]], unit='s', display_name='test'), + ], ids=['add_array_to_unit', 'incompatible_units'], ) def test_sub_exception(self, descriptor: DescriptorArray, test): @@ -1474,17 +1536,14 @@ def test_negation(self, descriptor): # Expect expected = DescriptorArray( - name='name', value=[[-1.0, -2.0], [-3.0, -4.0]], unit='m', variance=[[0.1, 0.2], [0.3, 0.4]], description='description', url='url', display_name='display_name', - parent=None, ) assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -1493,14 +1552,12 @@ def test_negation(self, descriptor): def test_abs(self, descriptor): # When negated = DescriptorArray( - name='name', value=[[-1.0, -2.0], [-3.0, -4.0]], unit='m', variance=[[0.1, 0.2], [0.3, 0.4]], description='description', url='url', display_name='display_name', - parent=None, ) # Then @@ -1508,7 +1565,6 @@ def test_abs(self, descriptor): # Expect assert type(result) == DescriptorArray - assert result.name == result.unique_name assert np.array_equal(result.value, descriptor.value) assert result.unit == descriptor.unit assert np.allclose(result.variance, descriptor.variance) @@ -1519,40 +1575,55 @@ def test_abs(self, descriptor): [ ( DescriptorArray( - 'test + name', [[3.0, 4.0], [5.0, 6.0]], 'm', [[0.11, 0.21], [0.31, 0.41]] + [[3.0, 4.0], [5.0, 6.0]], + unit='m', + variance=[[0.11, 0.21], [0.31, 0.41]], + display_name='test + name', ), - DescriptorNumber('test', 9, 'm', 0.52), + DescriptorNumber(9, unit='m', variance=0.52, display_name='test'), ), ( DescriptorArray( - 'test + name', [[101.0, 201.0], [301.0, 401.0]], - 'dimensionless', - [[1010.0, 2010.0], [3010.0, 4010.0]], + unit='dimensionless', + variance=[[1010.0, 2010.0], [3010.0, 4010.0]], + display_name='test + name', + ), + DescriptorNumber( + 502.0, unit='dimensionless', variance=5020.0, display_name='test' ), - DescriptorNumber('test', 502.0, 'dimensionless', 5020.0), ), ( - DescriptorArray('test', np.ones((9, 9)), 'dimensionless', np.ones((9, 9))), - DescriptorNumber('test', 9.0, 'dimensionless', 9.0), + DescriptorArray( + np.ones((9, 9)), + unit='dimensionless', + variance=np.ones((9, 9)), + display_name='test', + ), + DescriptorNumber(9.0, unit='dimensionless', variance=9.0, display_name='test'), ), ( - DescriptorArray('test', np.ones((3, 3, 3)), 'dimensionless', np.ones((3, 3, 3))), DescriptorArray( - 'test', + np.ones((3, 3, 3)), + unit='dimensionless', + variance=np.ones((3, 3, 3)), + display_name='test', + ), + DescriptorArray( [3.0, 3.0, 3.0], - 'dimensionless', - [ + unit='dimensionless', + variance=[ 3.0, 3.0, 3.0, ], dimensions=['dim2'], + display_name='test', ), ), ( - DescriptorArray('test', [[2.0]], 'dimensionless'), - DescriptorNumber('test', 2.0, 'dimensionless'), + DescriptorArray([[2.0]], unit='dimensionless', display_name='test'), + DescriptorNumber(2.0, unit='dimensionless', display_name='test'), ), ], ids=['2d_unit', '2d_dimensionless', '2d_large', '3d_dimensionless', '1d_dimensionless'], @@ -1560,7 +1631,6 @@ def test_abs(self, descriptor): def test_trace(self, test: DescriptorArray, expected: DescriptorNumber): result = test.trace() assert type(result) == type(expected) - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit if test.variance is not None: @@ -1573,14 +1643,17 @@ def test_trace(self, test: DescriptorArray, expected: DescriptorNumber): [ ( DescriptorArray( - 'test', np.ones((3, 3, 4, 5)), 'dimensionless', np.ones((3, 3, 4, 5)) + np.ones((3, 3, 4, 5)), + unit='dimensionless', + variance=np.ones((3, 3, 4, 5)), + display_name='test', ), DescriptorArray( - 'test', - 3 * np.ones((3, 4)), - 'dimensionless', 3 * np.ones((3, 4)), + unit='dimensionless', + variance=3 * np.ones((3, 4)), dimensions=['dim0', 'dim2'], + display_name='test', ), ('dim1', 'dim3'), ) @@ -1592,7 +1665,6 @@ def test_trace_select_dimensions( ): result = test.trace(dimension1=dimensions[0], dimension2=dimensions[1]) assert type(result) == type(expected) - assert result.name == result.unique_name assert np.array_equal(result.value.shape, expected.value.shape) assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit @@ -1602,17 +1674,32 @@ def test_trace_select_dimensions( 'test,dimensions,message', [ ( - DescriptorArray('test', np.ones((3, 3, 3)), 'dimensionless', np.ones((3, 3, 3))), + DescriptorArray( + np.ones((3, 3, 3)), + unit='dimensionless', + variance=np.ones((3, 3, 3)), + display_name='test', + ), ('dim0', None), 'Either both or none', ), ( - DescriptorArray('test', np.ones((3, 3, 3)), 'dimensionless', np.ones((3, 3, 3))), + DescriptorArray( + np.ones((3, 3, 3)), + unit='dimensionless', + variance=np.ones((3, 3, 3)), + display_name='test', + ), ('dim0', 'dim0'), 'must be different', ), ( - DescriptorArray('test', np.ones((3, 3, 3)), 'dimensionless', np.ones((3, 3, 3))), + DescriptorArray( + np.ones((3, 3, 3)), + unit='dimensionless', + variance=np.ones((3, 3, 3)), + display_name='test', + ), ('dim0', 'dim1337'), 'does not exist', ), @@ -1635,9 +1722,10 @@ def test_slicing(self, descriptor: DescriptorArray): assert type(last_value) == DescriptorArray assert type(second_array) == DescriptorArray - assert first_value.name != descriptor.unique_name - assert last_value.name != descriptor.unique_name - assert second_array.name != descriptor.unique_name + # Slices are fresh objects with their own identity, not the sliced parent. + assert first_value.unique_name != descriptor.unique_name + assert last_value.unique_name != descriptor.unique_name + assert second_array.unique_name != descriptor.unique_name assert np.array_equal( first_value.full_value.values, descriptor.full_value['dim0', 0].values @@ -1680,26 +1768,31 @@ def test_slice_assignment_exception(self, descriptor_dimensionless: DescriptorAr [ ( DescriptorArray( - 'test + name', [[3.0, 4.0], [5.0, 6.0]], 'm', [[0.11, 0.21], [0.31, 0.41]] + [[3.0, 4.0], [5.0, 6.0]], + unit='m', + variance=[[0.11, 0.21], [0.31, 0.41]], + display_name='test + name', ), - DescriptorNumber('test', 18, 'm', 1.04), + DescriptorNumber(18, unit='m', variance=1.04, display_name='test'), ), ( DescriptorArray( - 'test + name', [[101.0, 201.0], [301.0, 401.0]], - 'cm', - [[1010.0, 2010.0], [3010.0, 4010.0]], + unit='cm', + variance=[[1010.0, 2010.0], [3010.0, 4010.0]], + display_name='test + name', ), - DescriptorNumber('test', 1004.0, 'cm', 10040.0), + DescriptorNumber(1004.0, unit='cm', variance=10040.0, display_name='test'), ), ( - DescriptorArray('test', [[2.0, 3.0]], 'dimensionless', [[1.0, 2.0]]), - DescriptorNumber('test', 5.0, 'dimensionless', 3.0), + DescriptorArray( + [[2.0, 3.0]], unit='dimensionless', variance=[[1.0, 2.0]], display_name='test' + ), + DescriptorNumber(5.0, unit='dimensionless', variance=3.0, display_name='test'), ), ( - DescriptorArray('test', [[2.0, 3.0]], 'dimensionless'), - DescriptorNumber('test', 5.0, 'dimensionless'), + DescriptorArray([[2.0, 3.0]], unit='dimensionless', display_name='test'), + DescriptorNumber(5.0, unit='dimensionless', display_name='test'), ), ], ids=[ @@ -1712,7 +1805,6 @@ def test_slice_assignment_exception(self, descriptor_dimensionless: DescriptorAr def test_sum(self, test, expected): result = test.sum() assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit if test.variance is not None: @@ -1721,15 +1813,20 @@ def test_sum(self, test, expected): @pytest.mark.parametrize( 'expected, dim', [ - (DescriptorArray('test', [4.0, 6.0], 'm', [0.4, 0.6]), 'dim0'), - (DescriptorArray('test', [3.0, 7.0], 'm', [0.3, 0.7]), 'dim1'), + ( + DescriptorArray([4.0, 6.0], unit='m', variance=[0.4, 0.6], display_name='test'), + 'dim0', + ), + ( + DescriptorArray([3.0, 7.0], unit='m', variance=[0.3, 0.7], display_name='test'), + 'dim1', + ), ], ids=['descriptor_array_dim0', 'descriptor_array_dim1'], ) def test_sum_over_subset(self, descriptor, expected, dim): result = descriptor.sum(dim) assert type(result) == type(expected) - assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) @@ -1737,25 +1834,40 @@ def test_sum_over_subset(self, descriptor, expected, dim): @pytest.mark.parametrize( 'test, dimensions', [ - (DescriptorArray('test', [1.0], 'dimensionless', [1.0]), ['dim0']), ( - DescriptorArray('test', [[1.0, 1.0]], 'dimensionless', [[1.0, 1.0]]), + DescriptorArray([1.0], unit='dimensionless', variance=[1.0], display_name='test'), + ['dim0'], + ), + ( + DescriptorArray( + [[1.0, 1.0]], unit='dimensionless', variance=[[1.0, 1.0]], display_name='test' + ), ['dim0', 'dim1'], ), ( - DescriptorArray('test', [[1.0], [1.0]], 'dimensionless', [[1.0], [1.0]]), + DescriptorArray( + [[1.0], [1.0]], + unit='dimensionless', + variance=[[1.0], [1.0]], + display_name='test', + ), ['dim0', 'dim1'], ), ( - DescriptorArray('test', [[[1.0, 1.0, 1.0]]], 'dimensionless', [[[1.0, 1.0, 1.0]]]), + DescriptorArray( + [[[1.0, 1.0, 1.0]]], + unit='dimensionless', + variance=[[[1.0, 1.0, 1.0]]], + display_name='test', + ), ['dim0', 'dim1', 'dim2'], ), ( DescriptorArray( - 'test', - [[[1.0]], [[1.0]], [[1.0]]], - 'dimensionless', [[[1.0]], [[1.0]], [[1.0]]], + unit='dimensionless', + variance=[[[1.0]], [[1.0]], [[1.0]]], + display_name='test', ), ['dim0', 'dim1', 'dim2'], ), @@ -1770,7 +1882,9 @@ def test_array_set_dimensions_exception(self, descriptor): descriptor.dimensions = ['too_few'] assert 'must have the same shape' with pytest.raises(ValueError) as e: - DescriptorArray('test', [[1.0]], 'm', [[1.0]], dimensions=['dim']) + DescriptorArray( + [[1.0]], unit='m', variance=[[1.0]], dimensions=['dim'], display_name='test' + ) assert 'Length of dimensions' in str(e) def test_array_set_integer_value(self, descriptor): @@ -1793,7 +1907,9 @@ def test_array_create_with_mixed_integers_and_floats(self): value = [[1, 2], [3, 4]] variance = [[0.1, 0.2], [0.3, 0.4]] # Then Expect - descriptor = DescriptorArray('test', value, 'dimensionless', variance) # Should not raise + descriptor = DescriptorArray( + value, unit='dimensionless', variance=variance, display_name='test' + ) # Should not raise assert isinstance(descriptor.value[0][0], float) assert isinstance(descriptor.variance[0][0], float) diff --git a/tests/unit/variable/test_descriptor_base.py b/tests/unit/variable/test_descriptor_base.py index 24c342dc..df69218f 100644 --- a/tests/unit/variable/test_descriptor_base.py +++ b/tests/unit/variable/test_descriptor_base.py @@ -1,9 +1,14 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +import gc + import pytest from easyscience import global_object +from easyscience.base_classes import EasyBase +from easyscience.base_classes import EasyList +from easyscience.variable import DescriptorNumber from easyscience.variable.descriptor_base import DescriptorBase @@ -13,34 +18,34 @@ def descriptor(self): # This avoids the error: TypeError: Can't instantiate abstract class DescriptorBase with abstract methods __init__ DescriptorBase.__abstractmethods__ = set() DescriptorBase.__repr__ = lambda x: 'DescriptorBase' - self.objs_before_new_descriptor = len(global_object.map.created_objs) + # The registry holds weak references; collect objects left over from + # earlier tests so they cannot vanish between the count and the assert + gc.collect() + self.objs_before_new_descriptor = len(global_object.registry) descriptor = DescriptorBase( - name='name', description='description', url='url', display_name='display_name', - parent=None, ) return descriptor @pytest.fixture def clear(self): - global_object.map._clear() + global_object.registry.clear() @pytest.mark.parametrize( - 'name', - [1, True, 1.0, [], {}, (), None, object()], - ids=['int', 'bool', 'float', 'list', 'dict', 'tuple', 'None', 'object'], + 'unique_name', + [1, True, 1.0, [], {}, (), object()], + ids=['int', 'bool', 'float', 'list', 'dict', 'tuple', 'object'], ) - def test_init_name_type_error(self, name): + def test_init_unique_name_type_error(self, unique_name): # When Then with pytest.raises(TypeError): DescriptorBase( - name=name, + unique_name=unique_name, description='description', url='url', display_name='display_name', - parent=None, ) @pytest.mark.parametrize( @@ -52,11 +57,9 @@ def test_init_display_name_type_error(self, display_name): # When Then with pytest.raises(TypeError): DescriptorBase( - name='name', description='description', url='url', display_name=display_name, - parent=None, ) @pytest.mark.parametrize( @@ -68,11 +71,9 @@ def test_init_description_type_error(self, description): # When Then with pytest.raises(TypeError): DescriptorBase( - name='name', description=description, url='url', display_name='display_name', - parent=None, ) @pytest.mark.parametrize( @@ -84,19 +85,16 @@ def test_init_url_type_error(self, url): # When Then with pytest.raises(TypeError): DescriptorBase( - name='name', description='description', url=url, display_name='display_name', - parent=None, ) def test_init(self, descriptor: DescriptorBase): - assert descriptor._name == 'name' assert descriptor._description == 'description' assert descriptor._url == 'url' assert descriptor._display_name == 'display_name' - assert len(global_object.map.created_objs) - self.objs_before_new_descriptor == 1 + assert len(global_object.registry) - self.objs_before_new_descriptor == 1 def test_display_name(self, descriptor: DescriptorBase): # When Then Expect @@ -106,7 +104,7 @@ def test_display_name_none(self, descriptor: DescriptorBase): # When descriptor._display_name = None # Then Expect - assert descriptor.display_name == 'name' + assert descriptor.display_name == descriptor.unique_name def test_display_name_setter(self, descriptor: DescriptorBase): # When @@ -124,22 +122,6 @@ def test_display_name_setter_type_error(self, descriptor: DescriptorBase, displa with pytest.raises(TypeError): descriptor.display_name = display_name - def test_name_setter(self, descriptor: DescriptorBase): - # When - descriptor.name = 'new_name' - # Then Expect - assert descriptor.name == 'new_name' - - @pytest.mark.parametrize( - 'name', - [1, True, 1.0, [], {}, (), object(), None], - ids=['int', 'bool', 'float', 'list', 'dict', 'tuple', 'object', 'None'], - ) - def test_name_setter_type_error(self, descriptor: DescriptorBase, name): - # When Then - with pytest.raises(TypeError): - descriptor.name = name - def test_description_setter(self, descriptor: DescriptorBase): # When descriptor.description = 'new_description' @@ -172,21 +154,12 @@ def test_url_setter_type_error(self, descriptor: DescriptorBase, url): with pytest.raises(TypeError): descriptor.url = url - @pytest.mark.parametrize('stack_enabled,stack_elements', [(False, 0), (True, 1)]) - def test_set_display_name_without_global_object_stack( - self, descriptor: DescriptorBase, stack_enabled, stack_elements - ): - # When - descriptor.__repr__ = lambda x: 'DescriptorBase' - global_object.stack.clear() - global_object.stack._enabled = stack_enabled - - # Then + def test_set_display_name(self, descriptor: DescriptorBase): + # When Then descriptor.display_name = 'new_display_name' # Expect assert descriptor.display_name == 'new_display_name' - assert len(global_object.stack.history) == stack_elements def test_copy(self, descriptor: DescriptorBase): # When Then @@ -194,18 +167,17 @@ def test_copy(self, descriptor: DescriptorBase): # Expect assert type(descriptor_copy) == DescriptorBase - assert descriptor_copy._name == descriptor._name assert descriptor_copy._description == descriptor._description assert descriptor_copy._url == descriptor._url assert descriptor_copy._display_name == descriptor._display_name def test_unique_name_generator(self, clear, descriptor: DescriptorBase): # When - second_descriptor = DescriptorBase(name='test', unique_name='DescriptorBase_2') + second_descriptor = DescriptorBase(display_name='test', unique_name='DescriptorBase_2') # Then - third_descriptor = DescriptorBase(name='test2') - fourth_descriptor = DescriptorBase(name='test3') + third_descriptor = DescriptorBase(display_name='test2') + fourth_descriptor = DescriptorBase(display_name='test3') # Expect assert descriptor.unique_name == 'DescriptorBase_0' diff --git a/tests/unit/variable/test_descriptor_bool.py b/tests/unit/variable/test_descriptor_bool.py index 3c181f0e..8aa6befe 100644 --- a/tests/unit/variable/test_descriptor_bool.py +++ b/tests/unit/variable/test_descriptor_bool.py @@ -11,25 +11,22 @@ class TestDescriptorBool: @pytest.fixture def descriptor(self): descriptor = DescriptorBool( - name='name', value=True, description='description', url='url', display_name='display_name', - parent=None, ) return descriptor @pytest.fixture def clear(self): - global_object.map._clear() + global_object.registry.clear() def test_init(self, descriptor: DescriptorBool): # When Then Expect assert descriptor._bool_value == True # From super - assert descriptor._name == 'name' assert descriptor._description == 'description' assert descriptor._url == 'url' assert descriptor._display_name == 'display_name' @@ -40,12 +37,10 @@ def test_init_bool_value_type_exception(self, bool_value): # When Then Expect with pytest.raises(ValueError): DescriptorBool( - name='name', value=bool_value, description='description', url='url', display_name='display_name', - parent=None, ) def test_value(self, descriptor: DescriptorBool): @@ -70,7 +65,7 @@ def test_repr(self, descriptor: DescriptorBool): repr_str = str(descriptor) # Expect - assert repr_str == "" + assert repr_str == "" def test_copy(self, descriptor: DescriptorBool): # When Then diff --git a/tests/unit/variable/test_descriptor_number.py b/tests/unit/variable/test_descriptor_number.py index 6abd3029..381c1db0 100644 --- a/tests/unit/variable/test_descriptor_number.py +++ b/tests/unit/variable/test_descriptor_number.py @@ -13,20 +13,18 @@ class TestDescriptorNumber: @pytest.fixture def descriptor(self): descriptor = DescriptorNumber( - name='name', value=1, unit='m', variance=0.1, description='description', url='url', display_name='display_name', - parent=None, ) return descriptor @pytest.fixture def clear(self): - global_object.map._clear() + global_object.registry.clear() def test_init(self, descriptor: DescriptorNumber): # When Then Expect @@ -36,7 +34,6 @@ def test_init(self, descriptor: DescriptorNumber): assert descriptor._observers == [] # From super - assert descriptor._name == 'name' assert descriptor._description == 'description' assert descriptor._url == 'url' assert descriptor._display_name == 'display_name' @@ -44,14 +41,12 @@ def test_init(self, descriptor: DescriptorNumber): def test_init_sc_unit(self): # When Then descriptor = DescriptorNumber( - name='name', value=1, unit=sc.units.Unit('m'), variance=0.1, description='description', url='url', display_name='display_name', - parent=None, ) # Expect @@ -63,14 +58,12 @@ def test_init_sc_unit_unknown(self): # When Then Expect with pytest.raises(UnitError): DescriptorNumber( - name='name', value=1, unit='unknown', variance=0.1, description='description', url='url', display_name='display_name', - parent=None, ) @pytest.mark.parametrize('value', [True, 'string']) @@ -80,14 +73,12 @@ def test_init_value_type_exception(self, value): # Then Expect with pytest.raises(TypeError): DescriptorNumber( - name='name', value=value, unit='m', variance=0.1, description='description', url='url', display_name='display_name', - parent=None, ) def test_init_variance_exception(self): @@ -97,14 +88,12 @@ def test_init_variance_exception(self): # Then Expect with pytest.raises(ValueError): DescriptorNumber( - name='name', value=1, unit='m', variance=variance, description='description', url='url', display_name='display_name', - parent=None, ) # test from_scipp @@ -113,7 +102,7 @@ def test_from_scipp(self): full_value = sc.scalar(1, unit='m') # Then - descriptor = DescriptorNumber.from_scipp(name='name', full_value=full_value) + descriptor = DescriptorNumber.from_scipp(display_name='name', full_value=full_value) # Expect assert descriptor._scalar.value == 1 @@ -134,7 +123,7 @@ def test_from_scipp(self): def test_from_scipp_type_exception(self, full_value): # When Then Expect with pytest.raises(TypeError): - DescriptorNumber.from_scipp(name='name', full_value=full_value) + DescriptorNumber.from_scipp(display_name='name', full_value=full_value) def test_full_value(self, descriptor: DescriptorNumber): # When Then Expect @@ -201,7 +190,7 @@ def test_repr(self, descriptor: DescriptorNumber): repr_str = str(descriptor) # Expect - assert repr_str == "" + assert repr_str == "" def test_copy(self, descriptor: DescriptorNumber): # When Then @@ -219,7 +208,7 @@ def test_copy(self, descriptor: DescriptorNumber): ) def test_base_unit(self, unit_string, expected): # When - descriptor = DescriptorNumber(name='name', value=1, unit=unit_string) + descriptor = DescriptorNumber(display_name='name', value=1, unit=unit_string) # Then base_unit = descriptor._base_unit() @@ -231,17 +220,12 @@ def test_base_unit(self, unit_string, expected): 'test, expected', [ ( - DescriptorNumber( - 'test', - 2, - 'm', - 0.01, - ), - DescriptorNumber('test + name', 3, 'm', 0.11), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), + DescriptorNumber(3, unit='m', variance=0.11, display_name='test + name'), ), ( - DescriptorNumber('test', 2, 'cm', 0.01), - DescriptorNumber('test + name', 102, 'cm', 1000.01), + DescriptorNumber(2, unit='cm', variance=0.01, display_name='test'), + DescriptorNumber(102, unit='cm', variance=1000.01, display_name='test + name'), ), ], ids=['regular', 'unit_conversion'], @@ -252,7 +236,6 @@ def test_addition(self, descriptor: DescriptorNumber, test, expected): # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -261,7 +244,7 @@ def test_addition(self, descriptor: DescriptorNumber, test, expected): def test_addition_with_scalar(self): # When - descriptor = DescriptorNumber(name='name', value=1, variance=0.1) + descriptor = DescriptorNumber(display_name='name', value=1, variance=0.1) # Then result = descriptor + 1.0 @@ -269,13 +252,11 @@ def test_addition_with_scalar(self): # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == 2.0 assert result.unit == 'dimensionless' assert result.variance == 0.1 assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == 2.0 assert result_reverse.unit == 'dimensionless' assert result_reverse.variance == 0.1 @@ -284,11 +265,7 @@ def test_addition_with_scalar(self): 'test', [ 1.0, - DescriptorNumber( - 'test', - 2, - 's', - ), + DescriptorNumber(2, unit='s', display_name='test'), ], ids=['add_scalar_to_unit', 'incompatible_units'], ) @@ -303,17 +280,12 @@ def test_addition_exception(self, descriptor: DescriptorNumber, test): 'test, expected', [ ( - DescriptorNumber( - 'test', - 2, - 'm', - 0.01, - ), - DescriptorNumber('test - name', 1, 'm', 0.11), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), + DescriptorNumber(1, unit='m', variance=0.11, display_name='test - name'), ), ( - DescriptorNumber('test', 2, 'cm', 0.01), - DescriptorNumber('test - name', -98, 'cm', 1000.01), + DescriptorNumber(2, unit='cm', variance=0.01, display_name='test'), + DescriptorNumber(-98, unit='cm', variance=1000.01, display_name='test - name'), ), ], ids=['regular', 'unit_conversion'], @@ -324,7 +296,6 @@ def test_subtraction(self, descriptor: DescriptorNumber, test, expected): # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -333,7 +304,7 @@ def test_subtraction(self, descriptor: DescriptorNumber, test, expected): def test_subtraction_with_scalar(self): # When - descriptor = DescriptorNumber(name='name', value=2, variance=0.1) + descriptor = DescriptorNumber(display_name='name', value=2, variance=0.1) # Then result = descriptor - 1.0 @@ -341,13 +312,11 @@ def test_subtraction_with_scalar(self): # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == 1.0 assert result.unit == 'dimensionless' assert result.variance == 0.1 assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == -1.0 assert result_reverse.unit == 'dimensionless' assert result_reverse.variance == 0.1 @@ -356,11 +325,7 @@ def test_subtraction_with_scalar(self): 'test', [ 1.0, - DescriptorNumber( - 'test', - 2, - 's', - ), + DescriptorNumber(2, unit='s', display_name='test'), ], ids=['sub_scalar_to_unit', 'incompatible_units'], ) @@ -375,17 +340,12 @@ def test_subtraction_exception(self, descriptor: DescriptorNumber, test): 'test, expected', [ ( - DescriptorNumber( - 'test', - 2, - 'm', - 0.01, - ), - DescriptorNumber('test * name', 2, 'm^2', 0.41), + DescriptorNumber(2, unit='m', variance=0.01, display_name='test'), + DescriptorNumber(2, unit='m^2', variance=0.41, display_name='test * name'), ), ( - DescriptorNumber('test', 2, 'dm', 0.01), - DescriptorNumber('test * name', 0.2, 'm^2', 0.0041), + DescriptorNumber(2, unit='dm', variance=0.01, display_name='test'), + DescriptorNumber(0.2, unit='m^2', variance=0.0041, display_name='test * name'), ), ], ids=['regular', 'base_unit_conversion'], @@ -396,7 +356,6 @@ def test_multiplication(self, descriptor: DescriptorNumber, test, expected): # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == pytest.approx(expected.variance) @@ -408,13 +367,11 @@ def test_multiplication_with_scalar(self, descriptor: DescriptorNumber): # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == 2.0 assert result.unit == 'm' assert result.variance == 0.4 assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == 2.0 assert result_reverse.unit == 'm' assert result_reverse.variance == 0.4 @@ -423,19 +380,14 @@ def test_multiplication_with_scalar(self, descriptor: DescriptorNumber): 'test, expected, expected_reverse', [ ( - DescriptorNumber( - 'test', - 2, - 'm^2', - 0.01, - ), - DescriptorNumber('name / test', 0.5, '1/m', 0.025625), - DescriptorNumber('test / name', 2, 'm', 0.41), + DescriptorNumber(2, unit='m^2', variance=0.01, display_name='test'), + DescriptorNumber(0.5, unit='1/m', variance=0.025625, display_name='name / test'), + DescriptorNumber(2, unit='m', variance=0.41, display_name='test / name'), ), ( 2, - DescriptorNumber('name / 2', 0.5, 'm', 0.025), - DescriptorNumber('2 / name', 2, '1/m', 0.4), + DescriptorNumber(0.5, unit='m', variance=0.025, display_name='name / 2'), + DescriptorNumber(2, unit='1/m', variance=0.4, display_name='2 / name'), ), ], ids=['descriptorNumber', 'scalar'], @@ -447,19 +399,19 @@ def test_division(self, descriptor: DescriptorNumber, test, expected, expected_r # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == pytest.approx(expected.variance) assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == pytest.approx(expected_reverse.variance) @pytest.mark.parametrize( - 'test', [0, DescriptorNumber('test', 0, 'm', 0.01)], ids=['zero', 'zero_descriptor'] + 'test', + [0, DescriptorNumber(0, unit='m', variance=0.01, display_name='test')], + ids=['zero', 'zero_descriptor'], ) def test_division_exception(self, descriptor: DescriptorNumber, test): # When Then Expect @@ -468,7 +420,7 @@ def test_division_exception(self, descriptor: DescriptorNumber, test): def test_division_exception_reverse(self): # When - descriptor = DescriptorNumber(name='name', value=0, variance=0.1) + descriptor = DescriptorNumber(display_name='name', value=0, variance=0.1) # Then Expect with pytest.raises(ZeroDivisionError): @@ -478,38 +430,41 @@ def test_division_exception_reverse(self): 'test, expected', [ ( - DescriptorNumber('test', 2), - DescriptorNumber('name ** test', 4, unit='m^2', variance=1.6), + DescriptorNumber(2, display_name='test'), + DescriptorNumber(4, unit='m^2', variance=1.6, display_name='name ** test'), + ), + (2, DescriptorNumber(4, unit='m^2', variance=1.6, display_name='name ** 2')), + ( + -2, + DescriptorNumber(0.25, unit='1/m^2', variance=0.00625, display_name='name ** -2'), ), - (2, DescriptorNumber('name ** 2', 4, unit='m^2', variance=1.6)), - (-2, DescriptorNumber('name ** -2', 0.25, unit='1/m^2', variance=0.00625)), ], ids=['descriptorNumber', 'scalar', 'negative_scalar'], ) def test_power_of_descriptor(self, test, expected): # When - descriptor = DescriptorNumber(name='name', value=2, unit='m', variance=0.1) + descriptor = DescriptorNumber(display_name='name', value=2, unit='m', variance=0.1) # Then result = descriptor**test # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance def test_power_of_dimensionless_descriptor(self): # When - descriptor = DescriptorNumber(name='name', value=2, unit='dimensionless', variance=0.1) + descriptor = DescriptorNumber( + display_name='name', value=2, unit='dimensionless', variance=0.1 + ) # Then result = descriptor**0.5 # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == 1.4142135623730951 assert result.unit == 'dimensionless' assert result.variance == pytest.approx(0.0125) @@ -517,10 +472,18 @@ def test_power_of_dimensionless_descriptor(self): @pytest.mark.parametrize( 'descriptor, exponent, exception', [ - (DescriptorNumber('name', 2), DescriptorNumber('test', 2, unit='m'), UnitError), - (DescriptorNumber('name', 2), DescriptorNumber('test', 2, variance=0.1), ValueError), - (DescriptorNumber('name', 2, unit='m'), 0.5, UnitError), - (DescriptorNumber('name', -2), 0.5, ValueError), + ( + DescriptorNumber(2, display_name='name'), + DescriptorNumber(2, unit='m', display_name='test'), + UnitError, + ), + ( + DescriptorNumber(2, display_name='name'), + DescriptorNumber(2, variance=0.1, display_name='test'), + ValueError, + ), + (DescriptorNumber(2, unit='m', display_name='name'), 0.5, UnitError), + (DescriptorNumber(-2, display_name='name'), 0.5, ValueError), ], ids=[ 'descriptor_unit', @@ -536,7 +499,7 @@ def test_power_of_descriptor_exceptions(self, descriptor, exponent, exception): def test_descriptor_as_exponentiation(self): # When - descriptor = DescriptorNumber(name='name', value=2) + descriptor = DescriptorNumber(display_name='name', value=2) # Then result = 2**descriptor @@ -547,8 +510,8 @@ def test_descriptor_as_exponentiation(self): @pytest.mark.parametrize( 'exponent, exception', [ - (DescriptorNumber('test', 2, unit='m'), UnitError), - (DescriptorNumber('test', 2, variance=0.1), ValueError), + (DescriptorNumber(2, unit='m', display_name='test'), UnitError), + (DescriptorNumber(2, variance=0.1, display_name='test'), ValueError), ], ids=['descriptor_unit', 'descriptor_variance'], ) @@ -559,28 +522,26 @@ def test_descriptor_as_exponentiation_exception(self, exponent, exception): def test_negation(self): # When - descriptor = DescriptorNumber(name='name', unit='m', value=2, variance=0.1) + descriptor = DescriptorNumber(display_name='name', unit='m', value=2, variance=0.1) # Then result = -descriptor # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == -2 assert result.unit == 'm' assert result.variance == 0.1 def test_abs(self): # When - descriptor = DescriptorNumber(name='name', unit='m', value=-2, variance=0.1) + descriptor = DescriptorNumber(display_name='name', unit='m', value=-2, variance=0.1) # Then result = abs(descriptor) # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == 2 assert result.unit == 'm' assert result.variance == 0.1 diff --git a/tests/unit/variable/test_descriptor_str.py b/tests/unit/variable/test_descriptor_str.py index 46acc847..bc9533e7 100644 --- a/tests/unit/variable/test_descriptor_str.py +++ b/tests/unit/variable/test_descriptor_str.py @@ -11,25 +11,22 @@ class TestDescriptorStr: @pytest.fixture def descriptor(self): descriptor = DescriptorStr( - name='name', value='string', description='description', url='url', display_name='display_name', - parent=None, ) return descriptor @pytest.fixture def clear(self): - global_object.map._clear() + global_object.registry.clear() def test_init(self, descriptor: DescriptorStr): # When Then Expect assert descriptor._string == 'string' # From super - assert descriptor._name == 'name' assert descriptor._description == 'description' assert descriptor._url == 'url' assert descriptor._display_name == 'display_name' @@ -39,12 +36,10 @@ def test_init_string_type_exception(self, string): # When Then Expect with pytest.raises(ValueError): DescriptorStr( - name='name', value=string, description='description', url='url', display_name='display_name', - parent=None, ) def test_value(self, descriptor: DescriptorStr): @@ -69,7 +64,7 @@ def test_repr(self, descriptor: DescriptorStr): repr_str = str(descriptor) # Expect - assert repr_str == "" + assert repr_str == "" def test_copy(self, descriptor: DescriptorStr): # When Then diff --git a/tests/unit/variable/test_parameter.py b/tests/unit/variable/test_parameter.py index 1ac05b45..9e1bcbc7 100644 --- a/tests/unit/variable/test_parameter.py +++ b/tests/unit/variable/test_parameter.py @@ -9,7 +9,7 @@ from scipp import UnitError from easyscience import DescriptorNumber -from easyscience import ObjBase +from easyscience import ModelBase from easyscience import Parameter from easyscience import global_object @@ -17,9 +17,7 @@ class TestParameter: @pytest.fixture def parameter(self) -> Parameter: - self.mock_callback = MagicMock() parameter = Parameter( - name='name', value=1, unit='m', variance=0.01, @@ -28,26 +26,17 @@ def parameter(self) -> Parameter: description='description', url='url', display_name='display_name', - callback=self.mock_callback, - parent=None, ) return parameter @pytest.fixture def normal_parameter(self) -> Parameter: - parameter = Parameter( - name='name', - value=1, - unit='m', - variance=0.01, - min=0, - max=10, - ) + parameter = Parameter(display_name='name', value=1, unit='m', variance=0.01, min=0, max=10) return parameter @pytest.fixture def clear(self): - global_object.map._clear() + global_object.registry.clear() def compare_parameters(self, parameter1: Parameter, parameter2: Parameter): assert parameter1.value == parameter2.value @@ -64,14 +53,12 @@ def test_init(self, parameter: Parameter): assert parameter._min.unit == 'm' assert parameter._max.value == 10 assert parameter._max.unit == 'm' - assert parameter._callback == self.mock_callback assert parameter._independent == True # From super assert parameter._scalar.value == 1 assert parameter._scalar.unit == 'm' assert parameter._scalar.variance == 0.01 - assert parameter._name == 'name' assert parameter._description == 'description' assert parameter._url == 'url' assert parameter._display_name == 'display_name' @@ -80,13 +67,11 @@ def test_init(self, parameter: Parameter): def test_init_value_min_exception(self): # When - mock_callback = MagicMock() value = -1 # Then Expect with pytest.raises(ValueError): Parameter( - name='name', value=value, unit='m', variance=0.01, @@ -95,19 +80,15 @@ def test_init_value_min_exception(self): description='description', url='url', display_name='display_name', - callback=mock_callback, - parent=None, ) def test_init_value_max_exception(self): # When - mock_callback = MagicMock() value = 100 # Then Expect with pytest.raises(ValueError): Parameter( - name='name', value=value, unit='m', variance=0.01, @@ -116,14 +97,12 @@ def test_init_value_max_exception(self): description='description', url='url', display_name='display_name', - callback=mock_callback, - parent=None, ) def test_make_dependent_on(self, normal_parameter: Parameter): # When independent_parameter = Parameter( - name='independent', value=1, unit='m', variance=0.01, min=0, max=10 + display_name='independent', value=1, unit='m', variance=0.01, min=0, max=10 ) # Then @@ -144,13 +123,10 @@ def test_make_dependent_on(self, normal_parameter: Parameter): normal_parameter.value == 4 self.compare_parameters(normal_parameter, 2 * independent_parameter) - def test_dependent_parameter_update_pushes_value_to_callback(self, parameter: Parameter): - # When # Simulate a calculator bound to the parameter via the callback (issue #281) - calculator = {'value': parameter._scalar.value} - self.mock_callback.fget.side_effect = lambda: calculator['value'] - self.mock_callback.fset.side_effect = lambda value: calculator.update(value=value) + def test_dependent_parameter_update_follows_independent(self, parameter: Parameter): + # When independent_parameter = Parameter( - name='independent', value=1, unit='m', variance=0.01, min=0, max=10 + display_name='independent', value=1, unit='m', variance=0.01, min=0, max=10 ) # Then @@ -158,30 +134,23 @@ def test_dependent_parameter_update_pushes_value_to_callback(self, parameter: Pa dependency_expression='2*a', dependency_map={'a': independent_parameter} ) - # Expect # Making the parameter dependent pushes the computed value to the calculator - assert calculator['value'] == 2.0 - self.mock_callback.fset.assert_called_with(2.0) - assert self.mock_callback.fset.call_count == 1 + # Expect # Making the parameter dependent computes its value + assert parameter.value == 2.0 + assert parameter.unit == 'm' # Then independent_parameter.value = 3 - # Expect # The change of the independent parameter propagates to the calculator - assert calculator['value'] == 6.0 - self.mock_callback.fset.assert_called_with(6.0) - assert self.mock_callback.fset.call_count == 2 - # Reading the value must not return a stale calculator value + # Expect # The change of the independent parameter propagates (issue #281) assert parameter.value == 6.0 + assert parameter.unit == 'm' - def test_dependent_parameter_update_pushes_value_to_callback_with_desired_unit( + def test_dependent_parameter_update_follows_independent_with_desired_unit( self, parameter: Parameter ): - # When # Simulate a calculator bound to the parameter via the callback (issue #281) - calculator = {'value': parameter._scalar.value} - self.mock_callback.fget.side_effect = lambda: calculator['value'] - self.mock_callback.fset.side_effect = lambda value: calculator.update(value=value) + # When independent_parameter = Parameter( - name='independent', value=1, unit='m', variance=0.01, min=0, max=10 + display_name='independent', value=1, unit='m', variance=0.01, min=0, max=10 ) # Then @@ -191,23 +160,23 @@ def test_dependent_parameter_update_pushes_value_to_callback_with_desired_unit( desired_unit='cm', ) - # Expect # The calculator receives the value converted to the desired unit - assert calculator['value'] == 200.0 - self.mock_callback.fset.assert_called_with(200.0) + # Expect # The value is converted to the desired unit + assert parameter.value == 200.0 + assert parameter.unit == 'cm' # Then independent_parameter.value = 2 # Expect - assert calculator['value'] == 400.0 assert parameter.value == 400.0 + assert parameter.unit == 'cm' def test_dependent_parameter_make_dependent_on_with_desired_unit( self, normal_parameter: Parameter ): # When independent_parameter = Parameter( - name='independent', value=1, unit='m', variance=0.01, min=0, max=10 + display_name='independent', value=1, unit='m', variance=0.01, min=0, max=10 ) # Then @@ -287,7 +256,7 @@ def test_dependent_parameter_make_dependent_on_with_desired_unit_incompatible_un ): # When independent_parameter = Parameter( - name='independent', value=1, unit='m', variance=0.01, min=0, max=10 + display_name='independent', value=1, unit='m', variance=0.01, min=0, max=10 ) # Then Expect @@ -303,7 +272,7 @@ def test_dependent_parameter_make_dependent_on_with_incorrect_unit_raises( ): # When independent_parameter = Parameter( - name='independent', value=1, unit='m', variance=0.01, min=0, max=10 + display_name='independent', value=1, unit='m', variance=0.01, min=0, max=10 ) # Then Expect @@ -319,7 +288,6 @@ def test_dependent_parameter_make_dependent_on_with_incorrect_unit_raises( def test_parameter_from_dependency(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -329,7 +297,6 @@ def test_parameter_from_dependency(self, normal_parameter: Parameter): assert dependent_parameter._independent == False assert dependent_parameter.dependency_expression == '2*a' assert dependent_parameter.dependency_map == {'a': normal_parameter} - assert dependent_parameter.name == 'dependent' assert dependent_parameter.display_name == 'display_name' self.compare_parameters(dependent_parameter, 2 * normal_parameter) @@ -342,7 +309,6 @@ def test_parameter_from_dependency(self, normal_parameter: Parameter): def test_parameter_from_dependency_with_desired_unit(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -353,7 +319,6 @@ def test_parameter_from_dependency_with_desired_unit(self, normal_parameter: Par assert dependent_parameter._independent == False assert dependent_parameter.dependency_expression == '2*a' assert dependent_parameter.dependency_map == {'a': normal_parameter} - assert dependent_parameter.name == 'dependent' assert dependent_parameter.display_name == 'display_name' assert dependent_parameter.value == 200 * normal_parameter.value @@ -386,7 +351,6 @@ def test_parameter_from_dependency_with_desired_unit_incompatible_unit_raises( # When Then Expect with pytest.raises(UnitError): dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -396,8 +360,7 @@ def test_parameter_from_dependency_with_desired_unit_incompatible_unit_raises( def test_dependent_parameter_with_unique_name(self, clear, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', - dependency_expression='2*"Parameter_0"', + display_name='dependent', dependency_expression='2*"Parameter_0"' ) # Expect @@ -416,7 +379,7 @@ def test_process_dependency_unique_names_double_quotes( ): # When independent_parameter = Parameter( - name='independent', + display_name='independent', value=1, unit='m', variance=0.01, @@ -438,7 +401,7 @@ def test_process_dependency_unique_names_single_quotes( ): # When independent_parameter = Parameter( - name='independent', + display_name='independent', value=1, unit='m', variance=0.01, @@ -447,7 +410,7 @@ def test_process_dependency_unique_names_single_quotes( unique_name='Special_name', ) independent_parameter_2 = Parameter( - name='independent_2', + display_name='independent_2', value=1, unit='m', variance=0.01, @@ -489,7 +452,7 @@ def test_process_dependency_unique_names_exception_not_a_descriptorNumber( ): # When normal_parameter._dependency_map = {} - base_obj = ObjBase(name='ObjBase', unique_name='base_obj') + base_obj = ModelBase(unique_name='base_obj') # Then Expect with pytest.raises( @@ -501,10 +464,10 @@ def test_process_dependency_unique_names_exception_not_a_descriptorNumber( @pytest.mark.parametrize( 'dependency_expression, dependency_map', [ - (2, {'a': Parameter(name='a', value=1)}), - ('2*a', ['a', Parameter(name='a', value=1)]), - ('2*a', {4: Parameter(name='a', value=1)}), - ('2*a', {'a': ObjBase(name='a')}), + (2, {'a': Parameter(display_name='a', value=1)}), + ('2*a', ['a', Parameter(display_name='a', value=1)]), + ('2*a', {4: Parameter(display_name='a', value=1)}), + ('2*a', {'a': ModelBase(display_name='a')}), ], ids=[ 'dependency_expression_not_a_string', @@ -519,7 +482,7 @@ def test_parameter_from_dependency_input_exceptions( # When Then Expect with pytest.raises(TypeError): Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression=dependency_expression, dependency_map=dependency_map, ) @@ -543,9 +506,11 @@ def test_parameter_make_dependent_on_exceptions_cleanup_previously_dependent( self, normal_parameter, dependency_expression, error ): # When - independent_parameter = Parameter(name='independent', value=10, unit='s', variance=0.02) + independent_parameter = Parameter( + display_name='independent', value=10, unit='s', variance=0.02 + ) dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='best', dependency_map={'best': independent_parameter}, ) @@ -568,7 +533,9 @@ def test_parameter_make_dependent_on_exceptions_cleanup_previously_independent( self, normal_parameter ): # When - independent_parameter = Parameter(name='independent', value=10, unit='s', variance=0.02) + independent_parameter = Parameter( + display_name='independent', value=10, unit='s', variance=0.02 + ) # Then Expect # Check that the correct error is raised with pytest.raises(NameError): @@ -585,7 +552,7 @@ def test_parameter_make_dependent_on_exceptions_cleanup_previously_independent( def test_dependent_parameter_updates(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -612,17 +579,17 @@ def test_dependent_parameter_updates(self, normal_parameter: Parameter): def test_dependent_parameter_indirect_updates(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) dependent_parameter_2 = Parameter.from_dependency( - name='dependent_2', + display_name='dependent_2', dependency_expression='10*a', dependency_map={'a': normal_parameter}, ) dependent_parameter_3 = Parameter.from_dependency( - name='dependent_3', + display_name='dependent_3', dependency_expression='b+c', dependency_map={'b': dependent_parameter, 'c': dependent_parameter_2}, ) @@ -639,12 +606,12 @@ def test_dependent_parameter_indirect_updates(self, normal_parameter: Parameter) def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) dependent_parameter_2 = Parameter.from_dependency( - name='dependent_2', + display_name='dependent_2', dependency_expression='2*b', dependency_map={'b': dependent_parameter}, ) @@ -664,7 +631,7 @@ def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Paramet def test_dependent_parameter_logical_dependency(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='a if a.value > 0 else -a', dependency_map={'a': normal_parameter}, ) @@ -678,11 +645,13 @@ def test_dependent_parameter_logical_dependency(self, normal_parameter: Paramete def test_dependent_parameter_return_is_descriptor_number(self): # When - descriptor_number = DescriptorNumber(name='descriptor', value=1, unit='m', variance=0.01) + descriptor_number = DescriptorNumber( + display_name='descriptor', value=1, unit='m', variance=0.01 + ) # Then dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*descriptor', dependency_map={'descriptor': descriptor_number}, ) @@ -702,12 +671,9 @@ def test_dependent_parameter_division_expression_order(self): would work correctly. """ # When - angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + angstrom = DescriptorNumber(1e-10, unit='m', display_name='angstrom') jump_length = Parameter( - name='jump_length', - value=float(1.0), - fixed=False, - unit='angstrom', + display_name='jump_length', value=float(1.0), fixed=False, unit='angstrom' ) expression = 'jump_length / angstrom' @@ -721,7 +687,7 @@ def test_dependent_parameter_division_expression_order(self): expected_value = expected_result.value # Then - This should not raise an error - dependent_param = Parameter(name='a', value=1.0) + dependent_param = Parameter(display_name='a', value=1.0) dependent_param.make_dependent_on( dependency_expression=expression, dependency_map=dependency_map, @@ -732,7 +698,7 @@ def test_dependent_parameter_division_expression_order(self): # Also test the alternative expression that previously worked expression_alt = '1/angstrom * jump_length' - dependent_param_alt = Parameter(name='b', value=1.0) + dependent_param_alt = Parameter(display_name='b', value=1.0) dependent_param_alt.make_dependent_on( dependency_expression=expression_alt, dependency_map=dependency_map, @@ -742,7 +708,7 @@ def test_dependent_parameter_division_expression_order(self): def test_dependent_parameter_overwrite_dependency(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -750,7 +716,7 @@ def test_dependent_parameter_overwrite_dependency(self, normal_parameter: Parame # Then normal_parameter_2 = Parameter( - name='a2', value=-2, unit='m', variance=0.01, min=-10, max=0 + display_name='a2', value=-2, unit='m', variance=0.01, min=-10, max=0 ) dependent_parameter.make_dependent_on( dependency_expression='3*a2', dependency_map={'a2': normal_parameter_2} @@ -766,7 +732,7 @@ def test_dependent_parameter_overwrite_dependency(self, normal_parameter: Parame def test_make_independent(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -800,7 +766,7 @@ def test_independent_parameter_dependency_expression(self, normal_parameter: Par def test_dependent_parameter_dependency_expression_setter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -817,7 +783,7 @@ def test_independent_parameter_dependency_map(self, normal_parameter: Parameter) def test_dependent_parameter_dependency_map_setter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -832,8 +798,6 @@ def test_min(self, parameter: Parameter): def test_set_min(self, parameter: Parameter): # When Then - self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value - parameter.min = 0.1 # Expect @@ -842,7 +806,7 @@ def test_set_min(self, parameter: Parameter): def test_set_min_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -866,7 +830,7 @@ def test_set_max(self, parameter: Parameter): def test_set_max_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -893,7 +857,6 @@ def test_convert_unit(self, parameter: Parameter): def test_set_desired_unit(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -931,7 +894,6 @@ def test_set_desired_unit(self, normal_parameter: Parameter): def test_set_desired_unit_incompatible_units_raises(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -947,7 +909,6 @@ def test_set_desired_unit_incompatible_units_raises(self, normal_parameter: Para def test_set_desired_unit_None(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -975,7 +936,6 @@ def test_set_desired_unit_independent_parameter_raises(self, normal_parameter: P def test_set_desired_unit_incorrect_unit_type_raises(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, display_name='display_name', @@ -995,7 +955,7 @@ def test_set_fixed(self, parameter: Parameter): def test_set_fixed_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -1034,7 +994,9 @@ def test_set_error_exception(self, parameter: Parameter): def test_repr(self, parameter: Parameter): # When Then Expect - assert repr(parameter) == "" + assert ( + repr(parameter) == "" + ) def test_repr_fixed(self, parameter: Parameter): # When @@ -1042,43 +1004,33 @@ def test_repr_fixed(self, parameter: Parameter): # Then Expect assert ( - repr(parameter) == "" + repr(parameter) + == "" ) - def test_value_match_callback(self, parameter: Parameter): + def test_value_read_is_repeatable_and_has_no_side_effects(self, parameter: Parameter): # When - self.mock_callback.fget.return_value = 1.0 + observer = MagicMock() + parameter._attach_observer(observer) - # Then Expect - assert parameter.value == 1.0 - assert parameter._callback.fget.call_count == 1 - - def test_value_no_match_callback(self, parameter: Parameter): - # When - self.mock_callback.fget.return_value = 2.0 + # Then + values = [parameter.value for _ in range(3)] - # Then Expect - assert parameter.value == 2.0 - assert parameter._callback.fget.call_count == 1 + # Expect + assert values == [1.0, 1.0, 1.0] + assert observer.mock_calls == [] def test_set_value(self, parameter: Parameter): - # When - # First call returns 1.0 that is used to enforce the undo/redo functionality to register the value has changed - # Second and third call returns 2.0 is used in the constraint check - self.mock_callback.fget.side_effect = [1.0, 2.0, 2.0] - - # Then + # When Then parameter.value = 2 # Expect - parameter._callback.fset.assert_called_with(2) - assert parameter._callback.fset.call_count == 1 assert parameter._scalar == sc.scalar(2, unit='m') def test_set_value_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -1095,7 +1047,7 @@ def test_set_full_value(self, parameter: Parameter): def test_set_variance_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -1107,7 +1059,7 @@ def test_set_variance_dependent_parameter(self, normal_parameter: Parameter): def test_set_error_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name='dependent', + display_name='dependent', dependency_expression='2*a', dependency_map={'a': normal_parameter}, ) @@ -1118,15 +1070,12 @@ def test_set_error_dependent_parameter(self, normal_parameter: Parameter): def test_copy(self, parameter: Parameter): # When Then - self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value parameter_copy = parameter.__copy__() # Expect assert type(parameter_copy) == Parameter assert id(parameter_copy._scalar) != id(parameter._scalar) - assert isinstance(parameter_copy._callback, property) - assert parameter_copy._name == parameter._name assert parameter_copy._scalar == parameter._scalar assert parameter_copy._min == parameter._min assert parameter_copy._max == parameter._max @@ -1140,19 +1089,32 @@ def test_copy(self, parameter: Parameter): 'test, expected, expected_reverse', [ ( - Parameter('test', 2, 'm', 0.01, -10, 20), - Parameter('name + test', 3, 'm', 0.02, -10, 30), - Parameter('test + name', 3, 'm', 0.02, -10, 30), + Parameter(2, unit='m', variance=0.01, min=-10, max=20, display_name='test'), + Parameter(3, unit='m', variance=0.02, min=-10, max=30, display_name='name + test'), + Parameter(3, unit='m', variance=0.02, min=-10, max=30, display_name='test + name'), ), ( - Parameter('test', 2, 'm', 0.01), - Parameter('name + test', 3, 'm', 0.02, min=-np.inf, max=np.inf), - Parameter('test + name', 3, 'm', 0.02, min=-np.inf, max=np.inf), + Parameter(2, unit='m', variance=0.01, display_name='test'), + Parameter( + 3, unit='m', variance=0.02, min=-np.inf, max=np.inf, display_name='name + test' + ), + Parameter( + 3, unit='m', variance=0.02, min=-np.inf, max=np.inf, display_name='test + name' + ), ), ( - Parameter('test', 2, 'cm', 0.01, -10, 10), - Parameter('name + test', 1.02, 'm', 0.010001, -0.1, 10.1), - Parameter('test + name', 102, 'cm', 100.01, -10, 1010), + Parameter(2, unit='cm', variance=0.01, min=-10, max=10, display_name='test'), + Parameter( + 1.02, + unit='m', + variance=0.010001, + min=-0.1, + max=10.1, + display_name='name + test', + ), + Parameter( + 102, unit='cm', variance=100.01, min=-10, max=1010, display_name='test + name' + ), ), ], ids=['regular', 'no_bounds', 'unit_conversion'], @@ -1164,22 +1126,17 @@ def test_addition_with_parameter( expected: Parameter, expected_reverse: Parameter, ): - # When - parameter._callback = property() - - # Then + # When Then result = parameter + test result_reverse = test + parameter # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance assert result.min == expected.min assert result.max == expected.max - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == expected_reverse.variance @@ -1190,21 +1147,19 @@ def test_addition_with_parameter( def test_addition_with_scalar(self): # When - parameter = Parameter(name='name', value=1, variance=0.01, min=0, max=10) + parameter = Parameter(display_name='name', value=1, variance=0.01, min=0, max=10) # Then result = parameter + 1.0 result_reverse = 1.0 + parameter # Expect - assert result.name == result.unique_name assert result.value == 2.0 assert result.unit == 'dimensionless' assert result.variance == 0.01 assert result.min == 1.0 assert result.max == 11.0 - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == 2.0 assert result_reverse.unit == 'dimensionless' assert result_reverse.variance == 0.01 @@ -1213,8 +1168,7 @@ def test_addition_with_scalar(self): def test_addition_with_descriptor_number(self, parameter: Parameter): # When - parameter._callback = property() - descriptor_number = DescriptorNumber(name='test', value=1, variance=0.1, unit='cm') + descriptor_number = DescriptorNumber(display_name='test', value=1, variance=0.1, unit='cm') # Then result = parameter + descriptor_number @@ -1222,7 +1176,6 @@ def test_addition_with_descriptor_number(self, parameter: Parameter): # Expect assert type(result) == Parameter - assert result.name == result.unique_name assert result.value == 1.01 assert result.unit == 'm' assert result.variance == 0.01001 @@ -1230,7 +1183,6 @@ def test_addition_with_descriptor_number(self, parameter: Parameter): assert result.max == 10.01 assert type(result_reverse) == Parameter - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == 101.0 assert result_reverse.unit == 'cm' assert result_reverse.variance == 100.1 @@ -1244,11 +1196,7 @@ def test_addition_with_descriptor_number(self, parameter: Parameter): 'test', [ 1.0, - Parameter( - 'test', - 2, - 's', - ), + Parameter(2, unit='s', display_name='test'), ], ids=['add_scalar_to_unit', 'incompatible_units'], ) @@ -1263,19 +1211,39 @@ def test_addition_exception(self, parameter: Parameter, test): 'test, expected, expected_reverse', [ ( - Parameter('test', 2, 'm', 0.01, -20, 20), - Parameter('name - test', -1, 'm', 0.02, -20, 30), - Parameter('test - name', 1, 'm', 0.02, -30, 20), + Parameter(2, unit='m', variance=0.01, min=-20, max=20, display_name='test'), + Parameter( + -1, unit='m', variance=0.02, min=-20, max=30, display_name='name - test' + ), + Parameter(1, unit='m', variance=0.02, min=-30, max=20, display_name='test - name'), ), ( - Parameter('test', 2, 'm', 0.01), - Parameter('name - test', -1, 'm', 0.02, min=-np.inf, max=np.inf), - Parameter('test - name', 1, 'm', 0.02, min=-np.inf, max=np.inf), + Parameter(2, unit='m', variance=0.01, display_name='test'), + Parameter( + -1, + unit='m', + variance=0.02, + min=-np.inf, + max=np.inf, + display_name='name - test', + ), + Parameter( + 1, unit='m', variance=0.02, min=-np.inf, max=np.inf, display_name='test - name' + ), ), ( - Parameter('test', 2, 'cm', 0.01, -10, 10), - Parameter('name - test', 0.98, 'm', 0.010001, -0.1, 10.1), - Parameter('test - name', -98, 'cm', 100.01, -1010, 10), + Parameter(2, unit='cm', variance=0.01, min=-10, max=10, display_name='test'), + Parameter( + 0.98, + unit='m', + variance=0.010001, + min=-0.1, + max=10.1, + display_name='name - test', + ), + Parameter( + -98, unit='cm', variance=100.01, min=-1010, max=10, display_name='test - name' + ), ), ], ids=['regular', 'no_bounds', 'unit_conversion'], @@ -1287,22 +1255,17 @@ def test_subtraction_with_parameter( expected: Parameter, expected_reverse: Parameter, ): - # When - parameter._callback = property() - - # Then + # When Then result = parameter - test result_reverse = test - parameter # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance assert result.min == expected.min assert result.max == expected.max - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == expected_reverse.variance @@ -1313,22 +1276,20 @@ def test_subtraction_with_parameter( def test_subtraction_with_parameter_nan_cases(self): # When - parameter = Parameter(name='name', value=1, variance=0.01, min=-np.inf, max=np.inf) - test = Parameter(name='test', value=2, variance=0.01, min=-np.inf, max=np.inf) + parameter = Parameter(display_name='name', value=1, variance=0.01, min=-np.inf, max=np.inf) + test = Parameter(display_name='test', value=2, variance=0.01, min=-np.inf, max=np.inf) # Then result = parameter - test result_reverse = test - parameter # Expect - assert result.name == result.unique_name assert result.value == -1.0 assert result.unit == 'dimensionless' assert result.variance == 0.02 assert result.min == -np.inf assert result.max == np.inf - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == 1.0 assert result_reverse.unit == 'dimensionless' assert result_reverse.variance == 0.02 @@ -1337,21 +1298,19 @@ def test_subtraction_with_parameter_nan_cases(self): def test_subtraction_with_scalar(self): # When - parameter = Parameter(name='name', value=2, variance=0.01, min=0, max=10) + parameter = Parameter(display_name='name', value=2, variance=0.01, min=0, max=10) # Then result = parameter - 1.0 result_reverse = 1.0 - parameter # Expect - assert result.name == result.unique_name assert result.value == 1.0 assert result.unit == 'dimensionless' assert result.variance == 0.01 assert result.min == -1.0 assert result.max == 9.0 - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == -1.0 assert result_reverse.unit == 'dimensionless' assert result_reverse.variance == 0.01 @@ -1360,8 +1319,7 @@ def test_subtraction_with_scalar(self): def test_subtraction_with_descriptor_number(self, parameter: Parameter): # When - parameter._callback = property() - descriptor_number = DescriptorNumber(name='test', value=1, variance=0.1, unit='cm') + descriptor_number = DescriptorNumber(display_name='test', value=1, variance=0.1, unit='cm') # Then result = parameter - descriptor_number @@ -1369,7 +1327,6 @@ def test_subtraction_with_descriptor_number(self, parameter: Parameter): # Expect assert type(result) == Parameter - assert result.name == result.unique_name assert result.value == 0.99 assert result.unit == 'm' assert result.variance == 0.01001 @@ -1377,7 +1334,6 @@ def test_subtraction_with_descriptor_number(self, parameter: Parameter): assert result.max == 9.99 assert type(result_reverse) == Parameter - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == -99.0 assert result_reverse.unit == 'cm' assert result_reverse.variance == 100.1 @@ -1391,11 +1347,7 @@ def test_subtraction_with_descriptor_number(self, parameter: Parameter): 'test', [ 1.0, - Parameter( - 'test', - 2, - 's', - ), + Parameter(2, unit='s', display_name='test'), ], ids=['sub_scalar_to_unit', 'incompatible_units'], ) @@ -1410,19 +1362,41 @@ def test_subtraction_exception(self, parameter: Parameter, test): 'test, expected, expected_reverse', [ ( - Parameter('test', 2, 'm', 0.01, -10, 20), - Parameter('name * test', 2, 'm^2', 0.05, -100, 200), - Parameter('test * name', 2, 'm^2', 0.05, -100, 200), + Parameter(2, unit='m', variance=0.01, min=-10, max=20, display_name='test'), + Parameter( + 2, unit='m^2', variance=0.05, min=-100, max=200, display_name='name * test' + ), + Parameter( + 2, unit='m^2', variance=0.05, min=-100, max=200, display_name='test * name' + ), ), ( - Parameter('test', 2, 'm', 0.01), - Parameter('name * test', 2, 'm^2', 0.05, min=-np.inf, max=np.inf), - Parameter('test * name', 2, 'm^2', 0.05, min=-np.inf, max=np.inf), + Parameter(2, unit='m', variance=0.01, display_name='test'), + Parameter( + 2, + unit='m^2', + variance=0.05, + min=-np.inf, + max=np.inf, + display_name='name * test', + ), + Parameter( + 2, + unit='m^2', + variance=0.05, + min=-np.inf, + max=np.inf, + display_name='test * name', + ), ), ( - Parameter('test', 2, 'dm', 0.01, -10, 20), - Parameter('name * test', 0.2, 'm^2', 0.0005, -10, 20), - Parameter('test * name', 0.2, 'm^2', 0.0005, -10, 20), + Parameter(2, unit='dm', variance=0.01, min=-10, max=20, display_name='test'), + Parameter( + 0.2, unit='m^2', variance=0.0005, min=-10, max=20, display_name='name * test' + ), + Parameter( + 0.2, unit='m^2', variance=0.0005, min=-10, max=20, display_name='test * name' + ), ), ], ids=['regular', 'no_bounds', 'base_unit_conversion'], @@ -1434,22 +1408,17 @@ def test_multiplication_with_parameter( expected: Parameter, expected_reverse: Parameter, ): - # When - parameter._callback = property() - - # Then + # When Then result = parameter * test result_reverse = test * parameter # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == pytest.approx(expected.variance) assert result.min == expected.min assert result.max == expected.max - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == pytest.approx(expected_reverse.variance) @@ -1460,35 +1429,61 @@ def test_multiplication_with_parameter( 'test, expected, expected_reverse', [ ( - Parameter('test', 0, '', 0.01, -10, 0), - Parameter('name * test', 0.0, 'dimensionless', 0.01, -np.inf, 0), - Parameter('test * name', 0, 'dimensionless', 0.01, -np.inf, 0), + Parameter(0, unit='', variance=0.01, min=-10, max=0, display_name='test'), + Parameter( + 0.0, + unit='dimensionless', + variance=0.01, + min=-np.inf, + max=0, + display_name='name * test', + ), + Parameter( + 0, + unit='dimensionless', + variance=0.01, + min=-np.inf, + max=0, + display_name='test * name', + ), ), ( - Parameter('test', 0, '', 0.01, 0, 10), - Parameter('name * test', 0.0, 'dimensionless', 0.01, 0, np.inf), - Parameter('test * name', 0, 'dimensionless', 0.01, 0, np.inf), + Parameter(0, unit='', variance=0.01, min=0, max=10, display_name='test'), + Parameter( + 0.0, + unit='dimensionless', + variance=0.01, + min=0, + max=np.inf, + display_name='name * test', + ), + Parameter( + 0, + unit='dimensionless', + variance=0.01, + min=0, + max=np.inf, + display_name='test * name', + ), ), ], ids=['zero_min', 'zero_max'], ) def test_multiplication_with_parameter_nan_cases(self, test, expected, expected_reverse): # When - parameter = Parameter(name='name', value=1, variance=0.01, min=1, max=np.inf) + parameter = Parameter(display_name='name', value=1, variance=0.01, min=1, max=np.inf) # Then result = parameter * test result_reverse = test * parameter # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance assert result.min == expected.min assert result.max == expected.max - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == expected_reverse.variance @@ -1499,14 +1494,18 @@ def test_multiplication_with_parameter_nan_cases(self, test, expected, expected_ 'test, expected, expected_reverse', [ ( - DescriptorNumber(name='test', value=2, variance=0.1, unit='cm'), - Parameter('name * test', 2, 'dm^2', 0.14, 0, 20), - Parameter('test * name', 2, 'dm^2', 0.14, 0, 20), + DescriptorNumber(display_name='test', value=2, variance=0.1, unit='cm'), + Parameter( + 2, unit='dm^2', variance=0.14, min=0, max=20, display_name='name * test' + ), + Parameter( + 2, unit='dm^2', variance=0.14, min=0, max=20, display_name='test * name' + ), ), ( - DescriptorNumber(name='test', value=0, variance=0.1, unit='cm'), - DescriptorNumber('name * test', 0, 'dm^2', 0.1), - DescriptorNumber('test * name', 0, 'dm^2', 0.1), + DescriptorNumber(display_name='test', value=0, variance=0.1, unit='cm'), + DescriptorNumber(0, unit='dm^2', variance=0.1, display_name='name * test'), + DescriptorNumber(0, unit='dm^2', variance=0.1, display_name='test * name'), ), ], ids=['regular', 'zero_value'], @@ -1514,16 +1513,12 @@ def test_multiplication_with_parameter_nan_cases(self, test, expected, expected_ def test_multiplication_with_descriptor_number( self, parameter: Parameter, test, expected, expected_reverse ): - # When - parameter._callback = property() - - # Then + # When Then result = parameter * test result_reverse = test * parameter # Expect assert type(result) == type(expected) - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1532,7 +1527,6 @@ def test_multiplication_with_descriptor_number( assert result.max == expected.max assert type(result_reverse) == type(expected_reverse) - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == expected_reverse.variance @@ -1545,13 +1539,13 @@ def test_multiplication_with_descriptor_number( [ ( 2, - Parameter('name * 2', 2, 'm', 0.04, 0, 20), - Parameter('2 * name', 2, 'm', 0.04, 0, 20), + Parameter(2, unit='m', variance=0.04, min=0, max=20, display_name='name * 2'), + Parameter(2, unit='m', variance=0.04, min=0, max=20, display_name='2 * name'), ), ( 0, - DescriptorNumber('name * 0', 0, 'm', 0), - DescriptorNumber('0 * name', 0, 'm', 0), + DescriptorNumber(0, unit='m', variance=0, display_name='name * 0'), + DescriptorNumber(0, unit='m', variance=0, display_name='0 * name'), ), ], ids=['regular', 'zero_value'], @@ -1559,16 +1553,12 @@ def test_multiplication_with_descriptor_number( def test_multiplication_with_scalar( self, parameter: Parameter, test, expected, expected_reverse ): - # When - parameter._callback = property() - - # Then + # When Then result = parameter * test result_reverse = test * parameter # Expect assert type(result) == type(expected) - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1576,7 +1566,6 @@ def test_multiplication_with_scalar( assert result.min == expected.min assert result.max == expected.max - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == expected_reverse.variance @@ -1588,34 +1577,62 @@ def test_multiplication_with_scalar( 'test, expected, expected_reverse', [ ( - Parameter('test', 2, 's', 0.01, -10, 20), - Parameter('name / test', 0.5, 'm/s', 0.003125, -np.inf, np.inf), - Parameter('test / name', 2, 's/m', 0.05, -np.inf, np.inf), + Parameter(2, unit='s', variance=0.01, min=-10, max=20, display_name='test'), + Parameter( + 0.5, + unit='m/s', + variance=0.003125, + min=-np.inf, + max=np.inf, + display_name='name / test', + ), + Parameter( + 2, + unit='s/m', + variance=0.05, + min=-np.inf, + max=np.inf, + display_name='test / name', + ), ), ( - Parameter('test', 2, 's', 0.01, 0, 20), - Parameter('name / test', 0.5, 'm/s', 0.003125, 0.0, np.inf), - Parameter('test / name', 2, 's/m', 0.05, 0.0, np.inf), + Parameter(2, unit='s', variance=0.01, min=0, max=20, display_name='test'), + Parameter( + 0.5, + unit='m/s', + variance=0.003125, + min=0.0, + max=np.inf, + display_name='name / test', + ), + Parameter( + 2, unit='s/m', variance=0.05, min=0.0, max=np.inf, display_name='test / name' + ), ), ( - Parameter('test', -2, 's', 0.01, -10, 0), - Parameter('name / test', -0.5, 'm/s', 0.003125, -np.inf, 0.0), - Parameter('test / name', -2, 's/m', 0.05, -np.inf, 0.0), + Parameter(-2, unit='s', variance=0.01, min=-10, max=0, display_name='test'), + Parameter( + -0.5, + unit='m/s', + variance=0.003125, + min=-np.inf, + max=0.0, + display_name='name / test', + ), + Parameter( + -2, unit='s/m', variance=0.05, min=-np.inf, max=0.0, display_name='test / name' + ), ), ], ids=['crossing_zero', 'only_positive', 'only_negative'], ) def test_division_with_parameter(self, parameter: Parameter, test, expected, expected_reverse): - # When - parameter._callback = property() - - # Then + # When Then result = parameter / test result_reverse = test / parameter # Expect assert type(result) == Parameter - assert result.name == result.unique_name assert result.value == pytest.approx(expected.value) assert result.unit == expected.unit assert result.variance == pytest.approx(expected.variance) @@ -1623,7 +1640,6 @@ def test_division_with_parameter(self, parameter: Parameter, test, expected, exp assert result.max == expected.max assert type(result) == Parameter - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == pytest.approx(expected_reverse.value) assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == pytest.approx(expected_reverse.variance) @@ -1634,19 +1650,35 @@ def test_division_with_parameter(self, parameter: Parameter, test, expected, exp 'first, second, expected', [ ( - Parameter('name', 1, 'm', 0.01, -10, 20), - Parameter('test', -2, 's', 0.01, -10, 0), - Parameter('name / test', -0.5, 'm/s', 0.003125, -np.inf, np.inf), + Parameter(1, unit='m', variance=0.01, min=-10, max=20, display_name='name'), + Parameter(-2, unit='s', variance=0.01, min=-10, max=0, display_name='test'), + Parameter( + -0.5, + unit='m/s', + variance=0.003125, + min=-np.inf, + max=np.inf, + display_name='name / test', + ), ), ( - Parameter('name', -10, 'm', 0.01, -20, -10), - Parameter('test', -2, 's', 0.01, -10, 0), - Parameter('name / test', 5.0, 'm/s', 0.065, 1, np.inf), + Parameter(-10, unit='m', variance=0.01, min=-20, max=-10, display_name='name'), + Parameter(-2, unit='s', variance=0.01, min=-10, max=0, display_name='test'), + Parameter( + 5.0, unit='m/s', variance=0.065, min=1, max=np.inf, display_name='name / test' + ), ), ( - Parameter('name', 10, 'm', 0.01, 10, 20), - Parameter('test', -20, 's', 0.01, -20, -10), - Parameter('name / test', -0.5, 'm/s', 3.125e-5, -2, -0.5), + Parameter(10, unit='m', variance=0.01, min=10, max=20, display_name='name'), + Parameter(-20, unit='s', variance=0.01, min=-20, max=-10, display_name='test'), + Parameter( + -0.5, + unit='m/s', + variance=3.125e-5, + min=-2, + max=-0.5, + display_name='name / test', + ), ), ], ids=[ @@ -1660,7 +1692,6 @@ def test_division_with_parameter_remaining_cases(self, first, second, expected): result = first / second # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1671,14 +1702,20 @@ def test_division_with_parameter_remaining_cases(self, first, second, expected): 'test, expected, expected_reverse', [ ( - DescriptorNumber(name='test', value=2, variance=0.1, unit='s'), - Parameter('name / test', 0.5, 'm/s', 0.00875, 0, 5), - Parameter('test / name', 2, 's/m', 0.14, 0.2, np.inf), + DescriptorNumber(display_name='test', value=2, variance=0.1, unit='s'), + Parameter( + 0.5, unit='m/s', variance=0.00875, min=0, max=5, display_name='name / test' + ), + Parameter( + 2, unit='s/m', variance=0.14, min=0.2, max=np.inf, display_name='test / name' + ), ), ( 2, - Parameter('name / 2', 0.5, 'm', 0.0025, 0, 5), - Parameter('2 / name', 2, 'm**-1', 0.04, 0.2, np.inf), + Parameter(0.5, unit='m', variance=0.0025, min=0, max=5, display_name='name / 2'), + Parameter( + 2, unit='m**-1', variance=0.04, min=0.2, max=np.inf, display_name='2 / name' + ), ), ], ids=['descriptor_number', 'number'], @@ -1686,16 +1723,12 @@ def test_division_with_parameter_remaining_cases(self, first, second, expected): def test_division_with_descriptor_number_and_number( self, parameter: Parameter, test, expected, expected_reverse ): - # When - parameter._callback = property() - - # Then + # When Then result = parameter / test result_reverse = test / parameter # Expect assert type(result) == Parameter - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1703,7 +1736,6 @@ def test_division_with_descriptor_number_and_number( assert result.max == expected.max assert type(result_reverse) == Parameter - assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == expected_reverse.value assert result_reverse.unit == expected_reverse.unit assert result_reverse.variance == expected_reverse.variance @@ -1714,23 +1746,19 @@ def test_division_with_descriptor_number_and_number( 'test, expected', [ ( - DescriptorNumber(name='test', value=0, variance=0.1, unit='s'), - DescriptorNumber('test / name', 0.0, 's/m', 0.1), + DescriptorNumber(display_name='test', value=0, variance=0.1, unit='s'), + DescriptorNumber(0.0, unit='s/m', variance=0.1, display_name='test / name'), ), - (0, DescriptorNumber('0 / name', 0.0, '1/m', 0.0)), + (0, DescriptorNumber(0.0, unit='1/m', variance=0.0, display_name='0 / name')), ], ids=['descriptor_number', 'number'], ) def test_zero_value_divided_by_parameter(self, parameter: Parameter, test, expected): - # When - parameter._callback = property() - - # Then + # When Then result = test / parameter # Expect assert type(result) == DescriptorNumber - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1739,29 +1767,59 @@ def test_zero_value_divided_by_parameter(self, parameter: Parameter, test, expec 'first, second, expected', [ ( - DescriptorNumber('name', 1, 'm', 0.01), - Parameter('test', 2, 's', 0.1, -10, 10), - Parameter('name / test', 0.5, 'm/s', 0.00875, -np.inf, np.inf), + DescriptorNumber(1, unit='m', variance=0.01, display_name='name'), + Parameter(2, unit='s', variance=0.1, min=-10, max=10, display_name='test'), + Parameter( + 0.5, + unit='m/s', + variance=0.00875, + min=-np.inf, + max=np.inf, + display_name='name / test', + ), ), ( - DescriptorNumber('name', -1, 'm', 0.01), - Parameter('test', 2, 's', 0.1, 0, 10), - Parameter('name / test', -0.5, 'm/s', 0.00875, -np.inf, -0.1), + DescriptorNumber(-1, unit='m', variance=0.01, display_name='name'), + Parameter(2, unit='s', variance=0.1, min=0, max=10, display_name='test'), + Parameter( + -0.5, + unit='m/s', + variance=0.00875, + min=-np.inf, + max=-0.1, + display_name='name / test', + ), ), ( - DescriptorNumber('name', 1, 'm', 0.01), - Parameter('test', -2, 's', 0.1, -10, 0), - Parameter('name / test', -0.5, 'm/s', 0.00875, -np.inf, -0.1), + DescriptorNumber(1, unit='m', variance=0.01, display_name='name'), + Parameter(-2, unit='s', variance=0.1, min=-10, max=0, display_name='test'), + Parameter( + -0.5, + unit='m/s', + variance=0.00875, + min=-np.inf, + max=-0.1, + display_name='name / test', + ), ), ( - DescriptorNumber('name', -1, 'm', 0.01), - Parameter('test', -2, 's', 0.1, -10, 0), - Parameter('name / test', 0.5, 'm/s', 0.00875, 0.1, np.inf), + DescriptorNumber(-1, unit='m', variance=0.01, display_name='name'), + Parameter(-2, unit='s', variance=0.1, min=-10, max=0, display_name='test'), + Parameter( + 0.5, + unit='m/s', + variance=0.00875, + min=0.1, + max=np.inf, + display_name='name / test', + ), ), ( - DescriptorNumber('name', 1, 'm', 0.01), - Parameter('test', 2, 's', 0.1, 1, 10), - Parameter('name / test', 0.5, 'm/s', 0.00875, 0.1, 1), + DescriptorNumber(1, unit='m', variance=0.01, display_name='name'), + Parameter(2, unit='s', variance=0.1, min=1, max=10, display_name='test'), + Parameter( + 0.5, unit='m/s', variance=0.00875, min=0.1, max=1, display_name='name / test' + ), ), ], ids=[ @@ -1777,7 +1835,6 @@ def test_division_with_descriptor_number_missing_cases(self, first, second, expe result = first / second # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1786,12 +1843,11 @@ def test_division_with_descriptor_number_missing_cases(self, first, second, expe @pytest.mark.parametrize( 'test', - [0, DescriptorNumber('test', 0, 's', 0.1)], + [0, DescriptorNumber(0, unit='s', variance=0.1, display_name='test')], ids=['number', 'descriptor_number'], ) def test_divide_parameter_by_zero(self, parameter: Parameter, test): # When - parameter._callback = property() # Then Expect with pytest.raises(ZeroDivisionError): @@ -1799,8 +1855,8 @@ def test_divide_parameter_by_zero(self, parameter: Parameter, test): def test_divide_by_zero_value_parameter(self): # When - descriptor = DescriptorNumber('test', 1, 's', 0.1) - parameter = Parameter('name', 0, 'm', 0.01) + descriptor = DescriptorNumber(1, unit='s', variance=0.1, display_name='test') + parameter = Parameter(0, unit='m', variance=0.01, display_name='name') # Then Expect with pytest.raises(ZeroDivisionError): @@ -1809,14 +1865,41 @@ def test_divide_by_zero_value_parameter(self): @pytest.mark.parametrize( 'test, expected', [ - (3, Parameter('name ** 3', 125, 'm^3', 281.25, -125, 1000)), - (2, Parameter('name ** 2', 25, 'm^2', 5.0, 0, 100)), - (-1, Parameter('name ** -1', 0.2, '1/m', 8e-5, -np.inf, np.inf)), - (-2, Parameter('name ** -2', 0.04, '1/m^2', 1.28e-5, 0, np.inf)), - (0, DescriptorNumber('name ** 0', 1, 'dimensionless', 0)), ( - DescriptorNumber('test', 2), - Parameter('name ** test', 25, 'm^2', 5.0, 0, 100), + 3, + Parameter( + 125, unit='m^3', variance=281.25, min=-125, max=1000, display_name='name ** 3' + ), + ), + (2, Parameter(25, unit='m^2', variance=5.0, min=0, max=100, display_name='name ** 2')), + ( + -1, + Parameter( + 0.2, + unit='1/m', + variance=8e-5, + min=-np.inf, + max=np.inf, + display_name='name ** -1', + ), + ), + ( + -2, + Parameter( + 0.04, + unit='1/m^2', + variance=1.28e-5, + min=0, + max=np.inf, + display_name='name ** -2', + ), + ), + (0, DescriptorNumber(1, unit='dimensionless', variance=0, display_name='name ** 0')), + ( + DescriptorNumber(2, display_name='test'), + Parameter( + 25, unit='m^2', variance=5.0, min=0, max=100, display_name='name ** test' + ), ), ], ids=[ @@ -1830,14 +1913,13 @@ def test_divide_by_zero_value_parameter(self): ) def test_power_of_parameter(self, test, expected): # When - parameter = Parameter('name', 5, 'm', 0.05, -5, 10) + parameter = Parameter(5, unit='m', variance=0.05, min=-5, max=10, display_name='name') # Then result = parameter**test # Expect assert type(result) == type(expected) - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1849,46 +1931,73 @@ def test_power_of_parameter(self, test, expected): 'test, exponent, expected', [ ( - Parameter('name', 5, 'm', 0.05, 0, 10), + Parameter(5, unit='m', variance=0.05, min=0, max=10, display_name='name'), -1, - Parameter('name ** -1', 0.2, '1/m', 8e-5, 0.1, np.inf), + Parameter( + 0.2, unit='1/m', variance=8e-5, min=0.1, max=np.inf, display_name='name ** -1' + ), ), ( - Parameter('name', -5, 'm', 0.05, -5, 0), + Parameter(-5, unit='m', variance=0.05, min=-5, max=0, display_name='name'), -1, - Parameter('name ** -1', -0.2, '1/m', 8e-5, -np.inf, -0.2), + Parameter( + -0.2, + unit='1/m', + variance=8e-5, + min=-np.inf, + max=-0.2, + display_name='name ** -1', + ), ), ( - Parameter('name', 5, 'm', 0.05, 5, 10), + Parameter(5, unit='m', variance=0.05, min=5, max=10, display_name='name'), -1, - Parameter('name ** -1', 0.2, '1/m', 8e-5, 0.1, 0.2), + Parameter( + 0.2, unit='1/m', variance=8e-5, min=0.1, max=0.2, display_name='name ** -1' + ), ), ( - Parameter('name', -5, 'm', 0.05, -10, -5), + Parameter(-5, unit='m', variance=0.05, min=-10, max=-5, display_name='name'), -1, - Parameter('name ** -1', -0.2, '1/m', 8e-5, -0.2, -0.1), + Parameter( + -0.2, unit='1/m', variance=8e-5, min=-0.2, max=-0.1, display_name='name ** -1' + ), ), ( - Parameter('name', -5, 'm', 0.05, -10, -5), + Parameter(-5, unit='m', variance=0.05, min=-10, max=-5, display_name='name'), -2, - Parameter('name ** -2', 0.04, '1/m^2', 1.28e-5, 0.01, 0.04), + Parameter( + 0.04, + unit='1/m^2', + variance=1.28e-5, + min=0.01, + max=0.04, + display_name='name ** -2', + ), ), ( - Parameter('name', 5, '', 0.1, 1, 10), + Parameter(5, unit='', variance=0.1, min=1, max=10, display_name='name'), 0.3, Parameter( - 'name ** 0.3', 1.6206565966927624, - '', - 0.0009455500095853564, - 1, - 1.9952623149688795, + unit='', + variance=0.0009455500095853564, + min=1, + max=1.9952623149688795, + display_name='name ** 0.3', ), ), ( - Parameter('name', 5, '', 0.1), + Parameter(5, unit='', variance=0.1, display_name='name'), 0.5, - Parameter('name ** 0.5', 2.23606797749979, '', 0.005, 0, np.inf), + Parameter( + 2.23606797749979, + unit='', + variance=0.005, + min=0, + max=np.inf, + display_name='name ** 0.5', + ), ), ], ids=[ @@ -1906,7 +2015,6 @@ def test_power_of_diffent_parameters(self, test, exponent, expected): result = test**exponent # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance @@ -1917,17 +2025,17 @@ def test_power_of_diffent_parameters(self, test, exponent, expected): 'parameter, exponent, expected', [ ( - Parameter('name', 5, 'm'), - DescriptorNumber('test', 2, unit='s'), + Parameter(5, unit='m', display_name='name'), + DescriptorNumber(2, unit='s', display_name='test'), UnitError, ), ( - Parameter('name', 5, 'm'), - DescriptorNumber('test', 2, variance=0.01), + Parameter(5, unit='m', display_name='name'), + DescriptorNumber(2, variance=0.01, display_name='test'), ValueError, ), - (Parameter('name', 5, 'm'), 0.5, UnitError), - (Parameter('name', -5, ''), 0.5, ValueError), + (Parameter(5, unit='m', display_name='name'), 0.5, UnitError), + (Parameter(-5, unit='', display_name='name'), 0.5, ValueError), ], ids=[ 'exponent_unit', @@ -1943,13 +2051,12 @@ def test_power_exceptions(self, parameter, exponent, expected): def test_negation(self): # When - parameter = Parameter('name', 5, 'm', 0.05, -5, 10) + parameter = Parameter(5, unit='m', variance=0.05, min=-5, max=10, display_name='name') # Then result = -parameter # Expect - assert result.name == result.unique_name assert result.value == -5 assert result.unit == 'm' assert result.variance == 0.05 @@ -1960,12 +2067,12 @@ def test_negation(self): 'test, expected', [ ( - Parameter('name', -5, 'm', 0.05, -10, -5), - Parameter('abs(name)', 5, 'm', 0.05, 5, 10), + Parameter(-5, unit='m', variance=0.05, min=-10, max=-5, display_name='name'), + Parameter(5, unit='m', variance=0.05, min=5, max=10, display_name='abs(name)'), ), ( - Parameter('name', 5, 'm', 0.05, -10, 10), - Parameter('abs(name)', 5, 'm', 0.05, 0, 10), + Parameter(5, unit='m', variance=0.05, min=-10, max=10, display_name='name'), + Parameter(5, unit='m', variance=0.05, min=0, max=10, display_name='abs(name)'), ), ], ids=['pure_negative', 'crossing_zero'], @@ -1975,9 +2082,27 @@ def test_abs(self, test, expected): result = abs(test) # Expect - assert result.name == result.unique_name assert result.value == expected.value assert result.unit == expected.unit assert result.variance == expected.variance assert result.min == expected.min assert result.max == expected.max + + +@pytest.mark.parametrize('label', [None, '', 'label'], ids=['unset', 'empty', 'explicit']) +def test_display_name_serialization_preserves_unset_state(label): + # Given + p = Parameter(2, display_name=label) + + # When + encoded = p.to_dict() + + # Then Expect: an unset label is omitted rather than stored as the fallback + if label is None: + assert 'display_name' not in encoded + else: + assert encoded['display_name'] == label + + restored = Parameter.from_dict(p.to_dict(skip=['unique_name'])) + assert restored.unique_name != p.unique_name + assert restored.display_name == (restored.unique_name if label is None else label) diff --git a/tests/unit/variable/test_parameter_dependency_serialization.py b/tests/unit/variable/test_parameter_dependency_serialization.py index 62feb5f3..8e855d48 100644 --- a/tests/unit/variable/test_parameter_dependency_serialization.py +++ b/tests/unit/variable/test_parameter_dependency_serialization.py @@ -18,9 +18,9 @@ class TestParameterDependencySerialization: @pytest.fixture def clear_global_map(self): """This fixture pattern: - - Clears the map before each test (clean slate) + - Clears the registry before each test (clean slate) - Yields control to the test - - Clears the map after each test (cleanup) + - Clears the registry after each test (cleanup) Dependency serialization tests require more robust setup-yield-cleanup pattern because they involve complex @@ -28,19 +28,17 @@ def clear_global_map(self): and dependency resolution that are particularly sensitive to global state contamination. """ - # The global map uses weakref.WeakValueDictionary() for object storage, - # but also maintains strong references in __type_dict that need explicit cleanup. - global_object.map._clear() + global_object.registry.clear() yield # final cleanup after test - global_object.map._clear() + global_object.registry.clear() def test_independent_parameter_serialization(self, clear_global_map): """Test that independent parameters serialize normally without dependency info.""" - param = Parameter(name='test', value=5.0, unit='m', min=0, max=10) + param = Parameter(display_name='test', value=5.0, unit='m', min=0, max=10) # Serialize - serialized = param.as_dict() + serialized = param.to_dict() # Should not contain dependency fields assert '_dependency_string' not in serialized @@ -48,11 +46,11 @@ def test_independent_parameter_serialization(self, clear_global_map): assert '_independent' not in serialized # Deserialize - global_object.map._clear() + global_object.registry.clear() new_param = Parameter.from_dict(serialized) # Should be identical - assert new_param.name == param.name + assert new_param.display_name == param.display_name assert new_param.value == param.value assert new_param.unit == param.unit assert new_param.independent is True @@ -60,15 +58,15 @@ def test_independent_parameter_serialization(self, clear_global_map): def test_dependent_parameter_serialization(self, clear_global_map): """Test serialization of parameters with dependencies.""" # Create independent parameter - a = Parameter(name='a', value=2.0, unit='m', min=0, max=10) + a = Parameter(display_name='a', value=2.0, unit='m', min=0, max=10) # Create dependent parameter b = Parameter.from_dependency( - name='b', dependency_expression='2 * a', dependency_map={'a': a}, unit='m' + display_name='b', dependency_expression='2 * a', dependency_map={'a': a}, unit='m' ) # Serialize dependent parameter - serialized = b.as_dict() + serialized = b.to_dict() # Should contain dependency information assert serialized['_dependency_string'] == '2 * a' @@ -78,7 +76,7 @@ def test_dependent_parameter_serialization(self, clear_global_map): assert serialized['_independent'] is False # Deserialize - global_object.map._clear() + global_object.registry.clear() new_b = Parameter.from_dict(serialized) # Should have pending dependency info @@ -92,11 +90,11 @@ def test_dependent_parameter_serialization(self, clear_global_map): def test_dependency_resolution_after_deserialization(self, clear_global_map): """Test that dependencies are properly resolved after deserialization.""" # Create test parameters with dependencies - a = Parameter(name='a', value=2.0, unit='m', min=0, max=10) - b = Parameter(name='b', value=3.0, unit='m', min=0, max=10) + a = Parameter(display_name='a', value=2.0, unit='m', min=0, max=10) + b = Parameter(display_name='b', value=3.0, unit='m', min=0, max=10) c = Parameter.from_dependency( - name='c', + display_name='c', dependency_expression='a + b', dependency_map={'a': a, 'b': b}, unit='m', @@ -106,10 +104,10 @@ def test_dependency_resolution_after_deserialization(self, clear_global_map): assert c.value == 5.0 # 2 + 3 # Serialize all parameters - params_data = {'a': a.as_dict(), 'b': b.as_dict(), 'c': c.as_dict()} + params_data = {'a': a.to_dict(), 'b': b.to_dict(), 'c': c.to_dict()} # Clear and deserialize (manual approach) - global_object.map._clear() + global_object.registry.clear() new_params = {} for name, data in params_data.items(): new_params[name] = Parameter.from_dict(data) @@ -122,7 +120,7 @@ def test_dependency_resolution_after_deserialization(self, clear_global_map): resolve_all_parameter_dependencies(new_params) # Alternative simplified approach using the helper function: - # global_object.map._clear() + # global_object.registry.clear() # new_params = deserialize_and_resolve_parameters(params_data) # After resolution, c should be dependent and functional @@ -136,11 +134,11 @@ def test_dependency_resolution_after_deserialization(self, clear_global_map): def test_dependency_resolution_after_deserialization_desired_unit(self, clear_global_map): """Test that dependencies are properly resolved after deserialization.""" # Create test parameters with dependencies - a = Parameter(name='a', value=2.0, unit='m', min=0, max=10) - b = Parameter(name='b', value=3.0, unit='m', min=0, max=10) + a = Parameter(display_name='a', value=2.0, unit='m', min=0, max=10) + b = Parameter(display_name='b', value=3.0, unit='m', min=0, max=10) c = Parameter.from_dependency( - name='c', + display_name='c', dependency_expression='a + b', dependency_map={'a': a, 'b': b}, desired_unit='cm', @@ -151,10 +149,10 @@ def test_dependency_resolution_after_deserialization_desired_unit(self, clear_gl assert c.unit == 'cm' # Serialize all parameters - params_data = {'a': a.as_dict(), 'b': b.as_dict(), 'c': c.as_dict()} + params_data = {'a': a.to_dict(), 'b': b.to_dict(), 'c': c.to_dict()} # Clear and deserialize (manual approach) - global_object.map._clear() + global_object.registry.clear() new_params = {} for name, data in params_data.items(): new_params[name] = Parameter.from_dict(data) @@ -167,7 +165,7 @@ def test_dependency_resolution_after_deserialization_desired_unit(self, clear_gl resolve_all_parameter_dependencies(new_params) # Alternative simplified approach using the helper function: - # global_object.map._clear() + # global_object.registry.clear() # new_params = deserialize_and_resolve_parameters(params_data) # After resolution, c should be dependent and functional @@ -182,18 +180,16 @@ def test_dependency_resolution_after_deserialization_desired_unit(self, clear_gl def test_unique_name_dependency_serialization(self, clear_global_map): """Test serialization of dependencies using unique names.""" - a = Parameter(name='a', value=3.0, unit='m', min=0, max=10) + a = Parameter(display_name='a', value=3.0, unit='m', min=0, max=10) # Create dependent parameter using unique name b = Parameter.from_dependency( - name='b', - dependency_expression='2 * "Parameter_0"', # Using unique name - unit='m', + display_name='b', dependency_expression='2 * "Parameter_0"', unit='m' ) # Serialize both parameters - a_serialized = a.as_dict() - b_serialized = b.as_dict() + a_serialized = a.to_dict() + b_serialized = b.to_dict() # Should contain unique name mapping assert b_serialized['_dependency_string'] == '2 * __Parameter_0__' @@ -204,9 +200,9 @@ def test_unique_name_dependency_serialization(self, clear_global_map): ) # Deserialize both and resolve - global_object.map._clear() + global_object.registry.clear() c = Parameter( - name='c', value=0.0 + display_name='c', value=0.0 ) # Dummy to occupy unique name, to force new unique_names # Remove unique_name from serialized data to force generation of new unique names @@ -225,11 +221,11 @@ def test_unique_name_dependency_serialization(self, clear_global_map): def test_json_serialization_roundtrip(self, clear_global_map): """Test that parameter dependencies survive JSON serialization.""" # Create parameters with dependencies - length = Parameter(name='length', value=10.0, unit='m', min=0, max=100) - width = Parameter(name='width', value=5.0, unit='m', min=0, max=50) + length = Parameter(display_name='length', value=10.0, unit='m', min=0, max=100) + width = Parameter(display_name='width', value=5.0, unit='m', min=0, max=50) area = Parameter.from_dependency( - name='area', + display_name='area', dependency_expression='length * width', dependency_map={'length': length, 'width': width}, unit='m^2', @@ -237,14 +233,14 @@ def test_json_serialization_roundtrip(self, clear_global_map): # Serialize to JSON params_data = { - 'length': length.as_dict(), - 'width': width.as_dict(), - 'area': area.as_dict(), + 'length': length.to_dict(), + 'width': width.to_dict(), + 'area': area.to_dict(), } json_str = json.dumps(params_data, default=str) # Deserialize from JSON - global_object.map._clear() + global_object.registry.clear() loaded_data = json.loads(json_str) new_params = {} for name, data in loaded_data.items(): @@ -263,14 +259,14 @@ def test_json_serialization_roundtrip(self, clear_global_map): def test_multiple_dependent_parameters(self, clear_global_map): """Test serialization with multiple dependent parameters.""" # Create a chain of dependencies - x = Parameter(name='x', value=2.0, unit='m', min=0, max=10) + x = Parameter(display_name='x', value=2.0, unit='m', min=0, max=10) y = Parameter.from_dependency( - name='y', dependency_expression='2 * x', dependency_map={'x': x}, unit='m' + display_name='y', dependency_expression='2 * x', dependency_map={'x': x}, unit='m' ) z = Parameter.from_dependency( - name='z', + display_name='z', dependency_expression='y + x', dependency_map={'y': y, 'x': x}, unit='m', @@ -281,10 +277,10 @@ def test_multiple_dependent_parameters(self, clear_global_map): assert z.value == 6.0 # 4 + 2 # Serialize all - params_data = {'x': x.as_dict(), 'y': y.as_dict(), 'z': z.as_dict()} + params_data = {'x': x.to_dict(), 'y': y.to_dict(), 'z': z.to_dict()} # Deserialize and resolve - global_object.map._clear() + global_object.registry.clear() new_params = {} for name, data in params_data.items(): new_params[name] = Parameter.from_dict(data) @@ -306,12 +302,10 @@ def test_dependency_with_descriptor_number(self, clear_global_map): # When - x = DescriptorNumber(name='x', value=3.0, unit='m') - y = Parameter(name='y', value=4.0, unit='m') + x = DescriptorNumber(display_name='x', value=3.0, unit='m') + y = Parameter(display_name='y', value=4.0, unit='m') z = Parameter.from_dependency( - name='z', - dependency_expression='x + y', - dependency_map={'x': x, 'y': y}, + display_name='z', dependency_expression='x + y', dependency_map={'x': x, 'y': y} ) # Verify original functionality @@ -319,9 +313,9 @@ def test_dependency_with_descriptor_number(self, clear_global_map): # Then # Serialize all - params_data = {'x': x.as_dict(), 'y': y.as_dict(), 'z': z.as_dict()} + params_data = {'x': x.to_dict(), 'y': y.to_dict(), 'z': z.to_dict()} # Deserialize and resolve - global_object.map._clear() + global_object.registry.clear() new_params = {} for name, data in params_data.items(): if name == 'x': @@ -344,14 +338,14 @@ def test_dependency_with_descriptor_number(self, clear_global_map): def test_get_parameters_with_pending_dependencies(self, clear_global_map): """Test utility function for finding parameters with pending dependencies.""" # Create parameters - a = Parameter(name='a', value=1.0, unit='m') + a = Parameter(display_name='a', value=1.0, unit='m') b = Parameter.from_dependency( - name='b', dependency_expression='2 * a', dependency_map={'a': a}, unit='m' + display_name='b', dependency_expression='2 * a', dependency_map={'a': a}, unit='m' ) # Serialize and deserialize - params_data = {'a': a.as_dict(), 'b': b.as_dict()} - global_object.map._clear() + params_data = {'a': a.to_dict(), 'b': b.to_dict()} + global_object.registry.clear() new_params = {} for name, data in params_data.items(): new_params[name] = Parameter.from_dict(data) @@ -360,7 +354,7 @@ def test_get_parameters_with_pending_dependencies(self, clear_global_map): pending = get_parameters_with_pending_dependencies(new_params) assert len(pending) == 1 - assert pending[0].name == 'b' + assert pending[0].display_name == 'b' assert hasattr(pending[0], '_pending_dependency_string') # After resolution, should be empty @@ -370,44 +364,22 @@ def test_get_parameters_with_pending_dependencies(self, clear_global_map): def test_error_handling_missing_dependency(self, clear_global_map): """Test error handling when dependency cannot be resolved.""" - a = Parameter(name='a', value=1.0, unit='m') + a = Parameter(display_name='a', value=1.0, unit='m') b = Parameter.from_dependency( - name='b', dependency_expression='2 * a', dependency_map={'a': a}, unit='m' + display_name='b', dependency_expression='2 * a', dependency_map={'a': a}, unit='m' ) # Serialize b but not a - b_data = b.as_dict() + b_data = b.to_dict() - # Deserialize without a in the global map - global_object.map._clear() + # Deserialize without a in the global registry + global_object.registry.clear() new_b = Parameter.from_dict(b_data) # Should raise error when trying to resolve with pytest.raises(ValueError, match='Cannot find parameter with serializer_id'): new_b.resolve_pending_dependencies() - def test_backward_compatibility_base_deserializer(self, clear_global_map): - """Test that the base deserializer path still works for dependent parameters.""" - from easyscience.io.serializer_dict import SerializerDict - - # Create dependent parameter - a = Parameter(name='a', value=2.0, unit='m') - b = Parameter.from_dependency( - name='b', dependency_expression='3 * a', dependency_map={'a': a}, unit='m' - ) - - # Use base serializer path (SerializerDict.decode) - serialized = b.encode(encoder=SerializerDict) - global_object.map._clear() - - # This should not raise the "_independent" error anymore - deserialized = SerializerDict.decode(serialized) - - # Should be a valid Parameter (but without dependency resolution) - assert isinstance(deserialized, Parameter) - assert deserialized.name == 'b' - assert deserialized.independent is True # Base path doesn't handle dependencies - @pytest.mark.parametrize( 'order', [['x', 'y', 'z'], ['z', 'x', 'y'], ['y', 'z', 'x'], ['z', 'y', 'x']], @@ -422,11 +394,11 @@ def test_serializer_id_system_order_independence(self, clear_global_map, order): """Test that dependency IDs allow parameters to be loaded in any order.""" # WHEN # Create parameters with dependencies - x = Parameter(name='x', value=5.0, unit='m', min=0, max=20) - y = Parameter(name='y', value=10.0, unit='m', min=0, max=30) + x = Parameter(display_name='x', value=5.0, unit='m', min=0, max=20) + y = Parameter(display_name='y', value=10.0, unit='m', min=0, max=30) z = Parameter.from_dependency( - name='z', + display_name='z', dependency_expression='x * y', dependency_map={'x': x, 'y': y}, unit='m^2', @@ -440,7 +412,7 @@ def test_serializer_id_system_order_independence(self, clear_global_map, order): y_dep_id = y._DescriptorNumber__serializer_id # Serialize all parameters - params_data = {'x': x.as_dict(), 'y': y.as_dict(), 'z': z.as_dict()} + params_data = {'x': x.to_dict(), 'y': y.to_dict(), 'z': z.to_dict()} # Verify dependency IDs are in serialized data assert params_data['x']['__serializer_id'] == x_dep_id @@ -449,7 +421,7 @@ def test_serializer_id_system_order_independence(self, clear_global_map, order): assert '_dependency_map_serializer_ids' in params_data['z'] # THEN - global_object.map._clear() + global_object.registry.clear() new_params = {} # Load in the specified order @@ -478,11 +450,11 @@ def test_serializer_id_system_order_independence(self, clear_global_map, order): def test_deserialize_and_resolve_parameters_helper(self, clear_global_map): """Test the convenience helper function for deserialization and dependency resolution.""" # Create test parameters with dependencies - a = Parameter(name='a', value=2.0, unit='m', min=0, max=10) - b = Parameter(name='b', value=3.0, unit='m', min=0, max=10) + a = Parameter(display_name='a', value=2.0, unit='m', min=0, max=10) + b = Parameter(display_name='b', value=3.0, unit='m', min=0, max=10) c = Parameter.from_dependency( - name='c', + display_name='c', dependency_expression='a + b', dependency_map={'a': a, 'b': b}, unit='m', @@ -492,10 +464,10 @@ def test_deserialize_and_resolve_parameters_helper(self, clear_global_map): assert c.value == 5.0 # 2 + 3 # Serialize all parameters - params_data = {'a': a.as_dict(), 'b': b.as_dict(), 'c': c.as_dict()} + params_data = {'a': a.to_dict(), 'b': b.to_dict(), 'c': c.to_dict()} - # Clear global map - global_object.map._clear() + # Clear global registry + global_object.registry.clear() # Use the helper function instead of manual deserialization + resolution new_params = deserialize_and_resolve_parameters(params_data) @@ -507,16 +479,16 @@ def test_deserialize_and_resolve_parameters_helper(self, clear_global_map): assert 'c' in new_params # Check that independent parameters work - assert new_params['a'].name == 'a' + assert new_params['a'].display_name == 'a' assert new_params['a'].value == 2.0 assert new_params['a'].independent is True - assert new_params['b'].name == 'b' + assert new_params['b'].display_name == 'b' assert new_params['b'].value == 3.0 assert new_params['b'].independent is True # Check that dependent parameter is properly resolved - assert new_params['c'].name == 'c' + assert new_params['c'].display_name == 'c' assert new_params['c'].value == 5.0 # 2 + 3 assert new_params['c'].independent is False @@ -527,3 +499,29 @@ def test_deserialize_and_resolve_parameters_helper(self, clear_global_map): # Verify no pending dependencies remain pending = get_parameters_with_pending_dependencies(new_params) assert len(pending) == 0 + + def test_nested_dependent_parameter_round_trip(self, clear_global_map): + """A dependent parameter held by a model keeps its dependency through to_dict/from_dict.""" + from easy_test_models import PairModel + + a = Parameter(display_name='a', value=2.0, unit='m') + b = Parameter.from_dependency( + display_name='b', dependency_expression='3 * a', dependency_map={'a': a}, unit='m' + ) + model = PairModel(a=a, b=b) + + model_dict = model.to_dict() + + # The nested parameter was serialized by its own to_dict + assert model_dict['b']['_independent'] is False + assert model_dict['b']['_dependency_string'] == '3 * a' + assert '_dependency_map_serializer_ids' in model_dict['b'] + + global_object.registry.clear() + new_model = PairModel.from_dict(model_dict) + new_model.b.resolve_pending_dependencies() + + assert new_model.b.independent is False + assert new_model.b.value == 6.0 + new_model.a.value = 5.0 + assert new_model.b.value == 15.0