Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[next]: Embedded field scan #1365

Merged
merged 122 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 112 commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
a6269f7
Add definitions and placeholders for basic embedded field view concepts
egparedes Jul 24, 2023
006093f
Fixes
egparedes Jul 24, 2023
c4f9443
make it pass tests
havogt Jul 24, 2023
bf649f0
save state
havogt Jul 25, 2023
c73c296
define DomainLike
havogt Jul 25, 2023
555e9f2
verify
havogt Jul 25, 2023
40a2d37
wip
havogt Jul 25, 2023
0afad47
refactor type translation
havogt Jul 25, 2023
0122863
small fixes
havogt Jul 26, 2023
80c9c12
finish refactoring builtins
havogt Jul 26, 2023
1475f17
cleanup
havogt Jul 26, 2023
5ab9a54
builtin tests
havogt Jul 26, 2023
5df8d9d
more tests
havogt Jul 26, 2023
1993f32
comments
havogt Jul 26, 2023
4f749ca
cleanup
havogt Jul 26, 2023
5555ad7
fix and ignore
havogt Jul 26, 2023
6042750
some more qa fixes
havogt Jul 26, 2023
87f0975
a few more mypy errors
havogt Jul 26, 2023
cc32ed2
typing
havogt Jul 27, 2023
0e4ba03
more cleanup, non-dispatch test
havogt Jul 27, 2023
007a8d7
remove debug print
havogt Jul 27, 2023
b6e91b6
reenable test
havogt Jul 27, 2023
b3fc186
more typing fixes
havogt Jul 27, 2023
aa4c94c
Suggestions and fixes
egparedes Jul 27, 2023
b8279ae
More fixes
egparedes Jul 27, 2023
c20b610
Missing changes from previous commit
egparedes Jul 27, 2023
cfcb927
Add docs to FieldABC
egparedes Jul 28, 2023
c1444f5
fix some more typing problems
havogt Jul 28, 2023
85b4eca
fix remaining typing problems, move back registry
havogt Jul 28, 2023
8ead6d2
more typing
havogt Jul 28, 2023
e414e18
fix test matrix
havogt Jul 31, 2023
0543b85
fix builtin params
havogt Jul 31, 2023
33bdc76
move ndarray_field to embedded
havogt Jul 31, 2023
59c1962
fix check
havogt Jul 31, 2023
dadfe18
cartesian connectivity wip
havogt Aug 2, 2023
81f4bae
Merge remote-tracking branch 'origin/main' into embedded-field-view-impl
havogt Aug 2, 2023
d45ba0c
slightly cleaner
havogt Aug 2, 2023
45b8151
minor
havogt Aug 2, 2023
34a1391
improve implementation, basic program test
havogt Aug 2, 2023
c6fc23e
Merge remote-tracking branch 'origin/main' into embedded-field-view-impl
havogt Aug 3, 2023
27aad55
Merge remote-tracking branch 'origin/embedded-field-view-impl' into c…
havogt Aug 3, 2023
0c8ee0f
improve typing
havogt Aug 3, 2023
a8a0324
Introduce Fieldview Domain (#1310)
samkellerhals Aug 8, 2023
d3a7e44
Apply suggestions from code review
havogt Aug 8, 2023
aab3436
fix test
havogt Aug 8, 2023
202fc3b
fix imports
havogt Aug 8, 2023
60b2534
fix import
havogt Aug 8, 2023
4fbf5f6
fix import
havogt Aug 8, 2023
8bc332a
Merge remote-tracking branch 'origin/main' into embedded-field-view-impl
havogt Aug 9, 2023
9038dd1
Merge remote-tracking branch 'upstream/embedded-field-view-impl' into…
havogt Aug 9, 2023
c63b927
fix bug in Domain broadcast
havogt Aug 9, 2023
483dc17
undo unrelated changes
havogt Aug 9, 2023
3868dd3
Merge remote-tracking branch 'upstream/embedded-field-view-impl' into…
havogt Aug 10, 2023
de0a534
Merge remote-tracking branch 'upstream/main' into cartesian_connectivity
havogt Aug 10, 2023
a5718ef
Merge remote-tracking branch 'upstream/main' into cartesian_connectivity
havogt Aug 22, 2023
6be2db7
Merge branch 'main' into cartesian_connectivity
egparedes Nov 16, 2023
0b39199
Minor fixes after merge
egparedes Nov 16, 2023
bf378d0
Fix style
egparedes Nov 16, 2023
f8b7bc7
More fixes after the merge
egparedes Nov 16, 2023
3b16173
Fixes
egparedes Nov 16, 2023
f7c996b
Fix style
egparedes Nov 16, 2023
182b254
Fix tests
egparedes Nov 16, 2023
08ba6fe
Fix value_type leftovers
egparedes Nov 17, 2023
16a983c
Fix general inverse image (WIP remap and tests, partially broken even…
egparedes Nov 17, 2023
346f6f0
First working version of the general remap (cartesian still WIP)
egparedes Nov 17, 2023
a65b58c
First working version of refactored cartesian remap
egparedes Nov 17, 2023
6e007d2
Merge branch 'main' into cartesian_connectivity
egparedes Nov 18, 2023
08802c9
Fix merge issues
egparedes Nov 18, 2023
7a6053f
Add higher level connectivity constructors
egparedes Nov 18, 2023
c79b1c1
add sketch of scan
havogt Nov 18, 2023
a68ab85
prototyping a bit more
havogt Nov 19, 2023
643cfb1
refactor
havogt Nov 20, 2023
7d3d57c
make indirect call work
havogt Nov 20, 2023
ffc75e7
remove levftover
havogt Nov 20, 2023
23bec64
Fixes for most of cartesian shift tests
egparedes Nov 20, 2023
58209f7
Format fixes
egparedes Nov 20, 2023
f141408
fix test cases
havogt Nov 20, 2023
28b2620
fix intersect_domains
havogt Nov 20, 2023
01dcf72
fix last test
havogt Nov 20, 2023
68dec86
cleanups and typing
havogt Nov 20, 2023
e46e0f6
First embedded unstructured shift test working
egparedes Nov 20, 2023
c899c2c
Merge remote-tracking branch 'havogt/cartesian_connectivity' into car…
egparedes Nov 20, 2023
447d8c8
Fix test cases definitions
egparedes Nov 21, 2023
3b4b222
WIP adding neighbor sums
egparedes Nov 21, 2023
257a6b7
add reduction builtins
havogt Nov 21, 2023
22ca726
some typing
havogt Nov 21, 2023
7dbcc36
more typing
havogt Nov 21, 2023
bf6299c
more typing
havogt Nov 21, 2023
5183ac6
more
havogt Nov 21, 2023
af9bcb6
even more typing
havogt Nov 21, 2023
334bed0
last type problem
havogt Nov 21, 2023
f7399dd
fix bug
havogt Nov 21, 2023
2583a4d
Merge remote-tracking branch 'upstream/main' into cartesian_connectivity
havogt Nov 21, 2023
920455d
Apply suggestions from code review
havogt Nov 21, 2023
31ba6be
format and typing
havogt Nov 21, 2023
bbfa0d9
Enhance Domain with review suggestions
egparedes Nov 21, 2023
f0abba2
Fixes to domain changes
egparedes Nov 21, 2023
581cece
tiny tests and comment cleanup
havogt Nov 21, 2023
ac802d7
Merge remote-tracking branch 'origin/cartesian_connectivity' into emb…
havogt Nov 21, 2023
c342ad4
Merge remote-tracking branch 'upstream/main' into embedded_field_scan
havogt Nov 21, 2023
f7beac9
update column_range to NamedRange
havogt Nov 21, 2023
6cad860
refactor with context vars
havogt Nov 21, 2023
87303be
address review comments
havogt Nov 21, 2023
f5f022e
refactor tuple assigns
havogt Nov 21, 2023
1d4b4e1
delete combine_pos
havogt Nov 21, 2023
31c5902
add docstring
havogt Nov 21, 2023
c1d6629
fix strange test
havogt Nov 22, 2023
fb7f6b5
isolate embedded from decorators module
havogt Nov 22, 2023
62a6ec8
remove reference to foast_node in embedded execution
havogt Nov 29, 2023
13fe506
Merge remote-tracking branch 'upstream/main' into embedded_field_scan
havogt Nov 29, 2023
077a106
support different horizontal domains in scan tuples
havogt Nov 30, 2023
3f45a6b
fix tree_reduce
havogt Nov 30, 2023
4a0b1c9
add doctest for get_common_tuple_value
havogt Dec 5, 2023
650b052
annotation
havogt Dec 5, 2023
41fa12c
refactor dispatching logic
havogt Dec 5, 2023
8142eb4
isolate embedded operators
havogt Dec 5, 2023
440107e
rename back
havogt Dec 5, 2023
ed1bbae
fix mixed tuple and cleanup
havogt Dec 7, 2023
c8f8cc2
inline scanop
havogt Dec 7, 2023
61f563a
address review comment
havogt Dec 7, 2023
b763766
delete get_common_tuple_value
havogt Dec 7, 2023
a698189
delete a typevar
havogt Dec 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ image:
tasks:
- name: Setup venv and dev tools
init: |
ln -s /workspace/gt4py/.gitpod/.vscode /workspace/gt4py/.vscode
ln -sfn /workspace/gt4py/.gitpod/.vscode /workspace/gt4py/.vscode
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
Expand Down
17 changes: 17 additions & 0 deletions src/gt4py/next/embedded/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

from __future__ import annotations

import functools
import itertools
import operator

from gt4py.eve.extended_typing import Any, Optional, Sequence, cast
from gt4py.next import common
from gt4py.next.embedded import exceptions as embedded_exceptions
Expand Down Expand Up @@ -90,6 +94,19 @@ def _absolute_sub_domain(
return common.Domain(*named_ranges)


def intersect_domains(*domains: common.Domain) -> common.Domain:
return functools.reduce(
operator.and_,
domains,
common.Domain(dims=tuple(), ranges=tuple()),
)


def iterate_domain(domain: common.Domain):
for i in itertools.product(*[list(r) for r in domain.ranges]):
yield tuple(zip(domain.dims, i))


def _expand_ellipsis(
indices: common.RelativeIndexSequence, target_size: int
) -> tuple[common.IntIndex | slice, ...]:
Expand Down
4 changes: 2 additions & 2 deletions src/gt4py/next/embedded/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

#: Column range used in column mode (`column_axis != None`) in the current embedded iterator
#: closure execution context.
closure_column_range: cvars.ContextVar[range] = cvars.ContextVar("column_range")
closure_column_range: cvars.ContextVar[common.NamedRange] = cvars.ContextVar("column_range")

_undefined_offset_provider: common.OffsetProvider = {}

Expand All @@ -37,7 +37,7 @@
@contextlib.contextmanager
def new_context(
*,
closure_column_range: range | eve.NothingType = eve.NOTHING,
closure_column_range: common.NamedRange | eve.NothingType = eve.NOTHING,
offset_provider: common.OffsetProvider | eve.NothingType = eve.NOTHING,
):
import gt4py.next.embedded.context as this_module
Expand Down
8 changes: 3 additions & 5 deletions src/gt4py/next/embedded/nd_array_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import dataclasses
import functools
import operator
from collections.abc import Callable, Sequence
from types import ModuleType
from typing import ClassVar
Expand Down Expand Up @@ -49,11 +48,10 @@ def _builtin_op(*fields: common.Field | core_defs.Scalar) -> NdArrayField:
xp = first.__class__.array_ns
op = getattr(xp, array_builtin_name)

domain_intersection = functools.reduce(
operator.and_,
[f.domain for f in fields if common.is_field(f)],
common.Domain(dims=tuple(), ranges=tuple()),
domain_intersection = embedded_common.intersect_domains(
*[f.domain for f in fields if common.is_field(f)]
)

transformed: list[core_defs.NDArrayObject | core_defs.Scalar] = []
for f in fields:
if common.is_field(f):
Expand Down
196 changes: 196 additions & 0 deletions src/gt4py/next/embedded/operators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# GT4Py - GridTools Framework
#
# Copyright (c) 2014-2023, ETH Zurich
# All rights reserved.
#
# This file is part of the GT4Py project and the GridTools framework.
# GT4Py is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or any later
# version. See the LICENSE.txt file at the top-level directory of this
# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from typing import Any, Callable, Optional, Sequence

from gt4py import eve
from gt4py._core import definitions as core_defs
from gt4py.next import common, constructors, field_utils, utils
from gt4py.next.embedded import common as embedded_common, context as embedded_context


def field_operator_call(
op: Callable, operator_attributes: Optional[dict[str, Any]], args: Any, kwargs: Any
):
# embedded execution
if embedded_context.within_context():
# field_operator called from program or other field_operator in embedded execution
if "out" in kwargs:
# field_operator called from program in embedded execution

offset_provider = kwargs.pop("offset_provider", None)
# offset_provider should already be set
assert (
offset_provider is None or embedded_context.offset_provider.get() is offset_provider
)
havogt marked this conversation as resolved.
Show resolved Hide resolved
out = kwargs.pop("out")
domain = kwargs.pop("domain", None)
out_domain = (
common.domain(domain) if domain is not None else field_utils.get_domain(out)
)

vertical_range = _get_vertical_range(out_domain)

with embedded_context.new_context(closure_column_range=vertical_range) as ctx:
res = ctx.run(_run_operator, op, operator_attributes, args, kwargs)
_tuple_assign_field(
out,
res,
domain=out_domain,
)

else:
# field_operator called form field_operator in embedded execution
return _run_operator(op, operator_attributes, args, kwargs)

else:
# field_operator called directly
offset_provider = kwargs.pop("offset_provider", None)
out = kwargs.pop("out")
domain = kwargs.pop("domain", None)

out_domain = common.domain(domain) if domain is not None else field_utils.get_domain(out)

with embedded_context.new_context(
offset_provider=offset_provider, closure_column_range=_get_vertical_range(out_domain)
) as ctx:
res = ctx.run(_run_operator, op, operator_attributes, args, kwargs)
_tuple_assign_field(
out,
res,
domain=out_domain,
)
havogt marked this conversation as resolved.
Show resolved Hide resolved


def _run_operator(
op: Callable, operator_attributes: Optional[dict[str, Any]], args: Any, kwargs: Any
):
if operator_attributes is not None and any(
havogt marked this conversation as resolved.
Show resolved Hide resolved
has_scan_op_attribute := [
attribute in operator_attributes for attribute in ["init", "axis", "forward"]
]
):
assert all(has_scan_op_attribute)
forward = operator_attributes["forward"]
init = operator_attributes["init"]
axis = operator_attributes["axis"]

scan_range = embedded_context.closure_column_range.get()
assert axis == scan_range[0]

return scan(op, forward, init, scan_range, args, kwargs)
else:
return op(*args, **kwargs)


def _get_vertical_range(domain: common.Domain) -> common.NamedRange | eve.NothingType:
vertical_dim_filtered = [nr for nr in domain if nr[0].kind == common.DimensionKind.VERTICAL]
assert len(vertical_dim_filtered) <= 1
return vertical_dim_filtered[0] if vertical_dim_filtered else eve.NOTHING


def _tuple_assign_field(
target: tuple[common.MutableField | tuple, ...] | common.MutableField,
source: tuple[common.Field | tuple, ...] | common.Field,
domain: common.Domain,
):
@utils.tree_map
def impl(target: common.MutableField, source: common.Field):
target[domain] = source[domain]

impl(target, source)


def scan(
scan_op: Callable,
forward: bool,
init: core_defs.Scalar | tuple[core_defs.Scalar | tuple, ...],
scan_range: common.NamedRange,
args: tuple,
kwargs: dict,
):
scan_axis = scan_range[0]
domain_intersection = embedded_common.intersect_domains(
*[_intersect_tuple_domain(f) for f in [*args, *kwargs.values()] if _is_field_or_tuple(f)]
)
non_scan_domain = common.Domain(*[nr for nr in domain_intersection if nr[0] != scan_axis])

out_domain = common.Domain(
*[scan_range if nr[0] == scan_axis else nr for nr in domain_intersection]
)
if scan_axis not in out_domain.dims:
# even if the scan dimension is not in the input, we can scan over it
out_domain = common.Domain(*out_domain, (scan_range))

res = _construct_scan_array(out_domain)(init)

def scan_loop(hpos):
acc = init
for k in scan_range[1] if forward else reversed(scan_range[1]):
pos = (*hpos, (scan_axis, k))
new_args = [_tuple_at(pos, arg) for arg in args]
new_kwargs = {k: _tuple_at(pos, v) for k, v in kwargs.items()}
acc = scan_op(acc, *new_args, **new_kwargs)
_tuple_assign_value(pos, res, acc)

for hpos in embedded_common.iterate_domain(non_scan_domain):
scan_loop(hpos)
if len(non_scan_domain) == 0:
# if we don't have any dimension orthogonal to scan_axis, we need to do one scan_loop
scan_loop(())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for hpos in embedded_common.iterate_domain(non_scan_domain):
scan_loop(hpos)
if len(non_scan_domain) == 0:
# if we don't have any dimension orthogonal to scan_axis, we need to do one scan_loop
scan_loop(())
if len(non_scan_domain) == 0:
# if we don't have any dimension orthogonal to scan_axis, we need to do one scan_loop
scan_loop(())
else:
for hpos in embedded_common.iterate_domain(non_scan_domain):
scan_loop(hpos)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


return res


@utils.tree_reduce(init=common.Domain())
def _intersect_tuple_domain(a: common.Domain, b: common.Field) -> common.Domain:
return embedded_common.intersect_domains(a, b.domain)


@utils.get_common_tuple_value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the following line from above correctly

[field_utils.get_domain(f) for f in [*args, *kwargs.values()] if _is_field_or_tuple(f)]

then the function here should not use get_common_tuple_value because it also checks all values are the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above

def _is_field_or_tuple(field: common.Field) -> bool:
return common.is_field(field)


def _construct_scan_array(domain: common.Domain):
@utils.tree_map
def impl(init: core_defs.Scalar) -> common.Field:
return constructors.empty(domain, dtype=type(init))

return impl
havogt marked this conversation as resolved.
Show resolved Hide resolved


def _tuple_assign_value(
pos: Sequence[common.NamedIndex],
target: common.MutableField | tuple[common.MutableField | tuple, ...],
source: core_defs.Scalar | tuple[core_defs.Scalar | tuple, ...],
) -> None:
@utils.tree_map
def impl(target: common.MutableField, source: core_defs.Scalar):
target[pos] = source

impl(target, source)


def _tuple_at(
havogt marked this conversation as resolved.
Show resolved Hide resolved
pos: Sequence[common.NamedIndex],
field: common.Field | core_defs.Scalar | tuple[common.Field | core_defs.Scalar | tuple, ...],
) -> core_defs.Scalar | tuple[core_defs.ScalarT | tuple, ...]:
@utils.tree_map
def impl(field: common.Field | core_defs.Scalar) -> core_defs.Scalar:
res = field[pos] if common.is_field(field) else field
assert core_defs.is_scalar_type(res)
return res

return impl(field) # type: ignore[return-value]
84 changes: 23 additions & 61 deletions src/gt4py/next/ffront/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
from gt4py._core import definitions as core_defs
from gt4py.eve import utils as eve_utils
from gt4py.eve.extended_typing import Any, Optional
from gt4py.next import allocators as next_allocators, common, embedded as next_embedded
from gt4py.next import allocators as next_allocators, embedded as next_embedded
from gt4py.next.common import Dimension, DimensionKind, GridType
from gt4py.next.embedded import operators as embedded_operators
from gt4py.next.ffront import (
dialect_ast_enums,
field_operator_ast as foast,
Expand Down Expand Up @@ -545,6 +546,7 @@ class FieldOperator(GTCallable, Generic[OperatorNodeT]):
definition: Optional[types.FunctionType] = None
backend: Optional[ppi.ProgramExecutor] = DEFAULT_BACKEND
grid_type: Optional[GridType] = None
operator_attributes: Optional[dict[str, Any]] = None
_program_cache: dict = dataclasses.field(default_factory=dict)

@classmethod
Expand Down Expand Up @@ -581,6 +583,7 @@ def from_function(
definition=definition,
backend=backend,
grid_type=grid_type,
operator_attributes=operator_attributes,
)

def __gt_type__(self) -> ts.CallableType:
Expand Down Expand Up @@ -687,68 +690,27 @@ def __call__(
*args,
**kwargs,
) -> None:
# TODO(havogt): Don't select mode based on existence of kwargs,
# because now we cannot provide nice error messages. E.g. set context var
# if we are reaching this from a program call.
if "out" in kwargs:
out = kwargs.pop("out")
if not next_embedded.context.within_context() and self.backend is not None:
# non embedded execution
offset_provider = kwargs.pop("offset_provider", None)
if self.backend is not None:
# "out" and "offset_provider" -> field_operator as program
# When backend is None, we are in embedded execution and for now
# we disable the program generation since it would involve generating
# Python source code from a PAST node.
args, kwargs = type_info.canonicalize_arguments(self.foast_node.type, args, kwargs)
# TODO(tehrengruber): check all offset providers are given
# deduce argument types
arg_types = []
for arg in args:
arg_types.append(type_translation.from_value(arg))
kwarg_types = {}
for name, arg in kwargs.items():
kwarg_types[name] = type_translation.from_value(arg)

return self.as_program(arg_types, kwarg_types)(
*args, out, offset_provider=offset_provider, **kwargs
)
else:
# "out" -> field_operator called from program in embedded execution or
# field_operator called directly from Python in embedded execution
domain = kwargs.pop("domain", None)
if not next_embedded.context.within_context():
# field_operator from Python in embedded execution
with next_embedded.context.new_context(offset_provider=offset_provider) as ctx:
res = ctx.run(self.definition, *args, **kwargs)
else:
# field_operator from program in embedded execution (offset_provicer is already set)
assert (
offset_provider is None
or next_embedded.context.offset_provider.get() is offset_provider
)
res = self.definition(*args, **kwargs)
_tuple_assign_field(
out, res, domain=None if domain is None else common.domain(domain)
)
return
out = kwargs.pop("out")
args, kwargs = type_info.canonicalize_arguments(self.foast_node.type, args, kwargs)
# TODO(tehrengruber): check all offset providers are given
# deduce argument types
arg_types = []
for arg in args:
arg_types.append(type_translation.from_value(arg))
kwarg_types = {}
for name, arg in kwargs.items():
kwarg_types[name] = type_translation.from_value(arg)

return self.as_program(arg_types, kwarg_types)(
*args, out, offset_provider=offset_provider, **kwargs
)
else:
# field_operator called from other field_operator in embedded execution
assert self.backend is None
return self.definition(*args, **kwargs)


def _tuple_assign_field(
target: tuple[common.Field | tuple, ...] | common.Field,
source: tuple[common.Field | tuple, ...] | common.Field,
domain: Optional[common.Domain],
):
if isinstance(target, tuple):
if not isinstance(source, tuple):
raise RuntimeError(f"Cannot assign {source} to {target}.")
for t, s in zip(target, source):
_tuple_assign_field(t, s, domain)
else:
domain = domain or target.domain
target[domain] = source[domain]
return embedded_operators.field_operator_call(
self.definition, self.operator_attributes, args, kwargs
)


@typing.overload
Expand Down
Loading