Source code for fdgrid.domains

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2016-2019 Cyril Desjouy <cyril.desjouy@univ-lemans.fr>
#
# This file is part of fdgrid
#
# fdgrid 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
# (at your option) any later version.
#
# fdgrid is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with fdgrid. If not, see <http://www.gnu.org/licenses/>.
#
#
# Creation Date : 2019-02-13 - 07:54:54
#
# pylint: disable=too-many-public-methods
# pylint: disable=too-few-public-methods
# pylint: disable=attribute-defined-outside-init
"""
-----------

Module `domains` provides three main objects:

    * Subdomain : dataclass for subdivisions of the computational domain
    * Obstacle : Subdomain subclass with custom boundary possibilities
    * Domain : Container for Subdomain objects

-----------
"""

__all__ = ['Domain', 'Subdomain', 'Obstacle']

import copy as _copy
import itertools as _itertools
import numpy as _np
import scipy.signal as _sps
import matplotlib.pyplot as _plt
import matplotlib.patches as _patches
import dataclasses as _dataclasses
import fdgrid.utils as _utils

[docs]class Domain: """ Set of Subdomain objects. Parameters ---------- shape : Size of the domain. Must be a 2 elements tuple data : list of Subdomain objects or coordinates """ def __init__(self, shape, data=None): self.shape = shape self.nx, self.nz = shape self._data = dict({}) if data and isinstance(data, (list, tuple)): if not isinstance(data[0], Subdomain): data = self._subdomain_from_list(data) for sub in data: if not sub.key: sub.key = self._key self._data[sub.key] = sub # Additional rigid boundaries when bc is periodic self.additional_rigid_bc = None # Domain counter self._n = 0 @staticmethod def _subdomain_from_list(data): subs = [] for c in data: subs.append(Subdomain(c)) return subs
[docs] def show(self, legend=False, fcolor='b', ecolor='y'): """ Represent the Subdomains. """ _, ax = _plt.subplots(figsize=(9, 9)) for sub in self: rect = _patches.Rectangle((sub.ix[0], sub.iz[0]), sub.ix[1] - sub.ix[0], sub.iz[1] - sub.iz[0], linewidth=3, edgecolor=ecolor, facecolor=fcolor, alpha=0.5) ax.add_patch(rect) ax.set_xlim(0, self.nx) ax.set_ylim(0, self.nz) ax.set_aspect('equal') major_xticks = range(0, self.nx, 10) major_zticks = range(0, self.nz, 10) minor_xticks = range(0, self.nx, 2) minor_zticks = range(0, self.nz, 2) ax.set_xticks(major_xticks) ax.set_yticks(major_zticks) ax.set_xticks(minor_xticks, minor=True) ax.set_yticks(minor_zticks, minor=True) ax.grid(which='minor', alpha=0.2) ax.grid(which='major', alpha=0.5) for sub in self: ax.text(*sub.center, f'{sub.key} ({sub.tag})', color='k') if legend: print(self)
[docs] def keys(self): """ Return all keys of Domain instance. """ return _utils.sort(self._data.keys())
[docs] def sort(self, inplace=False): """ Sort domains. If inplace is True, Domain is sorted inplace. If inplace is False, return a new instance. """ tmp_list = [] tmp_dict = {} for key in self.keys(): tmp_list.append(self._data[key]) tmp_dict[key] = self[key] if inplace: self._data = tmp_dict return None return Domain(self.shape, data=tmp_list)
[docs] def copy(self): """ Create copy of the instance. """ return _copy.deepcopy(self)
[docs] def mask(self): """ Create mask array of the domain. """ mask = _np.zeros(self.shape) for sub in self: mask[sub.sx, sub.sz] += 1 return mask
[docs] def append(self, val, key=None): """ Append 'val' to the Domain object. Parameters ---------- val : The object to add to the instance. Must be a Subdomain object or a list/tuple of Subdomain objects key : new key for val """ if isinstance(val, Subdomain): if key: val.key = str(key) elif not val.key and not key: val.key = self._key if val.key in self._data.keys(): raise ValueError(f'Domain {val.key} already exists') self._data[val.key] = val self.sort(inplace=True) elif isinstance(val, (tuple, list)): for sub in val: if isinstance(sub, Subdomain): self.append(sub) else: raise ValueError('Not a Subdomain object')
[docs] def pop(self, n): """ Remove value of index n from the Domain object. """ self._check_key(str(n)) self._data.pop(str(n))
[docs] def steal(self, other, key, newkey=None): """ Steal key from other. """ if not newkey: newkey = key try: self.append(other[key], key=newkey) except ValueError: pass finally: other.pop(key)
[docs] def split(self, key, index, nbc=None, axis=None): """ Split 'key' subdomain. Parameters ---------- index : int|tuple|list nbc : Only if index is a sequence. New bc for the domain defined by 'index' axis : 0 or 1. Along which axis to split """ self[key] = self[key].split(index, nbc=nbc, axis=axis) self._clean_keys()
def _clean_keys(self): """ Convert 1.1.1 keys to 1.X """ badkeys = {key.split('.')[0] for key in self.keys() if len(key.split('.')) > 2} if badkeys: newkeys = self.keys().copy() for basekey in badkeys: subkey = (i for i in range(1, len(newkeys)+1)) for idx, key in enumerate(newkeys): if key.split('.')[0] == basekey: newkeys[idx] = '{}.{}'.format(basekey, next(subkey)) tmp = {} for idx, sub in enumerate(self): sub.key = newkeys[idx] tmp[sub.key] = sub self._data = tmp
[docs] def reset_keys(self): """ Reset all keys then attribute new ordered keys. """ tmp = {} newkeys = [str(i) for i in range(1, len(self.keys())+1)] for key, sub in zip(newkeys, self): sub.key = key tmp[key] = sub self._data = tmp
[docs] def join(self, key1, key2, include_split=False, tag=None): """ Join key1 and key2 Subdomain objects. """ self._check_key(key1) self._check_key(key2) sub1 = self[key1] sub2 = self[key2] if not tag: tag = sub1.tag if not self.isjoinable(sub1, sub2, include_split=include_split): raise ValueError("domains can't be joined") if sub1.rx == sub2.rx: bc = self._join_bc(sub1, sub2, axis=1) kwargs = {'bc':bc, 'key':sub1.key, 'axis':sub1.axis, 'tag':tag} iz = sub1.iz + sub2.iz self[sub1.key] = Subdomain([sub1.ix[0], min(iz), sub1.ix[1], max(iz)], **kwargs) self.pop(sub2.key) elif sub1.rz == sub2.rz: bc = self._join_bc(sub1, sub2, axis=0) kwargs = {'bc':bc, 'key':sub1.key, 'axis':sub1.axis, 'tag':tag} ix = sub1.ix + sub2.ix self[sub1.key] = Subdomain([min(ix), sub1.iz[0], max(ix), sub1.iz[1]], **kwargs) self.pop(sub2.key)
[docs] def autojoin(self, include_split=False): """ Automatically join domains that can be joined. """ for sub1, sub2 in _itertools.combinations(self, r=2): if self.isjoinable(sub1, sub2, include_split=include_split): self.join(sub1.key, sub2.key, include_split=include_split)
[docs] @staticmethod def isjoinable(sub1, sub2, include_split=False): """ Check if sub1 and sub2 can be joined. """ if include_split: c0 = True else: c0 = '.' not in sub1.key and '.' not in sub2.key c1 = sub1.rz == sub2.rz c2 = sub1.ix[1] == sub2.ix[0] - 1 or sub2.ix[1] == sub1.ix[0] - 1 c3 = sub1.rx == sub2.rx c4 = sub1.iz[1] == sub2.iz[0] - 1 or sub2.iz[1] == sub1.iz[0] - 1 c5 = sub1.axis == sub2.axis condition0 = c0 and c5 condition1 = c1 and c2 condition2 = c3 and c4 if (condition0 and condition1) or (condition0 and condition2): return True return False
@staticmethod def _join_bc(sub1, sub2, axis=None): """ Determinae bc for the joined Subdomain. """ if axis == 0: if sub1.axis == 0 and sub1.ix[0] < sub2.ix[0]: return f'{sub1.bc[0]}.{sub2.bc[2]}.' if sub1.axis == 0 and sub1.ix[0] > sub2.ix[0]: return f'{sub2.bc[0]}.{sub1.bc[2]}.' if axis == 1: if sub1.axis == 1 and sub1.iz[0] < sub2.iz[0]: return f'.{sub1.bc[1]}.{sub2.bc[3]}' if sub1.axis == 1 and sub1.iz[0] > sub2.iz[0]: return f'.{sub2.bc[1]}.{sub1.bc[3]}' return sub1.bc
[docs] def periodic_bc(self, mask, axis): """ Arange subdomains to take into account periodic boundary condtions. """ if axis == 0: self._xperiod(mask) self._get_rigid_bc(self.nx, axis=axis) elif axis == 1: self._zperiod(mask) self._get_rigid_bc(self.nz, axis=axis) else: raise ValueError('axis must be 0 or 1')
def _xperiod(self, mask): left = _np.argwhere(mask[0, :] != 0).ravel() right = _np.argwhere(mask[-1, :] != 0).ravel() left_rigid, right_rigid = self._periodic_bounds(left, right) for sub in [s for s in self if s.ix[0] == 0]: self._periodic_update(sub, left_rigid) for sub in [s for s in self if s.ix[1] == self.nx-1]: self._periodic_update(sub, right_rigid) def _zperiod(self, mask): bot = _np.argwhere(mask[:, 0] != 0).ravel() top = _np.argwhere(mask[:, -1] != 0).ravel() bot_rigid, top_rigid = self._periodic_bounds(bot, top) for sub in [s for s in self if s.iz[0] == 0]: self._periodic_update(sub, bot_rigid) for sub in [s for s in self if s.iz[1] == self.nz-1]: self._periodic_update(sub, top_rigid) @staticmethod def _periodic_bounds(bot, top): bot_rigid = list(set(top).difference(set(bot))) top_rigid = list(set(bot).difference(set(top))) if any(bot_rigid): bot_rigid = _utils.split_discontinuous(bot_rigid) else: bot_rigid = [] if any(top_rigid): top_rigid = _utils.split_discontinuous(top_rigid) else: top_rigid = [] return bot_rigid, top_rigid def _periodic_update(self, sub, bounds): if sub.axis == 0: r = getattr(sub, 'rz') axis = 0 elif sub.axis == 1: r = getattr(sub, 'rx') axis = 1 nbc = sub.bc.replace('P', 'W') for bound in bounds: if set(range(bound[0], bound[1]+1)).issuperset(set(r)): # print(f'Process {sub.key} : update bc') self[sub.key].bc = nbc elif set(range(bound[0], bound[1]+1)).issubset(set(r)): # print(f'Process {sub.key} : split subdomain') self.split(sub.key, bound, nbc=nbc, axis=axis) def _get_rigid_bc(self, N, axis=None): """ Returns a list of rigid boundary conditions. """ self.additional_rigid_bc = [] for sub in self: if sub.xz[axis] == 0 and sub.bc[axis] == 'W': if axis == 0: self.additional_rigid_bc.append((0, sub.sz)) elif axis == 1: self.additional_rigid_bc.append((sub.sx, 0)) elif sub.xz[axis+2] == N-1 and sub.bc[axis+2] == 'W': if axis == 0: self.additional_rigid_bc.append((-1, sub.sz)) elif axis == 1: self.additional_rigid_bc.append((sub.sx, -1)) @property def _key(self): self._n = 1 while str(self._n) in self._data.keys(): self._n += 1 return str(self._n) @property def xz(self): """ Return list of coordinates. """ if self._data: return [s.xz for s in self] return None @property def sx(self): """ Return list of slices (over Subdomain x coordinates). """ if self._data: return [s.sx for s in self] return None @property def sz(self): """ Return list of slices (over Subdomain z coordinates). """ if self._data: return [s.sz for s in self] return None @property def rx(self): """ Return list of ranges (over Subdomain x coordinates). """ if self._data: return [s.rx for s in self] return None @property def rz(self): """ Return list of ranges (over Subdomain z coordinates). """ if self._data: return [s.rz for s in self] return None @property def ix(self): """ Return list of x coordinates. """ if self._data: return [s.ix for s in self] return None @property def iz(self): """ Return list of z coordinates. """ if self._data: return [s.iz for s in self] return None def _check_key(self, n): """ Check if key n exists. """ if str(n) not in self._data.keys(): raise KeyError('Sudbomain not found') def __setitem__(self, n, val): if isinstance(val, (Subdomain, tuple, list)): self.pop(str(n)) self.append(val, n) else: msg = 'Can only assign Subdomain object or sequence of Subdomain objects' raise ValueError(msg) def __getitem__(self, n): self._check_key(n) return self._data[str(n)] def __iter__(self): return iter(self._data.values()) def __add__(self, other): if not isinstance(other, Domain): raise TypeError('Can only concatenate Domain objects') if set(self._data.keys()).intersection(set(other.keys())): raise ValueError('Duplicate subdomains') else: tmp = Domain(self.shape) for sub in self: tmp.append(sub) for sub in other: tmp.append(sub) return tmp def __radd__(self, other): return self.__add__(other) def __len__(self): return len(self._data) def __str__(self): s = '' for sub in self: s += '\t' + sub.__str__() + '\n' return s def __repr__(self): return self.__str__()
[docs]@_dataclasses.dataclass class Subdomain: """ Subdivision of the computation domain. Parameters ---------- xz : list Coordinates of the Subdomain : left, bottom, right, top bc : str Boundary conditions. Must be a string of 4 characters among 'A|W|Z|P|R' key : int, optional Key of the subdomain. axis : {0, 1}, optional The direction of the subdomain if relevant. tag : str, optional The type of Subdomain. """ xz: tuple bc: str = '....' key: str = '' axis: int = -1 tag: str = '' def __post_init__(self): self.ix = (self.xz[0], self.xz[2]) self.iz = (self.xz[1], self.xz[3]) self.rx = range(self.xz[0], self.xz[2]+1) self.rz = range(self.xz[1], self.xz[3]+1) self.sx = slice(self.xz[0], self.xz[2]+1) self.sz = slice(self.xz[1], self.xz[3]+1) @property def axname(self): """ Returns "x" or "z" whether axis is 0 or 1. Return 2 if both. """ if self.axis == 0: return 'x' if self.axis == 1: return 'z' if self.axis == 2: return 'xz' return None @property def center(self): """ Returns the center of the Subdomain. """ return [int((self.ix[1]+self.ix[0])/2), int((self.iz[1]+self.iz[0])/2)]
[docs] def intersection(self, other): """ Return common border with another object. """ edges = self._which_edge(other) if edges: if (0, 2) in edges: return self.xz[0], slice(self.xz[1]+1, self.xz[3]) if (2, 0) in edges: return self.xz[2], slice(self.xz[1]+1, self.xz[3]) if (1, 3) in edges: return slice(self.xz[0]+1, self.xz[2]), self.xz[1] if (3, 1) in edges: return slice(self.xz[0]+1, self.xz[2]), self.xz[3] return None
[docs] def touch(self, other): """ Return borders that touch the domain. """ edges = self._which_edge(other) coords = [] if edges: for c in edges: if c == (0, 0): coords.append((self.xz[0], slice(self.xz[1]+1, self.xz[3]))) elif c == (1, 1): coords.append((slice(self.xz[0]+1, self.xz[2]), self.xz[1])) elif c == (2, 2): coords.append((self.xz[2], slice(self.xz[1]+1, self.xz[3]))) elif c == (3, 3): coords.append((slice(self.xz[0]+1, self.xz[2]), self.xz[3])) return coords
def _which_edge(self, other): """ Search for common edges. """ edges = [] if self.axis == 0 or other.axis == 0: indexes = [(0, 2, 1, 3)] elif self.axis == 1 or other.axis == 1: indexes = [(1, 3, 0, 2)] else: indexes = [(0, 2, 1, 3), (1, 3, 0, 2)] for a, b, c, d in indexes: for i, j in _itertools.product([a, b], repeat=2): c1 = other.xz[j] - 1 <= self.xz[i] <= other.xz[j] + 1 c2 = self.xz[c] >= other.xz[c] and self.xz[d] <= other.xz[d] if c1 and c2: edges.append((i, j)) return edges def __matmul__(self, other): """ Specify boundary if common edges with "other". """ edges = self._which_edge(other) bc_lst = list(self.bc) if not bc_lst: bc_lst = 4*['.'] for edge in edges: if other.bc[edge[1]] != '.': bc_lst[edge[0]] = other.bc[edge[1]] else: if other.axis: bc_lst[edge[0]] = 'X' self.bc = ''.join(bc_lst) return self.bc
[docs] def split(self, index, nbc=None, axis=None): """ Split the subdomain into several new subdomains. If index is integer, split into two Subdomain objects at 'index'. If index is tuple/list (len(index) must be 2), split into 3 (or less) Subdomain objects. Parameters ---------- index : {int, tuple, list} Where to split the subdomain nbc : str Only if index is a sequence. New bc for the domain defined by 'index' axis : {0, 1} Along which axis to split """ subkey = (i for i in range(1, 4)) xz = [] if axis is None: axis = self.axis bounds = self._split_indexes(index, axis) for bound in bounds: if axis == 0: xz.append([self.ix[0], bound[0], self.ix[1], bound[1]]) elif axis == 1: xz.append([bound[0], self.iz[0], bound[1], self.iz[1]]) else: raise ValueError('axis must be 0 or 1') bc = self._split_bc(xz, index, nbc, axis) tag = self._split_tag(xz) return [Subdomain(xz[i], bc[i], key=f'{self.key}.{next(subkey)}', axis=self.axis, tag=tag[i]) for i in range(len(xz))]
def _split_indexes(self, index, axis): if axis == 0: r = getattr(self, 'rz') c = getattr(self, 'iz') elif axis == 1: r = getattr(self, 'rx') c = getattr(self, 'ix') else: raise ValueError('axis must be 0 or 1') if isinstance(index, int): if c[1] == index: return [(c[0], c[1]), (c[1], c[1])] if c[0] <= index < c[1]: return [(c[0], index), (index+1, c[1])] raise ValueError('index must be in the subdomain') elif isinstance(index, (tuple, list)): if len(index) == 2: index = tuple(sorted(list(index))) index_set = set(range(index[0], index[1]+1)) domain_set = set(r) if index_set.issubset(domain_set): remaining = list(domain_set.difference(index_set)) lst = _utils.split_discontinuous(remaining) lst.append(index) lst.sort() return lst raise ValueError('indexes must be in the domain') else: raise ValueError('index must be of length 2') else: raise ValueError('index must be int, list, or tuple') def _split_bc(self, xz, index, nbc, axis): """ Determine bc when splitting Subdomain. """ if axis == 0: cid = 1 elif axis == 1: cid = 0 if nbc and len(xz) > 1: bc = [self.bc]*len(xz) for idx, coord in enumerate(xz): if set(range(coord[cid], coord[cid+2])).issubset(set(range(*index))): bc = [self.bc]*len(xz) bc[idx] = nbc elif axis != self.axis and self.axis == 0: bc = ['X.X.']*len(xz) bc[0] = f'{self.bc[0]}.X.' bc[-1] = f'X.{self.bc[2]}.' elif axis != self.axis and self.axis == 1: bc = ['.X.X']*len(xz) bc[0] = f'.{self.bc[1]}.X' bc[-1] = f'.X.{self.bc[3]}' else: bc = [self.bc]*len(xz) return bc def _split_tag(self, xz): """ Set right tags to each subdomain. """ tags = [self.tag]*len(xz) for i, c in enumerate(xz): if c[0] == c[2] or c[1] == c[3]: tags[i] = 'W' return tags
@_dataclasses.dataclass class Edges: """ Edges description for moving boundaries. """ func: str = None wav: str = None f0: float = 0 phi: float = 0 pn: _np.ndarray = 0 # profile of the wall vn: _np.ndarray = 0 # time evolution of the wall vt: _np.ndarray = 0 # time evolution of the wall sx: slice = 0 sz: slice = 0 axis: int = 0
[docs]class Obstacle(Subdomain): """ Obstacle object. Parameters ---------- xz : Coordinates of the Subdomain : left, bottom, right, top bc : Boundary conditions. Must be a string of 4 characters among 'W|Z|V' key : Key of the subdomain. Optional axis : 0 or 1. The direction of the subdomain if relevant. Optional. tag : str. The type of Subdomain. Optional """ def __post_init__(self): super().__post_init__() self.edges = []
[docs] def set_moving_bc(self, *args): """ Set parameters for moving bc. For each moving bc of the obstacle, you can specify the parameters as a dictionnary containing the following optional keys : Parameters ---------- f : float, str frequency of the normal velocity. A : float Amplitude of the normal velocity phi : float Phase of the normal velocity profile : {'sine', 'tukey'} Function for normal velocity profile construction kwargs : dict optional arguments for profile Note ---- `profile` argument can be: * 'sine' : sinus profile. kw: n - wavelength (default: 2 for half period) * 'tukey' : tapered cosine profile. kw: alpha - edge width (default: 0) Examples -------- >>> obs1 = Obstacle([10, 10, 100, 100], 'VVWV') >>> obs1.set_moving_bc({'f': 100, 'A': 2.1, 'profile': 'sine'}, {'f': 500, 'A': -1.5, 'profile': 'tukey'}, {'f': 500, 'A': 1.5 'profile': 'tukey', 'kwargs':{'alpha': 0.4}}) """ args = iter(list(args) + (4-len(args))*[{}]) self.setup = [{} if b == 'W' else next(args) for b in self.bc]
def _parse_bc_setup(self, setup): prof = setup.get('profile', 'tukey') prof = getattr(self, prof, self.tukey) wav = setup.get('wav', None) func = setup.get('func', None) f = setup.get('f', setup.get('f', 0)) phi = setup.get('phi', setup.get('phi', 0)) A = setup.get('A', setup.get('A', 0)) kwargs = setup.get('kwargs', {}) tag = True if wav or func or f else False return tag, prof, f, phi, A, kwargs, wav, func
[docs] def make_moving_bc(self, x, z, LOG=False): """ Construct moving boundaries. """ if hasattr(self, 'setup'): for i in range(4): if len(x.shape) == 2: self._make_moving_bc_curvilinear(i, x, z) else: self._make_moving_bc_cartesian(i, x, z) if LOG: for e in self.edges: _, ax = _plt.subplots(1, 2) ax[0].plot(e.pn) ax[0].grid() ax[0].set_title('Numerical domain') if e.axis == 0: ax[1].plot(z[e.sx, e.sz], e.pn) if e.axis == 1: ax[1].plot(x[e.sx, e.sz], e.pn) ax[1].set_title('Physical domain') ax[1].grid() _plt.tight_layout()
def _make_moving_bc_cartesian(self, i, x, z): tag, prof, f, phi, A, kw, wav, func = self._parse_bc_setup(self.setup[i]) d = {'f0': f, 'phi': phi, 'axis': i % 2, 'wav': wav, 'func': func} if i % 2 == 0: # following x d['sx'] = self.xz[i] d['sz'] = self.sz if prof and tag: d['pn'] = A*prof(z, self.sz, **kw) else: # following z d['sx'] = self.sx d['sz'] = self.xz[i] if prof and tag: d['pn'] = A*prof(x, self.sx, **kw) if prof and tag: self.edges.append(Edges(**d)) def _make_moving_bc_curvilinear(self, i, x, z): tag, prof, f, phi, A, kw, wav, func = self._parse_bc_setup(self.setup[i]) d = {'f0': f, 'phi': phi, 'axis': i % 2, 'wav': wav, 'func': func} if i % 2 == 0: # following x d['sx'] = self.xz[i] d['sz'] = self.sz if prof and tag: d['pn'] = A*prof(z[self.xz[i], :], self.sz, **kw) else: # following z d['sx'] = self.sx d['sz'] = self.xz[i] if prof and tag: d['pn'] = A*prof(x[:, self.xz[i]], self.sx, **kw) if prof and tag: self.edges.append(Edges(**d))
[docs] @staticmethod def sine(c, sc, **kwargs): """ Sine profile. """ L = c[sc.stop-1] - c[sc.start] n = kwargs.get('n', 2) kL = 2*_np.pi/(n*L) return _np.sin(kL*(c[sc] - c[sc.start]))
[docs] @staticmethod def tukey(_, sc, **kwargs): """ Tapered cosine profile. """ N = sc.stop - sc.start alpha = kwargs.get('alpha', 0) return _sps.tukey(N, alpha=alpha)