Passed
Push — master ( 6ed46e...742380 )
by Stefan
07:44
created

HarmonicModelCosineSine.compute_gradient()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 6

Duplication

Lines 6
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nop 2
dl 6
loc 6
ccs 6
cts 6
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
# vim:fileencoding=utf-8
3
#
4
# Copyright (c) 2017-2018 Stefan Bender
5
#
6
# This module is part of sciapy.
7
# sciapy is free software: you can redistribute it or modify
8
# it under the terms of the GNU General Public License as published
9
# by the Free Software Foundation, version 2.
10
# See accompanying LICENSE file or http://www.gnu.org/licenses/gpl-2.0.html.
11 1
"""SCIAMACHY regression models (celerite version)
12
13
Model classes for SCIAMACHY data regression fits using the
14
:mod:`celerite` [#]_ modeling protocol.
15
16
.. [#] https://celerite.readthedocs.io
17
"""
18 1
from __future__ import absolute_import, division, print_function
19
20 1
import numpy as np
21 1
from scipy.interpolate import interp1d
22
23 1
from celerite.modeling import Model, ModelSet, ConstantModel
24
25 1
__all__ = ["ConstantModel",
26
		"HarmonicModelCosineSine", "HarmonicModelAmpPhase",
27
		"ProxyModel", "TraceGasModelSet",
28
		"setup_proxy_model_with_bounds", "trace_gas_model"]
29
30
31 1 View Code Duplication
class HarmonicModelCosineSine(Model):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
32
	"""Model for harmonic terms
33
34
	Models harmonic terms using a cosine and sine part.
35
	The total amplitude and phase can be inferred from that.
36
37
	Parameters
38
	----------
39
	freq : float
40
		The frequency in years^-1
41
	cos : float
42
		The amplitude of the cosine part
43
	sin : float
44
		The amplitude of the sine part
45
	"""
46 1
	parameter_names = ("freq", "cos", "sin")
47
48 1
	def get_value(self, t):
49 1
		t = np.atleast_1d(t)
50 1
		return (self.cos * np.cos(self.freq * 2 * np.pi * t) +
51
				self.sin * np.sin(self.freq * 2 * np.pi * t))
52
53 1
	def get_amplitude(self):
54
		return np.sqrt(self.cos**2 + self.sin**2)
55
56 1
	def get_phase(self):
57
		return np.arctan2(self.sin, self.cos)
58
59 1
	def compute_gradient(self, t):
60 1
		t = np.atleast_1d(t)
61 1
		dcos = np.cos(self.freq * 2 * np.pi * t)
62 1
		dsin = np.sin(self.freq * 2 * np.pi * t)
63 1
		df = 2 * np.pi * t * (self.sin * dcos - self.cos * dsin)
64 1
		return np.array([df, dcos, dsin])
65
66
67 1 View Code Duplication
class HarmonicModelAmpPhase(Model):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
68
	"""Model for harmonic terms
69
70
	Models harmonic terms using a cosine and sine part.
71
	The total amplitude and phase can be inferred from that.
72
73
	Parameters
74
	----------
75
	freq : float
76
		The frequency in years^-1
77
	amp : float
78
		The amplitude of the harmonic term
79
	phase : float
80
		The phase of the harmonic part
81
	"""
82 1
	parameter_names = ("freq", "amp", "phase")
83
84 1
	def get_value(self, t):
85
		t = np.atleast_1d(t)
86
		return self.amp * np.cos(self.freq * 2 * np.pi * t + self.phase)
87
88 1
	def get_amplitude(self):
89
		return self.amp
90
91 1
	def get_phase(self):
92
		return self.phase
93
94 1
	def compute_gradient(self, t):
95
		t = np.atleast_1d(t)
96
		damp = np.cos(self.freq * 2 * np.pi * t + self.phase)
97
		dphi = -self.amp * np.sin(self.freq * 2 * np.pi * t + self.phase)
98
		df = 2 * np.pi * t * dphi
99
		return np.array([df, damp, dphi])
100
101
102 1 View Code Duplication
class ProxyModel(Model):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
103
	"""Model for proxy terms
104
105
	Models proxy terms with a finite and (semi-)annually varying life time.
106
107
	Parameters
108
	----------
109
	proxy_times : (N,) array_like
110
		The data times of the proxy values
111
	proxy_vals : (N,) array_like
112
		The proxy values at `proxy_times`
113
	amp : float
114
		The amplitude of the proxy term
115
	lag : float
116
		The lag of the proxy value in years.
117
	tau0 : float
118
		The base life time of the proxy
119
	taucos1 : float
120
		The amplitude of the cosine part of the annual life time variation.
121
	tausin1 : float
122
		The amplitude of the sine part of the annual life time variation.
123
	taucos2 : float
124
		The amplitude of the cosine part of the semi-annual life time variation.
125
	tausin2 : float
126
		The amplitude of the sine part of the semi-annual life time variation.
127
	ltscan : float
128
		The number of days to sum the previous proxy values. If it is
129
		negative, the value will be set to three times the maximal lifetime.
130
		No lifetime adjustemets are calculated when set to zero.
131
	center : bool, optional
132
		Centers the proxy values by subtracting the overall mean. The mean is
133
		calculated from the whole `proxy_vals` array and is stored in the
134
		`mean` attribute.
135
		Default: False
136
	sza_intp : scipy.interpolate.interp1d() instance, optional
137
		When not `None`, cos(sza) and sin(sza) are used instead
138
		of the time to model the annual variation of the lifetime.
139
		Semi-annual variations are not used in that case.
140
		Default: None
141
	fit_phase : bool, optional
142
		Fit the phase shift directly instead of using sine and cosine
143
		terms for the (semi-)annual lifetime variations. If True, the fitted
144
		cosine parameter is the amplitude and the sine parameter the phase.
145
		Default: False (= fit sine and cosine terms)
146
	lifetime_prior : str, optional
147
		The prior probability density for each coefficient of the lifetime.
148
		Possible types are "flat" or `None` for a flat prior, "exp" for an
149
		exponential density ~ :math:`\\text{exp}(-|\\tau| / \\text{metric})`,
150
		and "normal" for a normal distribution
151
		~ :math:`\\text{exp}(-\\tau^2 / (2 * \\text{metric}^2))`.
152
		Default: None (= flat prior).
153
	lifetime_metric : float, optional
154
		The metric (scale) of the lifetime priors in days, see `prior`.
155
		Default 1.
156
	days_per_time_unit : float, optional
157
		The number of days per time unit, used to normalize the lifetime
158
		units. Use 365.25 if the times are in fractional years, or 1 if
159
		they are in days.
160
		Default: 365.25
161
	"""
162 1
	parameter_names = ("amp", "lag", "tau0",
163
			"taucos1", "tausin1", "taucos2", "tausin2",
164
			"ltscan")
165
166 1
	def __init__(self, proxy_times, proxy_vals,
167
			center=False,
168
			sza_intp=None, fit_phase=False,
169
			lifetime_prior=None, lifetime_metric=1.,
170
			days_per_time_unit=365.25,
171
			*args, **kwargs):
172 1
		self.mean = 0.
173 1
		if center:
174
			self.mean = np.nanmean(proxy_vals)
175 1
		self.times = proxy_times
176 1
		self.dt = 1.
177 1
		self.values = proxy_vals.values - self.mean
178 1
		self.sza_intp = sza_intp
179 1
		self.fit_phase = fit_phase
180 1
		self.days_per_time_unit = days_per_time_unit
181 1
		self.omega = 2 * np.pi * days_per_time_unit / 365.25
182 1
		self.lifetime_prior = lifetime_prior
183 1
		self.lifetime_metric = lifetime_metric
184
		# Makes "(m)jd" and "jyear" compatible for the lifetime
185
		# seasonal variation. The julian epoch (the default)
186
		# is slightly offset with respect to (modified) julian days.
187 1
		self.t_adj = 0.
188 1
		if self.days_per_time_unit == 1:
189
			# discrinate betweet julian days and modified julian days,
190
			# 1.8e6 is year 216 in julian days and year 6787 in
191
			# modified julian days. It should be pretty safe to judge on
192
			# that for most use cases.
193
			if self.times[0] > 1.8e6:
194
				# julian days
195
				self.t_adj = 13.
196
			else:
197
				# modified julian days
198
				self.t_adj = -44.25
199 1
		super(ProxyModel, self).__init__(*args, **kwargs)
200
201 1
	def _lt_corr(self, t, tau, tmax=60.):
202
		"""Lifetime corrected values
203
204
		Corrects for a finite lifetime by summing over the last `tmax`
205
		days with an exponential decay given of lifetime(s) `taus`.
206
		"""
207 1
		bs = np.arange(self.dt, tmax + self.dt, self.dt)
208 1
		yp = np.zeros_like(t)
209 1
		tauexp = np.exp(-self.dt / tau)
210 1
		taufac = np.ones_like(tau)
211 1
		for b in bs:
212 1
			taufac *= tauexp
213 1
			yp += np.interp(t - self.lag - b / self.days_per_time_unit,
214
					self.times, self.values, left=0., right=0.) * taufac
215 1
		return yp * self.dt
216
217 1
	def _lt_corr_grad(self, t, tau, tmax=60.):
218
		"""Lifetime corrected gradient
219
220
		Corrects for a finite lifetime by summing over the last `tmax`
221
		days with an exponential decay given of lifetime(s) `taus`.
222
		"""
223 1
		bs = np.arange(self.dt, tmax + self.dt, self.dt)
224 1
		ypg = np.zeros_like(t)
225 1
		tauexp = np.exp(-self.dt / tau)
226 1
		taufac = np.ones_like(tau)
227 1
		for b in bs:
228 1
			taufac *= tauexp
229 1
			ypg += np.interp(t - self.lag - b / self.days_per_time_unit,
230
					self.times, self.values, left=0., right=0.) * taufac * b
231 1
		return ypg * self.dt / tau**2
232
233 1
	def get_value(self, t):
234 1
		t = np.atleast_1d(t)
235 1
		proxy_val = np.interp(t - self.lag,
236
				self.times, self.values, left=0., right=0.)
237 1
		if self.ltscan == 0:
238
			# no lifetime, nothing else to do
239 1
			return self.amp * proxy_val
240
		# annual variation of the proxy lifetime
241 1
		if self.sza_intp is not None:
242
			# using the solar zenith angle
243
			tau_cs = (self.taucos1 * np.cos(np.radians(self.sza_intp(t)))
244
					+ self.tausin1 * np.sin(np.radians(self.sza_intp(t))))
245 1
		elif self.fit_phase:
246
			# using time (cos) and phase (sin)
247
			tau_cs = (self.taucos1 * np.cos(1 * self.omega * (t + self.t_adj) + self.tausin1)
248
					+ self.taucos2 * np.cos(2 * self.omega * (t + self.t_adj) + self.tausin2))
249
		else:
250
			# using time
251 1
			tau_cs = (self.taucos1 * np.cos(1 * self.omega * (t + self.t_adj))
252
					+ self.tausin1 * np.sin(1 * self.omega * (t + self.t_adj))
253
					+ self.taucos2 * np.cos(2 * self.omega * (t + self.t_adj))
254
					+ self.tausin2 * np.sin(2 * self.omega * (t + self.t_adj)))
255 1
		tau_cs[tau_cs < 0] = 0.  # clip to zero
256 1
		tau = self.tau0 + tau_cs
257 1
		if self.ltscan > 0:
258 1
			_ltscn = int(np.floor(self.ltscan))
259
		else:
260
			# infer the scan time from the maximal lifetime
261
			_ltscn = 3 * int(np.ceil(self.tau0 +
262
						np.sqrt(self.taucos1**2 + self.tausin1**2)))
263 1
		if np.all(tau > 0):
264 1
			proxy_val += self._lt_corr(t, tau, tmax=_ltscn)
265 1
		return self.amp * proxy_val
266
267 1
	def compute_gradient(self, t):
268 1
		t = np.atleast_1d(t)
269 1
		proxy_val = np.interp(t - self.lag,
270
				self.times, self.values, left=0., right=0.)
271 1
		proxy_val_grad0 = proxy_val.copy()
272
		# annual variation of the proxy lifetime
273 1
		if self.sza_intp is not None:
274
			# using the solar zenith angle
275
			dtau_cos1 = np.cos(np.radians(self.sza_intp(t)))
276
			dtau_sin1 = np.sin(np.radians(self.sza_intp(t)))
277
			dtau_cos2 = np.zeros_like(t)
278
			dtau_sin2 = np.zeros_like(t)
279
			tau_cs = self.taucos1 * dtau_cos1 + self.tausin1 * dtau_sin1
280 1
		elif self.fit_phase:
281
			# using time (cos) and phase (sin)
282
			dtau_cos1 = np.cos(1 * self.omega * (t + self.t_adj) + self.tausin1)
283
			dtau_sin1 = -self.taucos1 * np.sin(1 * self.omega * t + self.tausin1)
284
			dtau_cos2 = np.cos(2 * self.omega * (t + self.t_adj) + self.tausin2)
285
			dtau_sin2 = -self.taucos2 * np.sin(2 * self.omega * t + self.tausin2)
286
			tau_cs = self.taucos1 * dtau_cos1 + self.taucos2 * dtau_cos2
287
		else:
288
			# using time
289 1
			dtau_cos1 = np.cos(1 * self.omega * (t + self.t_adj))
290 1
			dtau_sin1 = np.sin(1 * self.omega * (t + self.t_adj))
291 1
			dtau_cos2 = np.cos(2 * self.omega * (t + self.t_adj))
292 1
			dtau_sin2 = np.sin(2 * self.omega * (t + self.t_adj))
293 1
			tau_cs = (self.taucos1 * dtau_cos1 + self.tausin1 * dtau_sin1 +
294
					self.taucos2 * dtau_cos2 + self.tausin2 * dtau_sin2)
295 1
		tau_cs[tau_cs < 0] = 0.  # clip to zero
296 1
		tau = self.tau0 + tau_cs
297 1
		if self.ltscan > 0:
298 1
			_ltscn = int(np.floor(self.ltscan))
299
		else:
300
			# infer the scan time from the maximal lifetime
301 1
			_ltscn = 3 * int(np.ceil(self.tau0 +
302
						np.sqrt(self.taucos1**2 + self.tausin1**2)))
303 1
		if np.all(tau > 0):
304 1
			proxy_val += self._lt_corr(t, tau, tmax=_ltscn)
305 1
			proxy_val_grad0 += self._lt_corr_grad(t, tau, tmax=_ltscn)
306 1
		return np.array([proxy_val,
307
				# set the gradient wrt lag to zero for now
308
				np.zeros_like(t),
309
				self.amp * proxy_val_grad0,
310
				self.amp * proxy_val_grad0 * dtau_cos1,
311
				self.amp * proxy_val_grad0 * dtau_sin1,
312
				self.amp * proxy_val_grad0 * dtau_cos2,
313
				self.amp * proxy_val_grad0 * dtau_sin2,
314
				# set the gradient wrt lifetime scan to zero for now
315
				np.zeros_like(t)])
316
317 1
	def _log_prior_normal(self):
318
		l_prior = super(ProxyModel, self).log_prior()
319
		if not np.isfinite(l_prior):
320
			return -np.inf
321
		for n, p in self.get_parameter_dict().items():
322
			if n.startswith("tau"):
323
				# Gaussian prior for the lifetimes
324
				l_prior -= 0.5 * (p / self.lifetime_metric)**2
325
		return l_prior
326
327 1
	def _log_prior_exp(self):
328 1
		l_prior = super(ProxyModel, self).log_prior()
329 1
		if not np.isfinite(l_prior):
330 1
			return -np.inf
331 1
		for n, p in self.get_parameter_dict().items():
332 1
			if n.startswith("tau"):
333
				# exponential prior for the lifetimes
334 1
				l_prior -= np.abs(p / self.lifetime_metric)
335 1
		return l_prior
336
337 1
	def log_prior(self):
338 1
		_priors = {"exp": self._log_prior_exp,
339
				"normal": self._log_prior_normal}
340 1
		if self.lifetime_prior is None or self.lifetime_prior == "flat":
341 1
			return super(ProxyModel, self).log_prior()
342 1
		return _priors[self.lifetime_prior]()
343
344
345 1 View Code Duplication
class TraceGasModelSet(ModelSet):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
346
	"""Combined model class for trace gases (and probably other data)
347
348
	Inherited from :class:`celerite.ModelSet`, provides `get_value()`
349
	and `compute_gradient()` methods.
350
	"""
351 1
	def get_value(self, t):
352 1
		v = np.zeros_like(t)
353 1
		for m in self.models.values():
354 1
			v += m.get_value(t)
355 1
		return v
356
357 1
	def compute_gradient(self, t):
358 1
		grad = []
359 1
		for m in self.models.values():
360 1
			grad.extend(list(m.compute_gradient(t)))
361 1
		return np.array(grad)
362
363
364 1 View Code Duplication
def setup_proxy_model_with_bounds(times, values,
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
365
		max_amp=1e10, max_days=100,
366
		**kwargs):
367
	# extract setup from `kwargs`
368 1
	center = kwargs.get("center", False)
369 1
	fit_phase = kwargs.get("fit_phase", False)
370 1
	lag = kwargs.get("lag", 0.)
371 1
	lt_metric = kwargs.get("lifetime_metric", 1)
372 1
	lt_prior = kwargs.get("lifetime_prior", "exp")
373 1
	lt_scan = kwargs.get("lifetime_scan", 60)
374 1
	positive = kwargs.get("positive", False)
375 1
	sza_intp = kwargs.get("sza_intp", None)
376 1
	time_format = kwargs.get("time_format", "jyear")
377
378 1
	return ProxyModel(times, values,
379
			center=center,
380
			sza_intp=sza_intp,
381
			fit_phase=fit_phase,
382
			lifetime_prior=lt_prior,
383
			lifetime_metric=lt_metric,
384
			days_per_time_unit=1 if time_format.endswith("d") else 365.25,
385
			amp=0.,
386
			lag=lag,
387
			tau0=0,
388
			taucos1=0, tausin1=0,
389
			taucos2=0, tausin2=0,
390
			ltscan=lt_scan,
391
			bounds=dict([
392
				("amp", [0, max_amp] if positive else [-max_amp, max_amp]),
393
				("lag", [0, max_days]),
394
				("tau0", [0, max_days]),
395
				("taucos1", [0, max_days] if fit_phase else [-max_days, max_days]),
396
				("tausin1", [-np.pi, np.pi] if fit_phase else [-max_days, max_days]),
397
				# semi-annual cycles for the life time
398
				("taucos2", [0, max_days] if fit_phase else [-max_days, max_days]),
399
				("tausin2", [-np.pi, np.pi] if fit_phase else [-max_days, max_days]),
400
				("ltscan", [0, 200])])
401
			)
402
403
404 1 View Code Duplication
def _default_proxy_config(tfmt="jyear"):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
405
	from .load_data import load_dailymeanLya, load_dailymeanAE
406
	proxy_config = {}
407
	# Lyman-alpha
408
	plyat, plyadf = load_dailymeanLya(tfmt=tfmt)
409
	proxy_config.update({"Lya": {
410
		"times": plyat,
411
		"values": plyadf["Lya"],
412
		"center": False,
413
		"positive": False,
414
		"lifetime_scan": 0,
415
		}}
416
	)
417
	# AE index
418
	paet, paedf = load_dailymeanAE(name="GM", tfmt=tfmt)
419
	proxy_config.update({"GM": {
420
		"times": paet,
421
		"values": paedf["GM"],
422
		"center": False,
423
		"positive": True,
424
		"lifetime_scan": 60,
425
		}}
426
	)
427
	return proxy_config
428
429
430 1 View Code Duplication
def trace_gas_model(constant=True, freqs=None, proxy_config=None, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
431
	"""Trace gas model setup
432
433
	Sets up the trace gas model for easy access. All parameters are optional,
434
	defaults to an offset, no harmonics, proxies are uncentered and unscaled
435
	Lyman-alpha and AE. AE with only positive amplitude and a seasonally
436
	varying lifetime.
437
438
	Parameters
439
	----------
440
	constant : bool, optional
441
		Whether or not to include a constant (offset) term, default is True.
442
	freqs : list, optional
443
		Frequencies of the harmonic terms in 1 / a^-1 (inverse years).
444
	proxy_config : dict, optional
445
		Proxy configuration if different from the standard setup.
446
	**kwargs : optional
447
		Additional keyword arguments, all of them are also passed on to
448
		the proxy setup. For now, supported are the following which are
449
		also passed along to the proxy setup with
450
		`setup_proxy_model_with_bounds()`:
451
452
		* fit_phase : bool
453
			fit amplitude and phase instead of sine and cosine
454
		* scale : float
455
			the factor by which the data is scaled, used to constrain
456
			the maximum and minimum amplitudes to be fitted.
457
		* tfmt : string
458
			The `astropy.time.Time` format string to setup the time axis.
459
		* max_amp : float
460
			Maximum magnitude of the coefficients, used to constrain the
461
			parameter search.
462
		* max_days : float
463
			Maximum magnitude of the lifetimes, used to constrain the
464
			parameter search.
465
466
	Returns
467
	-------
468
	model : :class:`TraceGasModelSet` (extended :class:`celerite.ModelSet`)
469
	"""
470 1
	fit_phase = kwargs.get("fit_phase", False)
471 1
	scale = kwargs.get("scale", 1e-6)
472 1
	tfmt = kwargs.get("time_format", "jyear")
473
474 1
	max_amp = kwargs.pop("max_amp", 1e10 * scale)
475 1
	max_days = kwargs.pop("max_days", 100)
476
477 1
	offset_model = []
478 1
	if constant:
479 1
		offset_model = [("offset",
480
				ConstantModel(value=0.,
481
						bounds={"value": [-max_amp, max_amp]}))]
482
483 1
	freqs = freqs or []
484 1
	harmonic_models = []
485 1
	for freq in freqs:
486 1
		if not fit_phase:
487 1
			harm = HarmonicModelCosineSine(freq=freq,
488
					cos=0, sin=0,
489
					bounds=dict([
490
						("cos", [-max_amp, max_amp]),
491
						("sin", [-max_amp, max_amp])])
492
			)
493
		else:
494
			harm = HarmonicModelAmpPhase(freq=freq,
495
					amp=0, phase=0,
496
					bounds=dict([
497
						("amp", [0, max_amp]),
498
						("phase", [-np.pi, np.pi])])
499
			)
500 1
		harm.freeze_parameter("freq")
501 1
		harmonic_models.append(("f{0:.0f}".format(freq), harm))
502
503 1
	proxy_config = proxy_config or _default_proxy_config(tfmt=tfmt)
504 1
	proxy_models = []
505 1
	for pn, conf in proxy_config.items():
506 1
		if "max_amp" not in conf:
507
			conf.update(dict(max_amp=max_amp))
508 1
		if "max_days" not in conf:
509
			conf.update(dict(max_days=max_days))
510 1
		kw = kwargs.copy()  # don't mess with the passed arguments
511 1
		kw.update(conf)
512 1
		proxy_models.append(
513
			(pn, setup_proxy_model_with_bounds(**kw))
514
		)
515
516
	return TraceGasModelSet(offset_model + harmonic_models + proxy_models)
517