agd.Eikonal.DictIn_detail

This file implements additional functionality of the Eikonal.dictIn class, related to source factorization, and to solving PDEs on spheres.

  1# Copyright 2020 Jean-Marie Mirebeau, University Paris-Sud, CNRS, University Paris-Saclay
  2# Distributed WITHOUT ANY WARRANTY. Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0
  3
  4"""
  5This file implements additional functionality of the Eikonal.dictIn class, related to 
  6source factorization, and to solving PDEs on spheres.
  7"""
  8
  9import numpy as np
 10from .. import AutomaticDifferentiation as ad
 11from .. import FiniteDifferences as fd
 12from .. import LinearParallel as lp
 13from .. import Metrics
 14
 15def factoringPointChoice(self):
 16	if 'factoringPointChoice' in self: # 'Key' and 'Seed' are synonyms here
 17		assert self['factoringPointChoice'] in ('Both','Key','Seed') 
 18		return self['factoringPointChoice']
 19	elif 'factoringRadius' not in self: return None
 20	elif self.get('order',1)==3: return 'Both'
 21	else: return 'Seed'
 22
 23
 24def SetFactor(self,radius=None,value=None,gradient=None):
 25	"""
 26	This function setups additive factorization around the seeds.
 27	Inputs (optional): 
 28	- radius.
 29		Positive number -> approximate radius, in pixels, of source factorization.
 30		-1 -> Factorization over all the domain. 
 31		None -> source factorization over all the domain
 32	- value (optional).
 33	  callable, array -> approximate values of the solution. 
 34	  None -> reconstructed from the metric.
 35	- gradient (optional) 
 36	  callable, array -> approximate gradient of the solution.
 37	  Obtained from the values by automatic differentiation if unspecified.
 38
 39	Outputs : the subgrid used for factorization
 40	Side effect : sets 'factoringValues', 'factoringGradients', 
 41	   and in the case of a subgrid 'factoringIndexShift'
 42	"""
 43	# Set the factoring grid points
 44	if radius is None:
 45		radius = self.get('factoringRadius',10)
 46
 47	if radius<0 or radius>=np.max(self.shape):
 48		factGrid = self.Grid()
 49		self.pop('factoringIndexShift',None)
 50	else:
 51		seed_ci =  self.PointFromIndex(self['seed'],to=True)
 52		bottom = [max(0,int(np.floor(ci)-radius)) for ci in seed_ci]
 53		top = [min(si,int(np.ceil(ci)+radius)+1) for ci,si in zip(seed_ci,self.shape)]
 54		self['factoringIndexShift'] = bottom
 55		aX = [self.xp.arange(bi,ti,dtype=self.float_t) for bi,ti in zip(bottom,top)]
 56		factGrid = ad.array(np.meshgrid(*aX,indexing='ij'))
 57		factGrid = np.moveaxis(self.PointFromIndex(np.moveaxis(factGrid,0,-1)),-1,0)
 58#			raise ValueError("dictIn.SetFactor : unsupported radius type")
 59
 60	# Set the values
 61	if value is None:
 62		value = self.get('factoringPointChoice','Seed')
 63
 64	if isinstance(value,str):
 65		seed = self['seed']
 66		if 'metric' in self: metric = self['metric']
 67		elif self['model'].startswith('Isotropic'): metric = Metrics.Isotropic(1)
 68		else: raise ValueError("dictIn.SetFactor error : no metric found")
 69		if 'cost' in self: metric = metric.with_cost(self['cost'])
 70		fullGrid = factGrid if factGrid.shape[1:]==self.shape else self.Grid()
 71
 72		diff = lambda x : x-fd.as_field(seed,x.shape[1:],depth=1)
 73		if value in ('Key','Seed'):
 74			metric.set_interpolation(fullGrid) # Default order 1 interpolation suffices
 75			value = metric.at(seed)
 76		elif value=='Current': # Not really working ?
 77			# Strictly speaking, we are not interpolating the metric, 
 78			# since the point x at which it is evaluated lies on the grid
 79			metric.set_interpolation(fullGrid) 
 80			value = lambda x : metric.at(x).norm(diff(x))
 81			#Cheating : not differentiating w.r.t position, but should be enough here
 82			gradient = lambda x: metric.at(x).gradient(diff(x)) 
 83		elif value=='Both':
 84			# Pray that AD goes well ...
 85			metric.set_interpolation(fullGrid,order=3) # Need to differentiate w.r.t x
 86			value = lambda x : 0.5*(metric.at(seed).norm(diff(x)) + 
 87				metric.at(x).norm(diff(x)))
 88		else:
 89			raise ValueError(f"dictIn.SetFactor error : unsupported "
 90				"factoringPointChoice : {value} .")
 91
 92	if callable(value):
 93		if gradient is None:
 94			factGrid_ad = ad.Dense.identity(constant=factGrid, shape_free=(self.vdim,))
 95			value = value(factGrid_ad)
 96		else:
 97			value = value(factGrid)
 98
 99	if isinstance(value,Metrics.Base):
100		factDiff = factGrid - fd.as_field(self['seed'],factGrid.shape[1:],depth=1)
101		if gradient is None: 
102			gradient = value.gradient(factDiff)
103			# Avoids recomputation, but generates NaNs at the seed points.
104			value = lp.dot_VV(gradient,factDiff)
105			value[np.isnan(value)]=0 
106		else:
107			value = value.norm(factDiff)
108
109	if ad.is_ad(value):
110		gradient = value.gradient()
111		value = value.value
112
113	if not ad.isndarray(value): 
114		raise ValueError(f"dictIn.SetFactor : unsupported value type {type(value)}")
115
116	#Set the gradient
117	if callable(gradient):
118		gradient = gradient(factGrid)
119
120	if not ad.isndarray(gradient): 
121		raise ValueError(f"dictIn.SetFactor : unsupported gradient type {type(gradient)}")
122
123	self["factoringValues"] = value
124	self["factoringGradients"] = np.moveaxis(gradient,0,-1) # Geometry last in c++ code...
125
126	for key in ('factoringRadius', 'factoringPointChoice'): self.pop(key,None)
127
128	return factGrid
129
130#def proj_tocost(x): return 2./(1.+lp.dot_VV(x,x))
131def proj_tosphere(x):
132	"""
133	Maps a point of the equatorial plane to the sphere, by projection : 
134	x -> (2 x,1-|x∏^2)/(1+|x|^2)
135	""" 
136	sq = lp.dot_VV(x,x)
137	return np.concatenate((x,np.expand_dims((1.-sq)/2.,axis=0)),axis=0) * (2./(1.+sq))
138def proj_fromsphere(q): 
139	"""
140	Maps a point of the sphere to the equatorial plane, by projection.
141	(q,qz) -> q/(1+qz)
142	"""
143	return q[:-1]/(1.+q[-1])
144
145#def sphere_tocost(x,chart): return proj_tocost(x)
146def sphere_tosphere(x,chart): 
147	"""
148	See proj_tosphere. Last component is reversed according to chart.
149	"""
150	y = proj_tosphere(x)
151	y[-1] *= chart
152	return y
153def sphere_fromsphere(q,chart):
154	"""
155	See proj_fromsphere. Last component of q is reversed according to chart.
156	"""
157	q=q.copy()
158	q[-1] *= chart
159	return proj_fromsphere(q)
160
161def SetSphere(self,dimsp,separation=5,radius=1.1):
162	"""
163	Setups the manifold R^(d-k) x S^k, using the equatorial projection for the sphere S^k.
164	Only compatible with the GPU accelerated eikonal solver. 
165	Inputs : 
166		dimsp (int): the discretization of a half sphere involves dimsp^k pixels.
167		radius (optional, float > 1): each local chart has base domain [-radius,radius]^k.
168		  (This is NOT the radius of the sphere, which is 1, but of the parametrization.)
169		separation (optional, int) : number of pixels separating the two local charts.
170			Set to false to define a projective space using a single chart. 
171	Side effects : 
172		Sets chart_mapping,chart_jump, dimensions, origin, gridScales.
173	Output : 
174		- Conversion utilities, between the equatorial plane and the sphere 
175			(and grid in the non-projective case)
176	"""
177	if 'chart_mapping' in self: # Some space R^(d-k) x S^k is already set
178		vdim_rect = self.vdim - self['chart_mapping'].ndim+1
179		dims_rect = self['dims'][:vdim_rect]
180		origin_rect = self['origin'][:vdim_rect]
181		gridScales_rect = self['gridScales'][:vdim_rect]
182	elif 'dims' in self: # Some rectangle R^(d-k) is already set
183		dims_rect = self['dims']
184		origin_rect = self.get('origin',np.zeros_like(dims_rect))
185		if 'gridScales' in self: gridScales_rect = self['gridScales']
186		else: gridScales_rect = self['gridScale']*np.ones_like(dims_rect) 
187	else: # Nothing set. All coordinates are sphere like, d=k
188		dims_rect = self.xp.array(tuple(),dtype=self.float_t)
189		origin_rect = dims_rect.copy()
190		gridScales_rect = dims_rect.copy()
191
192	vdim_rect = len(dims_rect)
193	vdim_sphere = self.vdim-vdim_rect # Dimension of the sphere
194	gridScale_sphere = 2*radius/dimsp
195
196	if separation: # Sphere manifold, described using two charts
197		separation_radius = separation*gridScale_sphere /2
198		center_radius = radius + separation_radius
199
200		dims_sphere = (2*dimsp + separation,) + (dimsp,)*(vdim_sphere-1)
201		origin_sphere = (-2*radius-separation_radius,) + (-radius,)*(vdim_sphere-1)
202
203		def sphere_fromgrid(x):
204			"""
205			Produces a point of the equatorial plane, and a boolean indicating which projection to 
206			use (False:south pole, True:north pole), from a grid point.
207			"""
208			assert len(x)==vdim_sphere
209			x = x.copy()
210			chart = np.where(np.abs(x[0])>=separation_radius,np.sign(x[0]),np.nan)
211			x[0] -= center_radius*chart
212			return x,chart
213
214		def sphere_togrid(x,chart):
215			"""
216			Produces a point of the original grid, from a point of the equatorial plane 
217			and a boolean indicating the active chart.
218			"""
219			assert len(x)==vdim_sphere
220			x=x.copy()
221			mixed_chart = x[0]*chart < -radius # The two coordinate charts would get mixed
222			x[:,mixed_chart] = np.nan
223			x[0] += chart*center_radius
224			return x
225	else: # Projective space, described using a single chart
226		dims_sphere = (dimsp,)*vdim_sphere
227		origin_sphere = (-radius,)*vdim_sphere
228
229	dims_sphere,origin_sphere,gridScales_sphere = [self.array_float_caster(e) 
230		for e in (dims_sphere,origin_sphere, (gridScale_sphere,)*vdim_sphere)]
231
232	self['dims'] = np.concatenate((dims_rect,dims_sphere),axis=0)
233	self['gridScales'] = np.concatenate(
234		(gridScales_rect,gridScales_sphere),axis=0)
235	self.pop('gridScale',None)
236	self['origin'] = np.concatenate((origin_rect,origin_sphere),axis=0)
237
238	# Produce a coordinate system, and set the jumps, etc
239	aX = self.Axes()[vdim_rect:]
240	X = self.array_float_caster(np.meshgrid(*aX,indexing='ij'))
241	if separation: X,chart = sphere_fromgrid(X)
242	X2 = lp.dot_VV(X,X)
243
244	# Geodesics jump when the are sufficiently far away from the fundamental domain.
245	radius_jump = (1+radius)/2
246	self['chart_jump'] = X2 > radius_jump**2
247	if separation:
248		self['chart_mapping'] = sphere_togrid(
249			sphere_fromsphere(sphere_tosphere(X,chart),-chart),-chart)
250		return {'from_grid':sphere_fromgrid,'to_grid':sphere_togrid,
251			'from_sphere':sphere_fromsphere,'to_sphere':sphere_tosphere} 
252	else:
253		self['chart_mapping'] = proj_fromsphere(-proj_tosphere(X))
254		return {'from_sphere':proj_fromsphere,'to_sphere':proj_tosphere}
def factoringPointChoice(self):
16def factoringPointChoice(self):
17	if 'factoringPointChoice' in self: # 'Key' and 'Seed' are synonyms here
18		assert self['factoringPointChoice'] in ('Both','Key','Seed') 
19		return self['factoringPointChoice']
20	elif 'factoringRadius' not in self: return None
21	elif self.get('order',1)==3: return 'Both'
22	else: return 'Seed'
def SetFactor(self, radius=None, value=None, gradient=None):
 25def SetFactor(self,radius=None,value=None,gradient=None):
 26	"""
 27	This function setups additive factorization around the seeds.
 28	Inputs (optional): 
 29	- radius.
 30		Positive number -> approximate radius, in pixels, of source factorization.
 31		-1 -> Factorization over all the domain. 
 32		None -> source factorization over all the domain
 33	- value (optional).
 34	  callable, array -> approximate values of the solution. 
 35	  None -> reconstructed from the metric.
 36	- gradient (optional) 
 37	  callable, array -> approximate gradient of the solution.
 38	  Obtained from the values by automatic differentiation if unspecified.
 39
 40	Outputs : the subgrid used for factorization
 41	Side effect : sets 'factoringValues', 'factoringGradients', 
 42	   and in the case of a subgrid 'factoringIndexShift'
 43	"""
 44	# Set the factoring grid points
 45	if radius is None:
 46		radius = self.get('factoringRadius',10)
 47
 48	if radius<0 or radius>=np.max(self.shape):
 49		factGrid = self.Grid()
 50		self.pop('factoringIndexShift',None)
 51	else:
 52		seed_ci =  self.PointFromIndex(self['seed'],to=True)
 53		bottom = [max(0,int(np.floor(ci)-radius)) for ci in seed_ci]
 54		top = [min(si,int(np.ceil(ci)+radius)+1) for ci,si in zip(seed_ci,self.shape)]
 55		self['factoringIndexShift'] = bottom
 56		aX = [self.xp.arange(bi,ti,dtype=self.float_t) for bi,ti in zip(bottom,top)]
 57		factGrid = ad.array(np.meshgrid(*aX,indexing='ij'))
 58		factGrid = np.moveaxis(self.PointFromIndex(np.moveaxis(factGrid,0,-1)),-1,0)
 59#			raise ValueError("dictIn.SetFactor : unsupported radius type")
 60
 61	# Set the values
 62	if value is None:
 63		value = self.get('factoringPointChoice','Seed')
 64
 65	if isinstance(value,str):
 66		seed = self['seed']
 67		if 'metric' in self: metric = self['metric']
 68		elif self['model'].startswith('Isotropic'): metric = Metrics.Isotropic(1)
 69		else: raise ValueError("dictIn.SetFactor error : no metric found")
 70		if 'cost' in self: metric = metric.with_cost(self['cost'])
 71		fullGrid = factGrid if factGrid.shape[1:]==self.shape else self.Grid()
 72
 73		diff = lambda x : x-fd.as_field(seed,x.shape[1:],depth=1)
 74		if value in ('Key','Seed'):
 75			metric.set_interpolation(fullGrid) # Default order 1 interpolation suffices
 76			value = metric.at(seed)
 77		elif value=='Current': # Not really working ?
 78			# Strictly speaking, we are not interpolating the metric, 
 79			# since the point x at which it is evaluated lies on the grid
 80			metric.set_interpolation(fullGrid) 
 81			value = lambda x : metric.at(x).norm(diff(x))
 82			#Cheating : not differentiating w.r.t position, but should be enough here
 83			gradient = lambda x: metric.at(x).gradient(diff(x)) 
 84		elif value=='Both':
 85			# Pray that AD goes well ...
 86			metric.set_interpolation(fullGrid,order=3) # Need to differentiate w.r.t x
 87			value = lambda x : 0.5*(metric.at(seed).norm(diff(x)) + 
 88				metric.at(x).norm(diff(x)))
 89		else:
 90			raise ValueError(f"dictIn.SetFactor error : unsupported "
 91				"factoringPointChoice : {value} .")
 92
 93	if callable(value):
 94		if gradient is None:
 95			factGrid_ad = ad.Dense.identity(constant=factGrid, shape_free=(self.vdim,))
 96			value = value(factGrid_ad)
 97		else:
 98			value = value(factGrid)
 99
100	if isinstance(value,Metrics.Base):
101		factDiff = factGrid - fd.as_field(self['seed'],factGrid.shape[1:],depth=1)
102		if gradient is None: 
103			gradient = value.gradient(factDiff)
104			# Avoids recomputation, but generates NaNs at the seed points.
105			value = lp.dot_VV(gradient,factDiff)
106			value[np.isnan(value)]=0 
107		else:
108			value = value.norm(factDiff)
109
110	if ad.is_ad(value):
111		gradient = value.gradient()
112		value = value.value
113
114	if not ad.isndarray(value): 
115		raise ValueError(f"dictIn.SetFactor : unsupported value type {type(value)}")
116
117	#Set the gradient
118	if callable(gradient):
119		gradient = gradient(factGrid)
120
121	if not ad.isndarray(gradient): 
122		raise ValueError(f"dictIn.SetFactor : unsupported gradient type {type(gradient)}")
123
124	self["factoringValues"] = value
125	self["factoringGradients"] = np.moveaxis(gradient,0,-1) # Geometry last in c++ code...
126
127	for key in ('factoringRadius', 'factoringPointChoice'): self.pop(key,None)
128
129	return factGrid

This function setups additive factorization around the seeds. Inputs (optional):

  • radius. Positive number -> approximate radius, in pixels, of source factorization. -1 -> Factorization over all the domain. None -> source factorization over all the domain
  • value (optional). callable, array -> approximate values of the solution. None -> reconstructed from the metric.
  • gradient (optional) callable, array -> approximate gradient of the solution. Obtained from the values by automatic differentiation if unspecified.

Outputs : the subgrid used for factorization Side effect : sets 'factoringValues', 'factoringGradients', and in the case of a subgrid 'factoringIndexShift'

def proj_tosphere(x):
132def proj_tosphere(x):
133	"""
134	Maps a point of the equatorial plane to the sphere, by projection : 
135	x -> (2 x,1-|x∏^2)/(1+|x|^2)
136	""" 
137	sq = lp.dot_VV(x,x)
138	return np.concatenate((x,np.expand_dims((1.-sq)/2.,axis=0)),axis=0) * (2./(1.+sq))

Maps a point of the equatorial plane to the sphere, by projection : x -> (2 x,1-|x∏^2)/(1+|x|^2)

def proj_fromsphere(q):
139def proj_fromsphere(q): 
140	"""
141	Maps a point of the sphere to the equatorial plane, by projection.
142	(q,qz) -> q/(1+qz)
143	"""
144	return q[:-1]/(1.+q[-1])

Maps a point of the sphere to the equatorial plane, by projection. (q,qz) -> q/(1+qz)

def sphere_tosphere(x, chart):
147def sphere_tosphere(x,chart): 
148	"""
149	See proj_tosphere. Last component is reversed according to chart.
150	"""
151	y = proj_tosphere(x)
152	y[-1] *= chart
153	return y

See proj_tosphere. Last component is reversed according to chart.

def sphere_fromsphere(q, chart):
154def sphere_fromsphere(q,chart):
155	"""
156	See proj_fromsphere. Last component of q is reversed according to chart.
157	"""
158	q=q.copy()
159	q[-1] *= chart
160	return proj_fromsphere(q)

See proj_fromsphere. Last component of q is reversed according to chart.

def SetSphere(self, dimsp, separation=5, radius=1.1):
162def SetSphere(self,dimsp,separation=5,radius=1.1):
163	"""
164	Setups the manifold R^(d-k) x S^k, using the equatorial projection for the sphere S^k.
165	Only compatible with the GPU accelerated eikonal solver. 
166	Inputs : 
167		dimsp (int): the discretization of a half sphere involves dimsp^k pixels.
168		radius (optional, float > 1): each local chart has base domain [-radius,radius]^k.
169		  (This is NOT the radius of the sphere, which is 1, but of the parametrization.)
170		separation (optional, int) : number of pixels separating the two local charts.
171			Set to false to define a projective space using a single chart. 
172	Side effects : 
173		Sets chart_mapping,chart_jump, dimensions, origin, gridScales.
174	Output : 
175		- Conversion utilities, between the equatorial plane and the sphere 
176			(and grid in the non-projective case)
177	"""
178	if 'chart_mapping' in self: # Some space R^(d-k) x S^k is already set
179		vdim_rect = self.vdim - self['chart_mapping'].ndim+1
180		dims_rect = self['dims'][:vdim_rect]
181		origin_rect = self['origin'][:vdim_rect]
182		gridScales_rect = self['gridScales'][:vdim_rect]
183	elif 'dims' in self: # Some rectangle R^(d-k) is already set
184		dims_rect = self['dims']
185		origin_rect = self.get('origin',np.zeros_like(dims_rect))
186		if 'gridScales' in self: gridScales_rect = self['gridScales']
187		else: gridScales_rect = self['gridScale']*np.ones_like(dims_rect) 
188	else: # Nothing set. All coordinates are sphere like, d=k
189		dims_rect = self.xp.array(tuple(),dtype=self.float_t)
190		origin_rect = dims_rect.copy()
191		gridScales_rect = dims_rect.copy()
192
193	vdim_rect = len(dims_rect)
194	vdim_sphere = self.vdim-vdim_rect # Dimension of the sphere
195	gridScale_sphere = 2*radius/dimsp
196
197	if separation: # Sphere manifold, described using two charts
198		separation_radius = separation*gridScale_sphere /2
199		center_radius = radius + separation_radius
200
201		dims_sphere = (2*dimsp + separation,) + (dimsp,)*(vdim_sphere-1)
202		origin_sphere = (-2*radius-separation_radius,) + (-radius,)*(vdim_sphere-1)
203
204		def sphere_fromgrid(x):
205			"""
206			Produces a point of the equatorial plane, and a boolean indicating which projection to 
207			use (False:south pole, True:north pole), from a grid point.
208			"""
209			assert len(x)==vdim_sphere
210			x = x.copy()
211			chart = np.where(np.abs(x[0])>=separation_radius,np.sign(x[0]),np.nan)
212			x[0] -= center_radius*chart
213			return x,chart
214
215		def sphere_togrid(x,chart):
216			"""
217			Produces a point of the original grid, from a point of the equatorial plane 
218			and a boolean indicating the active chart.
219			"""
220			assert len(x)==vdim_sphere
221			x=x.copy()
222			mixed_chart = x[0]*chart < -radius # The two coordinate charts would get mixed
223			x[:,mixed_chart] = np.nan
224			x[0] += chart*center_radius
225			return x
226	else: # Projective space, described using a single chart
227		dims_sphere = (dimsp,)*vdim_sphere
228		origin_sphere = (-radius,)*vdim_sphere
229
230	dims_sphere,origin_sphere,gridScales_sphere = [self.array_float_caster(e) 
231		for e in (dims_sphere,origin_sphere, (gridScale_sphere,)*vdim_sphere)]
232
233	self['dims'] = np.concatenate((dims_rect,dims_sphere),axis=0)
234	self['gridScales'] = np.concatenate(
235		(gridScales_rect,gridScales_sphere),axis=0)
236	self.pop('gridScale',None)
237	self['origin'] = np.concatenate((origin_rect,origin_sphere),axis=0)
238
239	# Produce a coordinate system, and set the jumps, etc
240	aX = self.Axes()[vdim_rect:]
241	X = self.array_float_caster(np.meshgrid(*aX,indexing='ij'))
242	if separation: X,chart = sphere_fromgrid(X)
243	X2 = lp.dot_VV(X,X)
244
245	# Geodesics jump when the are sufficiently far away from the fundamental domain.
246	radius_jump = (1+radius)/2
247	self['chart_jump'] = X2 > radius_jump**2
248	if separation:
249		self['chart_mapping'] = sphere_togrid(
250			sphere_fromsphere(sphere_tosphere(X,chart),-chart),-chart)
251		return {'from_grid':sphere_fromgrid,'to_grid':sphere_togrid,
252			'from_sphere':sphere_fromsphere,'to_sphere':sphere_tosphere} 
253	else:
254		self['chart_mapping'] = proj_fromsphere(-proj_tosphere(X))
255		return {'from_sphere':proj_fromsphere,'to_sphere':proj_tosphere}

Setups the manifold R^(d-k) x S^k, using the equatorial projection for the sphere S^k. Only compatible with the GPU accelerated eikonal solver. Inputs : dimsp (int): the discretization of a half sphere involves dimsp^k pixels. radius (optional, float > 1): each local chart has base domain [-radius,radius]^k. (This is NOT the radius of the sphere, which is 1, but of the parametrization.) separation (optional, int) : number of pixels separating the two local charts. Set to false to define a projective space using a single chart. Side effects : Sets chart_mapping,chart_jump, dimensions, origin, gridScales. Output : - Conversion utilities, between the equatorial plane and the sphere (and grid in the non-projective case)