@@ -660,7 +660,10 @@ def local_section(self):
660660 @cached_property
661661 @deprecated ("axes.template_vec" )
662662 def template_vec (self ):
663- block_shape = self .shape if len (self ) == 1 else ()
663+ if is_mixed (self ):
664+ block_shape = ()
665+ else :
666+ block_shape = self .shape
664667 return self .layout_axes .template_vec (block_shape )
665668
666669 @cached_method ()
@@ -682,7 +685,7 @@ def lgmap(self, bcs: Iterable[DirichletBC] = (), index: int | None = None) -> PE
682685
683686 """
684687 lgmap_axes = self .axes
685- if len (self ) > 1 or any (bc .function_space ().component is not None for bc in bcs ):
688+ if is_mixed (self ) or any (bc .function_space ().component is not None for bc in bcs ):
686689 block_size = 1
687690 else :
688691 lgmap_axes = lgmap_axes .blocked (self .shape )
@@ -692,7 +695,7 @@ def lgmap(self, bcs: Iterable[DirichletBC] = (), index: int | None = None) -> PE
692695 # track which BCs are used so we can warn if any are missed
693696 unused_bcs = set (bcs )
694697 if index is None : # The lgmap is for the full space
695- if len (self ) > 1 :
698+ if is_mixed (self ):
696699 split_bcs = []
697700 for subspace in self :
698701 matching_bcs = []
@@ -781,7 +784,7 @@ def interior_facet_node_map_dat(self) -> op3.Dat:
781784 @property
782785 @deprecated ("cell_node_map_dat.data_ro" )
783786 def cell_node_list (self ) -> numpy .ndarray :
784- if len (self ) > 1 or self .parent :
787+ if is_mixed (self ) or self .parent :
785788 warnings .warn (
786789 "For mixed spaces it is no longer the case that offset=node*bsize, "
787790 "use a DoF list instead."
@@ -791,7 +794,7 @@ def cell_node_list(self) -> numpy.ndarray:
791794 @property
792795 @deprecated ("exterior_facet_node_map_dat.data_ro" )
793796 def exterior_facet_node_list (self ) -> numpy .ndarray :
794- if len (self ) > 1 or self .parent :
797+ if is_mixed (self ) or self .parent :
795798 warnings .warn (
796799 "For mixed spaces it is no longer the case that offset=node*bsize, "
797800 "use a DoF list instead."
@@ -801,7 +804,7 @@ def exterior_facet_node_list(self) -> numpy.ndarray:
801804 @property
802805 @deprecated ("interior_facet_node_map_dat.data_ro" )
803806 def interior_facet_node_list (self ) -> numpy .ndarray :
804- if len (self ) > 1 or self .parent :
807+ if is_mixed (self ) or self .parent :
805808 warnings .warn (
806809 "For mixed spaces it is no longer the case that offset=node*bsize "
807810 "because the strides between nodes are not constant as they are "
@@ -898,7 +901,7 @@ def _iterset_to_dof_map_dat(
898901 # └──➤ {field: [{functionspace0: 1}, {functionspace1: 1}]}
899902 # ├──➤ {some_label: 2}
900903 # └──➤ {some_label: 1}
901- if len (self ) > 1 :
904+ if is_mixed (self ):
902905 map_dof_axes = iterset_axes .add_axis (None , packed_offsets .axes .root )
903906 for label , subspace in zip (self ._labels , self ):
904907 path = iterset_axes .leaf_path | {"field" : label }
@@ -1606,7 +1609,7 @@ def entity_node_map(self, iteration_spec):
16061609 Entity node map.
16071610
16081611 """
1609- if len (self ) > 1 :
1612+ if is_mixed (self ):
16101613 raise NotImplementedError ("will this work?" )
16111614
16121615 iter_mesh = iteration_spec .mesh
@@ -2558,6 +2561,96 @@ def _(index: int, shape: tuple[int, ...]) -> tuple[int, ...]:
25582561
25592562
25602563
2564+ def merge_axis_constraints (root_axis : op3 .Axis , axis_constraintss : Sequence [Sequence [AxisConstraint ]]) -> tuple [AxisConstraint ]:
2565+ # start by collecting like axes
2566+ axis_info : defaultdict [op3 .Axis , dict [op3 .ComponentLabelT , idict ]] = defaultdict (dict )
2567+ for root_component , constraints in zip (root_axis .components , axis_constraintss , strict = True ):
2568+ for constraint in constraints :
2569+ axis_info [constraint .axis ][root_component .label ] = constraint .within_axes
2570+
2571+ # Now build the new set of constraints. To do this we inspect the
2572+ # per-component constraints for each axis: if the constraints are all the
2573+ # same then it is not necessary to specialise by component, otherwise an
2574+ # extra constraint is needed. For example:
2575+ #
2576+ # * Consider the "dof" axis for a mixed space with identical subspaces:
2577+ #
2578+ # {dof_axis: {0: {"mesh": None}, 1: {"mesh": None}}}
2579+ #
2580+ # Here this is saying that 'dof_axis' exists under root components 0 and 1
2581+ # and each time must satisfy the constraint of having '{"mesh": None}'
2582+ # above them.
2583+ #
2584+ # Since the constraints are identical for all components they do not need
2585+ # to be specialised. The final constraint is thus:
2586+ #
2587+ # AxisConstraint(dof_axis, {"mesh": None})
2588+ #
2589+ # * Alternatively consider a mixed space of CG1 x Real:
2590+ #
2591+ # {mesh_axis: {0: {}}}
2592+ #
2593+ # The "mesh" axis only exists for the CG1 subspace and so a new constraint
2594+ # is needed:
2595+ #
2596+ # AxisConstraint(mesh_axis, {"field": 0})
2597+ constraints = [AxisConstraint (root_axis )]
2598+ for axis , per_component_info in axis_info .items ():
2599+ if (
2600+ per_component_info .keys () == set (root_axis .component_labels )
2601+ and utils .is_single_valued (per_component_info .values ())
2602+ ):
2603+ # Axis present for all components and constraints match: use as is
2604+ within_axes = utils .single_valued (per_component_info .values ())
2605+ constraints .append (AxisConstraint (axis , within_axes ))
2606+ else :
2607+ # Constraint mismatch: need to specialise by component
2608+ for component_label , orig_within_axes in per_component_info .items ():
2609+ within_axes = orig_within_axes | {root_axis .label : component_label }
2610+ constraints .append (AxisConstraint (axis , within_axes ))
2611+ return tuple (constraints )
2612+
2613+
2614+ @functools .singledispatch
2615+ def parse_component_indices (indices : Any , shape : tuple [int , ...]) -> tuple [int , ...]:
2616+ raise TypeError
2617+
2618+
2619+ @parse_component_indices .register (tuple )
2620+ def _ (indices : tuple [int , ...], shape : tuple [int , ...]) -> tuple [int , ...]:
2621+ return indices
2622+
2623+
2624+ @parse_component_indices .register (int )
2625+ def _ (index : int , shape : tuple [int , ...]) -> tuple [int , ...]:
2626+ # Historically tensor-valued spaces would be addressed using a flat index
2627+ # instead of a tuple. Here we convert the old-style flat index to a
2628+ # nested one. Eventually we should be able to remove this and simply cast
2629+ # an integer index to a tuple (e.g. '3' to '(3,)').
2630+ if len (shape ) > 1 :
2631+ warnings .warn (
2632+ "Scalar indexing of a tensor-valued space is no longer recommended "
2633+ "practice, please pass a tuple instead" ,
2634+ FutureWarning ,
2635+ )
2636+ return list (numpy .ndindex (shape ))[index ]
2637+
2638+ for component in axis .components :
2639+ subconstraints_ = tuple (
2640+ subconstraint
2641+ for subconstraint in subconstraints
2642+ if axis .label not in subconstraint .within_axes
2643+ or (axis .label , component .label ) in subconstraint .within_axes .items ()
2644+ )
2645+ if subconstraints_ :
2646+ # FIXME: Not doing anything with visited_axes
2647+ subnest = _axis_nest_from_constraints (subconstraints_ , visited_axes )
2648+ axis_nest [axis ].append (subnest )
2649+
2650+ return idict (axis_nest ) if axis_nest else axis
2651+
2652+
2653+
25612654def merge_axis_constraints (root_axis : op3 .Axis , axis_constraintss : Sequence [Sequence [AxisConstraint ]]) -> tuple [AxisConstraint ]:
25622655 # start by collecting like axes
25632656 axis_info : defaultdict [op3 .Axis , dict [op3 .ComponentLabelT , idict ]] = defaultdict (dict )
@@ -2667,3 +2760,23 @@ def entity_permutations_key(entity_permutations):
26672760 key .append (tuple (sub_key ))
26682761 key = tuple (key )
26692762 return key
2763+
2764+
2765+ @functools .singledispatch
2766+ def is_mixed (obj ) -> bool :
2767+ raise TypeError
2768+
2769+
2770+ @is_mixed .register
2771+ def _ (elem : finat .ufl .FiniteElementBase ) -> bool :
2772+ return type (elem ) is finat .ufl .MixedElement
2773+
2774+
2775+ @is_mixed .register
2776+ def _ (V : WithGeometryBase | AbstractFunctionSpace ) -> bool :
2777+ return is_mixed (V .ufl_element ())
2778+
2779+
2780+ @is_mixed .register
2781+ def _ (arg : ufl .Argument , / ) -> bool :
2782+ return is_mixed (arg .ufl_function_space ())
0 commit comments