diff --git a/doc/release_notes.rst b/doc/release_notes.rst index b4a92e64..d256c2f1 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,7 @@ Upcoming Version - Comparison operators (``==``, ``<=``, ``>=``) fill missing RHS coords with NaN (no constraint created) - Fixes crash on ``subset + var`` / ``subset + expr`` reverse addition - Fixes superset DataArrays expanding result coords beyond the variable's coordinate space +* When passing DataArray bounds to ``add_variables`` with explicit ``coords``, the ``coords`` parameter now defines the variable's coordinates. DataArray bounds are validated against these coords (raises ``ValueError`` on mismatch) and broadcast to missing dimensions. Previously, the ``coords`` parameter was silently ignored for DataArray inputs. * Add ``add_piecewise_constraints()`` with SOS2, incremental, LP, and disjunctive formulations (``linopy.piecewise(x, x_pts, y_pts) == y``). * Add ``linopy.piecewise()`` to create piecewise linear function descriptors (`PiecewiseExpression`) from separate x/y breakpoint arrays. * Add ``linopy.breakpoints()`` factory for convenient breakpoint construction from lists, Series, DataFrames, DataArrays, or dicts. Supports slopes mode. @@ -28,7 +29,6 @@ Version 0.6.5 * Expose the knitro context to allow for more flexible use of the knitro python API. - Version 0.6.4 -------------- diff --git a/linopy/common.py b/linopy/common.py index 09f67355..513c935f 100644 --- a/linopy/common.py +++ b/linopy/common.py @@ -9,7 +9,7 @@ import operator import os -from collections.abc import Callable, Generator, Hashable, Iterable, Sequence +from collections.abc import Callable, Generator, Hashable, Iterable, Mapping, Sequence from functools import partial, reduce, wraps from pathlib import Path from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload @@ -128,6 +128,89 @@ def get_from_iterable(lst: DimsLike | None, index: int) -> Any | None: return lst[index] if 0 <= index < len(lst) else None +def _coords_to_mapping(coords: CoordsLike, dims: DimsLike | None = None) -> Mapping: + """ + Normalize coords to a Mapping. + + If coords is already a Mapping, return as-is. If it's a sequence, + convert to a dict using dims (if provided) or Index names. + """ + if isinstance(coords, Mapping): + return coords + seq = list(coords) + if dims is not None: + dim_names: list[str] = [dims] if isinstance(dims, str) else list(dims) # type: ignore[arg-type] + if len(dim_names) != len(seq): + raise ValueError( + f"Length of dims ({len(dim_names)}) does not match " + f"length of coords sequence ({len(seq)})." + ) + return dict(zip(dim_names, seq)) + result: dict = {} + for c in seq: + if not hasattr(c, "name") or not c.name: + warn( + f"Coordinate {c!r} has no .name attribute and will be ignored. " + "Pass dims explicitly or use named indexes (e.g. pd.RangeIndex).", + UserWarning, + stacklevel=3, + ) + continue + if c.name in result: + raise ValueError( + f"Duplicate coordinate name '{c.name}' in coords sequence." + ) + result[c.name] = c + return result + + +def _validate_dataarray_coords( + arr: DataArray, + coords: CoordsLike, + dims: DimsLike | None = None, +) -> None: + """ + Validate a DataArray against expected coords (strict). + + - Shared dimensions must have matching coordinates (raises ValueError). + - Extra dimensions in the DataArray always raise ValueError. + + This function only validates — it does not modify ``arr``. + Expansion of missing dims should be done before calling this. + """ + expected = _coords_to_mapping(coords, dims) + if not expected: + return + + # Filter to dimension coordinates only — skip non-dimension coords + # like MultiIndex levels (level1, level2) that share a dimension + # with their parent index. + if hasattr(coords, "dims"): + # xarray Coordinates object — only keep entries that are dimensions + dim_coords = {k: v for k, v in expected.items() if k in coords.dims} + else: + dim_coords = dict(expected) + + extra = set(arr.dims) - set(dim_coords) + if extra: + raise ValueError(f"DataArray has extra dimensions not in coords: {extra}") + + for k, v in dim_coords.items(): + if k not in arr.dims: + continue + # Skip validation for multiindex dimensions — the level coords + # cannot be compared directly via pd.Index.equals + if isinstance(arr.indexes.get(k), pd.MultiIndex): + continue + actual = arr.coords[k] + v_idx = v if isinstance(v, pd.Index) else pd.Index(v) + if not actual.to_index().equals(v_idx): + raise ValueError( + f"Coordinates for dimension '{k}' do not match: " + f"expected {v_idx.tolist()}, got {actual.values.tolist()}" + ) + + def pandas_to_dataarray( arr: pd.DataFrame | pd.Series, coords: CoordsLike | None = None, @@ -203,37 +286,24 @@ def numpy_to_dataarray( if dims is not None and len(dims) and coords is not None: if isinstance(coords, list): - coords = dict(zip(dims, coords[: arr.ndim])) + coords = _coords_to_mapping(coords, dims) elif is_dict_like(coords): coords = {k: v for k, v in coords.items() if k in dims} return DataArray(arr, coords=coords, dims=dims, **kwargs) -def as_dataarray( +def _type_dispatch( arr: Any, coords: CoordsLike | None = None, dims: DimsLike | None = None, **kwargs: Any, ) -> DataArray: """ - Convert an object to a DataArray. + Convert arr to a DataArray via type dispatch. - Parameters - ---------- - arr: - The input object. - coords (Union[dict, list, None]): - The coordinates for the DataArray. If None, default coordinates will be used. - dims (Union[list, None]): - The dimensions for the DataArray. If None, the dimensions will be automatically generated. - **kwargs: - Additional keyword arguments to be passed to the DataArray constructor. - - Returns - ------- - DataArray: - The converted DataArray. + This is the shared conversion logic used by both ``as_dataarray`` + and ``ensure_dataarray``. It does NOT validate or expand dims. """ if isinstance(arr, pd.Series | pd.DataFrame): arr = pandas_to_dataarray(arr, coords=coords, dims=dims, **kwargs) @@ -241,11 +311,21 @@ def as_dataarray( arr = numpy_to_dataarray(arr, coords=coords, dims=dims, **kwargs) elif isinstance(arr, pl.Series): arr = numpy_to_dataarray(arr.to_numpy(), coords=coords, dims=dims, **kwargs) - elif isinstance(arr, np.number): - arr = DataArray(float(arr), coords=coords, dims=dims, **kwargs) - elif isinstance(arr, int | float | str | bool | list): - arr = DataArray(arr, coords=coords, dims=dims, **kwargs) - + elif isinstance(arr, np.number | int | float | str | bool | list): + if isinstance(arr, np.number): + arr = float(arr) + # For scalars with coords but no dims, infer dims from coords + # to avoid xarray's CoordinateValidationError + if coords is not None and dims is None and np.ndim(arr) == 0: + inferred = _coords_to_mapping(coords) + if inferred: + arr = DataArray( + arr, coords=coords, dims=list(inferred.keys()), **kwargs + ) + else: + arr = DataArray(arr, coords=coords, dims=dims, **kwargs) + else: + arr = DataArray(arr, coords=coords, dims=dims, **kwargs) elif not isinstance(arr, DataArray): supported_types = [ np.number, @@ -263,7 +343,119 @@ def as_dataarray( f"Unsupported type of arr: {type(arr)}. Supported types are: {supported_types_str}" ) - arr = fill_missing_coords(arr) + return fill_missing_coords(arr) + + +def _expand_missing_dims( + arr: DataArray, + coords: CoordsLike, + dims: DimsLike | None = None, +) -> DataArray: + """Broadcast missing dims via expand_dims.""" + expected = _coords_to_mapping(coords, dims) + if not expected: + return arr + + # Filter to dimension coordinates only — skip non-dimension coords + # like MultiIndex levels (level1, level2) that share a dimension + # with their parent index. + if hasattr(coords, "dims"): + dim_coords = {k: v for k, v in expected.items() if k in coords.dims} + else: + dim_coords = dict(expected) + + expand = {k: v for k, v in dim_coords.items() if k not in arr.dims} + if expand: + arr = arr.expand_dims(expand) + return arr + + +def ensure_dataarray( + arr: Any, + coords: CoordsLike | None = None, + dims: DimsLike | None = None, + **kwargs: Any, +) -> DataArray: + """ + Internal: convert any type to DataArray using coords for construction. + + Only does type conversion — no coord validation, no expand_dims. + Callers handle alignment and broadcasting themselves. + + Parameters + ---------- + arr: + The input object. + coords: + Coordinates used as construction hints for types without + their own coords (numpy, scalar, list). Optional. + dims: + Dimension names. + **kwargs: + Additional keyword arguments passed to the DataArray constructor. + + Returns + ------- + DataArray + """ + return _type_dispatch(arr, coords=coords, dims=dims, **kwargs) + + +def as_dataarray( + arr: Any, + coords: CoordsLike | None = None, + dims: DimsLike | None = None, + **kwargs: Any, +) -> DataArray: + """ + Convert an object to a DataArray with optional strict coord validation. + + When ``coords`` is provided, performs type conversion plus + validation: + + - For inputs with their own coordinates (DataArray, pandas, scalars): shared dims must match exactly + (``ValueError`` if not), extra dims are rejected (``ValueError``), missing dims are broadcast via ``expand_dims``. + - For raw arrays (numpy, list, polars): ``coords`` is applied + during construction (no validation needed). + + When ``coords`` is ``None``, performs pure type conversion only + (accepts any input type, including numpy/list/polars). + + Parameters + ---------- + arr : + The input object. + coords : CoordsLike, optional + Expected coordinates. When provided, used for construction + (numpy, scalar, list) and for validation (DataArray, pandas). + When ``None``, only type conversion is performed. + dims : DimsLike, optional + Dimension names. + **kwargs : + Additional keyword arguments passed to the DataArray + constructor. + + Returns + ------- + DataArray + """ + if coords is None: + return _type_dispatch(arr, coords=None, dims=dims, **kwargs) + + # Inputs that already have their own coordinates (DataArray, + # pandas) or scalars need validation against coords. Raw arrays + # (numpy, list) get coords applied during construction, so + # validation and expand_dims are not needed. + needs_validation = isinstance(arr, DataArray | pd.Series | pd.DataFrame) or ( + not isinstance(arr, np.ndarray | pl.Series | list) and np.ndim(arr) == 0 + ) + + arr = _type_dispatch(arr, coords=coords, dims=dims, **kwargs) + + if needs_validation: + arr = _expand_missing_dims(arr, coords, dims) + _validate_dataarray_coords(arr, coords, dims) + return arr diff --git a/linopy/expressions.py b/linopy/expressions.py index d2ae9022..c8e5d61f 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -46,11 +46,11 @@ from linopy.common import ( EmptyDeprecationWrapper, LocIndexer, - as_dataarray, assign_multiindex_safe, check_common_keys_values, check_has_nulls, check_has_nulls_polars, + ensure_dataarray, fill_missing_coords, filter_nulls_polars, forward_as_properties, @@ -355,7 +355,7 @@ def __init__(self, data: Dataset | Any | None, model: Model) -> None: da = xr.DataArray([], dims=[TERM_DIM]) data = Dataset({"coeffs": da, "vars": da, "const": 0.0}) elif isinstance(data, SUPPORTED_CONSTANT_TYPES): - const = as_dataarray(data) + const = ensure_dataarray(data) da = xr.DataArray([], dims=[TERM_DIM]) data = Dataset({"coeffs": da, "vars": da, "const": const}) elif not isinstance(data, Dataset): @@ -597,7 +597,7 @@ def _add_constant( # so that missing data does not silently propagate through arithmetic. if np.isscalar(other) and join is None: return self.assign(const=self.const.fillna(0) + other) - da = as_dataarray(other, coords=self.coords, dims=self.coord_dims) + da = ensure_dataarray(other, coords=self.coords, dims=self.coord_dims) self_const, da, needs_data_reindex = self._align_constant( da, fill_value=0, join=join ) @@ -626,7 +626,7 @@ def _apply_constant_op( - factor (other) is filled with fill_value (0 for mul, 1 for div) - coeffs and const are filled with 0 (additive identity) """ - factor = as_dataarray(other, coords=self.coords, dims=self.coord_dims) + factor = ensure_dataarray(other, coords=self.coords, dims=self.coord_dims) self_const, factor, needs_data_reindex = self._align_constant( factor, fill_value=fill_value, join=join ) @@ -1142,7 +1142,7 @@ def to_constraint( ) if isinstance(rhs, SUPPORTED_CONSTANT_TYPES): - rhs = as_dataarray(rhs, coords=self.coords, dims=self.coord_dims) + rhs = ensure_dataarray(rhs, coords=self.coords, dims=self.coord_dims) extra_dims = set(rhs.dims) - set(self.coord_dims) if extra_dims: @@ -1705,7 +1705,7 @@ def __matmul__( Matrix multiplication with other, similar to xarray dot. """ if not isinstance(other, LinearExpression | variables.Variable): - other = as_dataarray(other, coords=self.coords, dims=self.coord_dims) + other = ensure_dataarray(other, coords=self.coords, dims=self.coord_dims) common_dims = list(set(self.coord_dims).intersection(other.dims)) return (self * other).sum(dim=common_dims) @@ -2039,7 +2039,7 @@ def process_one( DeprecationWarning, ) # assume that the element is a constant - const = as_dataarray(t[0]) + const = ensure_dataarray(t[0]) if model is None: raise ValueError("Model must be provided when using constants.") return LinearExpression(const, model) @@ -2067,7 +2067,7 @@ def from_constant(cls, model: Model, constant: ConstantLike) -> LinearExpression linopy.LinearExpression A linear expression representing the constant value. """ - const_da = as_dataarray(constant) + const_da = ensure_dataarray(constant) return LinearExpression(const_da, model) @@ -2191,7 +2191,7 @@ def __matmul__( "Higher order non-linear expressions are not yet supported." ) - other = as_dataarray(other, coords=self.coords, dims=self.coord_dims) + other = ensure_dataarray(other, coords=self.coords, dims=self.coord_dims) common_dims = list(set(self.coord_dims).intersection(other.dims)) return (self * other).sum(dim=common_dims) @@ -2326,7 +2326,7 @@ def as_expression( return obj.to_linexpr() else: try: - obj = as_dataarray(obj, **kwargs) + obj = ensure_dataarray(obj, **kwargs) except ValueError as e: raise ValueError("Cannot convert to LinearExpression") from e return LinearExpression(obj, model) diff --git a/linopy/model.py b/linopy/model.py index 54334411..0d9c86ef 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -30,6 +30,7 @@ assign_multiindex_safe, best_int, broadcast_mask, + ensure_dataarray, maybe_replace_signs, replace_by_map, set_int_index, @@ -521,10 +522,14 @@ def add_variables( Upper bound of the variable(s). Ignored if `binary` is True. The default is inf. coords : list/xarray.Coordinates, optional - The coords of the variable array. - These are directly passed to the DataArray creation of - `lower` and `upper`. For every single combination of - coordinates a optimization variable is added to the model. + The coords of the variable array. For every single + combination of coordinates an optimization variable is + added to the model. Data for `lower`, `upper` and `mask` + is fitted to these coords: shared dimensions must have + matching coordinates, and missing dimensions are broadcast. + A ValueError is raised if the data is not compatible. + When ``coords`` is ``None``, coordinates are inferred from + the bounds (``lower``/``upper``) if they are DataArrays. The default is None. name : str, optional Reference name of the added variables. The default None results in @@ -551,7 +556,9 @@ def add_variables( ------ ValueError If neither lower bound and upper bound have coordinates, nor - `coords` are directly given. + `coords` are directly given. Also raised if `lower` or + `upper` are DataArrays whose coordinates do not match the + provided `coords`. Returns ------- @@ -615,7 +622,9 @@ def add_variables( self._check_valid_dim_names(data) if mask is not None: - mask = as_dataarray(mask, coords=data.coords, dims=data.dims).astype(bool) + mask = ensure_dataarray(mask, coords=data.coords, dims=data.dims).astype( + bool + ) mask = broadcast_mask(mask, data.labels) # Auto-mask based on NaN in bounds (use numpy for speed) @@ -777,14 +786,14 @@ def add_constraints( name = f"con{self._connameCounter}" self._connameCounter += 1 if sign is not None: - sign = maybe_replace_signs(as_dataarray(sign)) + sign = maybe_replace_signs(ensure_dataarray(sign)) # Capture original RHS for auto-masking before constraint creation # (NaN values in RHS are lost during constraint creation) # Use numpy for speed instead of xarray's notnull() original_rhs_mask = None if self.auto_mask and rhs is not None: - rhs_da = as_dataarray(rhs) + rhs_da = ensure_dataarray(rhs) original_rhs_mask = (rhs_da.coords, rhs_da.dims, ~np.isnan(rhs_da.values)) if isinstance(lhs, LinearExpression): @@ -837,14 +846,16 @@ def add_constraints( mask = ( rhs_mask if mask is None - else (as_dataarray(mask).astype(bool) & rhs_mask) + else (ensure_dataarray(mask).astype(bool) & rhs_mask) ) data["labels"] = -1 (data,) = xr.broadcast(data, exclude=[TERM_DIM]) if mask is not None: - mask = as_dataarray(mask, coords=data.coords, dims=data.dims).astype(bool) + mask = ensure_dataarray(mask, coords=data.coords, dims=data.dims).astype( + bool + ) mask = broadcast_mask(mask, data.labels) # Auto-mask based on null expressions or NaN RHS (use numpy for speed) diff --git a/linopy/variables.py b/linopy/variables.py index 4332a037..8f3be95b 100644 --- a/linopy/variables.py +++ b/linopy/variables.py @@ -33,10 +33,10 @@ from linopy.common import ( LabelPositionIndex, LocIndexer, - as_dataarray, assign_multiindex_safe, check_has_nulls, check_has_nulls_polars, + ensure_dataarray, filter_nulls_polars, format_string_as_variable_name, generate_indices_for_printout, @@ -321,7 +321,7 @@ def to_linexpr( linopy.LinearExpression Linear expression with the variables and coefficients. """ - coefficient = as_dataarray(coefficient, coords=self.coords, dims=self.dims) + coefficient = ensure_dataarray(coefficient, coords=self.coords, dims=self.dims) coefficient = coefficient.reindex_like(self.labels, fill_value=0) coefficient = coefficient.fillna(0) ds = Dataset({"coeffs": coefficient, "vars": self.labels}).expand_dims( diff --git a/test/test_common.py b/test/test_common.py index f1190024..4feee38d 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -409,6 +409,155 @@ def test_as_dataarray_with_dataarray_default_dims_coords() -> None: assert list(da_out.coords["dim2"].values) == list(da_in.coords["dim2"].values) +def test_as_dataarray_with_dataarray_no_coords() -> None: + da_in = DataArray([1, 2, 3], dims=["x"], coords={"x": [10, 20, 30]}) + da_out = as_dataarray(da_in) + assert_equal(da_out, da_in) + + +def test_validate_dataarray_coords_match() -> None: + from linopy.common import _validate_dataarray_coords + + da = DataArray([1, 2, 3], dims=["x"], coords={"x": [10, 20, 30]}) + # Should not raise + _validate_dataarray_coords(da, coords={"x": [10, 20, 30]}) + + +def test_validate_dataarray_coords_mismatch() -> None: + from linopy.common import _validate_dataarray_coords + + da = DataArray([1, 2, 3], dims=["x"], coords={"x": [10, 20, 30]}) + with pytest.raises(ValueError, match="do not match"): + _validate_dataarray_coords(da, coords={"x": [10, 20, 40]}) + + +def test_validate_dataarray_coords_extra_dims() -> None: + from linopy.common import _validate_dataarray_coords + + da = DataArray([[1, 2], [3, 4]], dims=["x", "y"]) + with pytest.raises(ValueError, match="extra dimensions"): + _validate_dataarray_coords(da, coords={"x": [0, 1]}) + + +def test_validate_dataarray_coords_sequence() -> None: + from linopy.common import _validate_dataarray_coords + + da = DataArray([1, 2], dims=["x"], coords={"x": [0, 1]}) + idx = pd.RangeIndex(2, name="x") + # Should not raise + _validate_dataarray_coords(da, coords=[idx], dims=["x"]) + + +def test_validate_dataarray_coords_sequence_mismatch() -> None: + from linopy.common import _validate_dataarray_coords + + da = DataArray([1, 2], dims=["x"], coords={"x": [0, 1]}) + idx = pd.RangeIndex(3, name="x") + with pytest.raises(ValueError, match="do not match"): + _validate_dataarray_coords(da, coords=[idx], dims=["x"]) + + +def test_coords_to_mapping_dims_length_mismatch() -> None: + from linopy.common import _coords_to_mapping + + with pytest.raises(ValueError, match="Length of dims"): + _coords_to_mapping([pd.RangeIndex(3), pd.RangeIndex(5)], dims=["x"]) + + +def test_coords_to_mapping_unnamed_index_warns() -> None: + from linopy.common import _coords_to_mapping + + with pytest.warns(UserWarning, match="no .name attribute"): + result = _coords_to_mapping([np.array([1, 2, 3])]) + assert result == {} + + +def test_coords_to_mapping_duplicate_name() -> None: + from linopy.common import _coords_to_mapping + + idx1 = pd.RangeIndex(3, name="x") + idx2 = pd.RangeIndex(5, name="x") + with pytest.raises(ValueError, match="Duplicate coordinate name"): + _coords_to_mapping([idx1, idx2]) + + +def test_ensure_dataarray_scalar() -> None: + from linopy.common import ensure_dataarray + + da = ensure_dataarray(1, coords={"x": [10, 20]}, dims=["x"]) + assert isinstance(da, DataArray) + assert da.dims == ("x",) + assert list(da.coords["x"].values) == [10, 20] + + +def test_ensure_dataarray_numpy() -> None: + from linopy.common import ensure_dataarray + + arr = np.array([1, 2, 3]) + da = ensure_dataarray(arr, coords={"x": [10, 20, 30]}, dims=["x"]) + assert isinstance(da, DataArray) + assert da.dims == ("x",) + assert list(da.coords["x"].values) == [10, 20, 30] + + +def test_ensure_dataarray_no_expand() -> None: + from linopy.common import ensure_dataarray + + da_in = DataArray([1, 2], dims=["x"], coords={"x": ["a", "b"]}) + # Missing dim "y" should NOT be expanded — ensure only does type conversion + da = ensure_dataarray( + da_in, coords={"x": ["a", "b"], "y": [1, 2, 3]}, dims=["x", "y"] + ) + assert da.dims == ("x",) + assert "y" not in da.dims + + +def test_ensure_dataarray_allows_extra_dims() -> None: + from linopy.common import ensure_dataarray + + da_in = DataArray([[1, 2], [3, 4]], dims=["x", "y"]) + # Should NOT raise even though "y" is not in coords + da = ensure_dataarray(da_in, coords={"x": [0, 1]}, dims=["x"]) + assert set(da.dims) == {"x", "y"} + + +def test_ensure_dataarray_no_coord_validation() -> None: + from linopy.common import ensure_dataarray + + da_in = DataArray([1, 2, 3], dims=["x"], coords={"x": [10, 20, 30]}) + # Should NOT raise even though coords don't match + da = ensure_dataarray(da_in, coords={"x": [10, 20, 40]}, dims=["x"]) + assert list(da.coords["x"].values) == [10, 20, 30] + + +def test_add_variables_with_dataarray_bounds_and_coords() -> None: + model = Model() + time = pd.RangeIndex(5, name="time") + lower = DataArray([0, 0, 0, 0, 0], dims=["time"], coords={"time": range(5)}) + var = model.add_variables(lower=lower, coords=[time], name="x") + assert var.shape == (5,) + assert list(var.data.coords["time"].values) == list(range(5)) + + +def test_add_variables_with_dataarray_bounds_coord_mismatch() -> None: + model = Model() + time = pd.RangeIndex(5, name="time") + lower = DataArray([0, 0, 0], dims=["time"], coords={"time": [0, 1, 2]}) + with pytest.raises(ValueError, match="do not match"): + model.add_variables(lower=lower, coords=[time], name="x") + + +def test_add_variables_with_dataarray_bounds_broadcast() -> None: + model = Model() + time = pd.RangeIndex(3, name="time") + space = pd.Index(["a", "b"], name="space") + lower = DataArray([0, 0, 0], dims=["time"], coords={"time": range(3)}) + var = model.add_variables(lower=lower, coords=[time, space], name="x") + assert set(var.data.dims) == {"time", "space"} + assert var.data.sizes["time"] == 3 + assert var.data.sizes["space"] == 2 + + def test_as_dataarray_with_unsupported_type() -> None: with pytest.raises(TypeError): as_dataarray(lambda x: 1, dims=["dim1"], coords=[["a"]])