diff --git a/sbpy/activity/gas/core.py b/sbpy/activity/gas/core.py index 9348594f..e19c45cf 100644 --- a/sbpy/activity/gas/core.py +++ b/sbpy/activity/gas/core.py @@ -1042,9 +1042,11 @@ def _make_binned_production(qs, ts) -> Callable[[np.float64], np.float64]: """ - q_invsecs = qs.to(1 / (u.s)).value - t_at_p_secs = ts.to(u.s).value base_q = qs[0] + q_variations = qs - base_q + + q_invsecs = q_variations.to_value(1 / (u.s)) + t_at_p_secs = ts.to_value(u.s) # extend the arrays to simplify our comparisons in binned_q_t # last bin stops at observation time, t = 0 @@ -1052,16 +1054,18 @@ def _make_binned_production(qs, ts) -> Callable[[np.float64], np.float64]: # junk value for production because this is in the future q_invsecs = np.append(q_invsecs, [0]) + # this function represents the deviation from base_q at any time t, so + # we need to handle times outside of the range specified in 'ts' def q_t(t): # too long in the past? - if t > t_at_p_secs[0] or t < 0: - return base_q + if t > t_at_p_secs[0]: + return 0 # in the future? if t < 0: return 0 - # loop over all elements except the last so we can always look at - # [index+1] for the comparison + # find which bin the given time falls in, and return the + # corresponding production for that interval for i in range(len(q_invsecs) - 1): if t < t_at_p_secs[i] and t > t_at_p_secs[i + 1]: return q_invsecs[i] diff --git a/sbpy/activity/gas/tests/test_core.py b/sbpy/activity/gas/tests/test_core.py index d85a75bc..4caffa75 100644 --- a/sbpy/activity/gas/tests/test_core.py +++ b/sbpy/activity/gas/tests/test_core.py @@ -5,69 +5,74 @@ import astropy.units as u import astropy.constants as const from .. import core -from .. import (photo_lengthscale, photo_timescale, fluorescence_band_strength, - VectorialModel, Haser) +from .. import ( + photo_lengthscale, + photo_timescale, + fluorescence_band_strength, + VectorialModel, + Haser, +) from .... import exceptions as sbe from ....data import Phys def test_photo_lengthscale(): - gamma = photo_lengthscale('OH', 'CS93') + gamma = photo_lengthscale("OH", "CS93") assert gamma == 1.6e5 * u.km def test_photo_lengthscale_error(): with pytest.raises(ValueError): - photo_lengthscale('asdf') + photo_lengthscale("asdf") with pytest.raises(ValueError): - photo_lengthscale('OH', source='asdf') + photo_lengthscale("OH", source="asdf") def test_photo_timescale(): - tau = photo_timescale('CO2', 'CE83') + tau = photo_timescale("CO2", "CE83") assert tau == 5.0e5 * u.s def test_photo_timescale_error(): with pytest.raises(ValueError): - photo_timescale('asdf') + photo_timescale("asdf") with pytest.raises(ValueError): - photo_timescale('OH', source='asdf') - - -@pytest.mark.parametrize('band, test', ( - ('OH 0-0', 1.54e-15 * u.erg / u.s), - ('OH 1-0', 1.79e-16 * u.erg / u.s), - ('OH 1-1', 2.83e-16 * u.erg / u.s), - ('OH 2-2', 1.46e-18 * u.erg / u.s), - ('OH 0-1', 0.00356 * 1.54e-15 * u.erg / u.s), - ('OH 0-2', 0.00021 * 1.54e-15 * u.erg / u.s), - ('OH 1-2', 0.00610 * 2.83e-16 * u.erg / u.s), - ('OH 2-0', 0.274 * 1.46e-18 * u.erg / u.s), - ('OH 2-1', 1.921 * 1.46e-18 * u.erg / u.s), -)) + photo_timescale("OH", source="asdf") + + +@pytest.mark.parametrize( + "band, test", + ( + ("OH 0-0", 1.54e-15 * u.erg / u.s), + ("OH 1-0", 1.79e-16 * u.erg / u.s), + ("OH 1-1", 2.83e-16 * u.erg / u.s), + ("OH 2-2", 1.46e-18 * u.erg / u.s), + ("OH 0-1", 0.00356 * 1.54e-15 * u.erg / u.s), + ("OH 0-2", 0.00021 * 1.54e-15 * u.erg / u.s), + ("OH 1-2", 0.00610 * 2.83e-16 * u.erg / u.s), + ("OH 2-0", 0.274 * 1.46e-18 * u.erg / u.s), + ("OH 2-1", 1.921 * 1.46e-18 * u.erg / u.s), + ), +) def test_fluorescence_band_strength_OH_SA88(band, test): "Tests values for -1 km/s at 1 au" - eph = { - 'rh': [1, 2] * u.au, - 'rdot': [-1, -1] * u.km / u.s - } - LN = fluorescence_band_strength(band, eph, 'SA88').to(test.unit) + eph = {"rh": [1, 2] * u.au, "rdot": [-1, -1] * u.km / u.s} + LN = fluorescence_band_strength(band, eph, "SA88").to(test.unit) assert np.allclose(LN.value, test.value / np.r_[1, 2] ** 2) def test_fluorescence_band_strength_error(): with pytest.raises(ValueError): - fluorescence_band_strength('asdf') + fluorescence_band_strength("asdf") with pytest.raises(ValueError): - fluorescence_band_strength('OH 0-0', source='asdf') + fluorescence_band_strength("OH 0-0", source="asdf") def test_gascoma_scipy_error(monkeypatch): - monkeypatch.setattr(core, 'scipy', None) + monkeypatch.setattr(core, "scipy", None) test = Haser(1 / u.s, 1 * u.km / u.s, 1e6 * u.km) with pytest.raises(sbe.RequiredPackageUnavailable): test._integrate_volume_density(1e5) @@ -78,7 +83,6 @@ def test_gascoma_scipy_error(monkeypatch): class TestHaser: - def test_volume_density(self): """Test a set of dummy values.""" Q = 1e28 / u.s @@ -87,11 +91,13 @@ def test_volume_density(self): daughter = 1e5 * u.km r = np.logspace(1, 7) * u.km n = Haser(Q, v, parent, daughter).volume_density(r) - rel = (daughter / (parent - daughter) - * (np.exp(-r / parent) - np.exp(-r / daughter))) + rel = ( + daughter + / (parent - daughter) + * (np.exp(-r / parent) - np.exp(-r / daughter)) + ) # test radial profile - assert np.allclose((n / n[0]).value, - (rel / rel[0] * (r[0] / r) ** 2).value) + assert np.allclose((n / n[0]).value, (rel / rel[0] * (r[0] / r) ** 2).value) # test parent-only coma near nucleus against that expected for # a long-lived species; will be close, but not exact @@ -99,7 +105,8 @@ def test_volume_density(self): assert np.isclose( n.decompose().value, (Q / v / 4 / np.pi / (10 * u.km) ** 2).decompose().value, - rtol=0.001) + rtol=0.001, + ) def test_column_density_small_aperture(self): """Test column density for aperture << lengthscale. @@ -113,8 +120,7 @@ def test_column_density_small_aperture(self): parent = 1e4 * u.km N_avg = 2 * Haser(Q, v, parent).column_density(rho) ideal = Q / v / 2 / rho - assert np.isclose(N_avg.decompose().value, ideal.decompose().value, - rtol=0.001) + assert np.isclose(N_avg.decompose().value, ideal.decompose().value, rtol=0.001) def test_column_density_small_angular_aperture(self): """Test column density for angular aperture << lengthscale. @@ -130,11 +136,9 @@ def test_column_density_small_angular_aperture(self): eph = dict(delta=1 * u.au) parent = 1e4 * u.km N_avg = 2 * Haser(Q, v, parent).column_density(rho, eph) - rho_km = (rho * eph['delta'] * 725.24 * u.km / u.arcsec / u.au - ).to('km') + rho_km = (rho * eph["delta"] * 725.24 * u.km / u.arcsec / u.au).to("km") ideal = Q / v / 2 / rho_km - assert np.isclose(N_avg.to_value('1/m2'), - ideal.to_value('1/m2'), rtol=0.001) + assert np.isclose(N_avg.to_value("1/m2"), ideal.to_value("1/m2"), rtol=0.001) def test_column_density(self): """ @@ -147,7 +151,7 @@ def test_column_density(self): parent = 1000 * u.km coma = Haser(Q, v, parent) N_avg = coma.column_density(rho) - integral = coma._integrate_volume_density(rho.to('m').value)[0] + integral = coma._integrate_volume_density(rho.to("m").value)[0] assert np.isclose(N_avg.decompose().value, integral) def test_total_number_large_aperture(self): @@ -176,7 +180,7 @@ def test_total_number_circular_aperture_angular(self): ap = core.CircularAperture(1 * u.arcsec).as_length(1 * u.au) coma = Haser(Q, v, parent, daughter) N = coma.total_number(ap) - assert np.isclose(N, 5.238964562688742e+26) + assert np.isclose(N, 5.238964562688742e26) def test_total_number_rho_AC75(self): """Reproduce A'Hearn and Cowan 1975 @@ -229,14 +233,14 @@ def test_total_number_rho_AC75(self): tab = [ [1.773, 40272, 4.603e30, 0.000000, 0.000000, 26.16, 0.000, 0.00], [1.053, 43451, 3.229e31, 1.354e31, 9.527e30, 26.98, 26.52, 26.5], - [0.893, 39084, 4.097e31, 1.273e31, 2.875e31, 27.12, 26.54, 27.0] + [0.893, 39084, 4.097e31, 1.273e31, 2.875e31, 27.12, 26.54, 27.0], ] for rh, rho, NC2, NCN, NC3, QC2, QCN, QC3 in tab: if NC2 > 0: parent = 1.0e4 * u.km daughter = 6.61e4 * u.km - Q = 10 ** QC2 / u.s + Q = 10**QC2 / u.s coma = Haser(Q, 1 * u.km / u.s, parent, daughter) N = coma.total_number(rho * u.km) assert np.isclose(NC2, N, rtol=0.01) @@ -244,7 +248,7 @@ def test_total_number_rho_AC75(self): if NCN > 0: parent = 1.3e4 * u.km daughter = 1.48e5 * u.km - Q = 10 ** QCN / u.s + Q = 10**QCN / u.s coma = Haser(Q, 1 * u.km / u.s, parent, daughter) N = coma.total_number(rho * u.km) assert np.isclose(NCN, N, rtol=0.01) @@ -252,7 +256,7 @@ def test_total_number_rho_AC75(self): if NC3 > 0: parent = 0 * u.km daughter = 4.0e4 * u.km - Q = 10 ** QC3 / u.s + Q = 10**QC3 / u.s coma = Haser(Q, 1 * u.km / u.s, parent, daughter) N = coma.total_number(rho * u.km) assert np.isclose(NC3, N, rtol=0.01) @@ -303,10 +307,8 @@ def test_total_number_annulus(self): parent = 10 * u.km N = Haser(Q, v, parent).total_number(aper) - N1 = Haser(Q, v, parent).total_number( - core.CircularAperture(aper.dim[0])) - N2 = Haser(Q, v, parent).total_number( - core.CircularAperture(aper.dim[1])) + N1 = Haser(Q, v, parent).total_number(core.CircularAperture(aper.dim[0])) + N2 = Haser(Q, v, parent).total_number(core.CircularAperture(aper.dim[1])) assert np.allclose(N, N2 - N1) @@ -340,7 +342,7 @@ def test_total_number_rectangular_ap(self): coma = Haser(Q, v, parent, daughter) N = coma.total_number(aper) - assert np.isclose(N, 3.449607967230623e+26) + assert np.isclose(N, 3.449607967230623e26) def test_total_number_gaussian_ap(self): """ @@ -371,10 +373,10 @@ def test_total_number_gaussian_ap(self): coma = Haser(Q, 1 * u.km / u.s, parent) N = coma.total_number(aper) - assert np.isclose(N, 5.146824269306973e+27, rtol=0.005) + assert np.isclose(N, 5.146824269306973e27, rtol=0.005) def test_missing_scipy(self, monkeypatch): - monkeypatch.setattr(core, 'scipy', None) + monkeypatch.setattr(core, "scipy", None) test = Haser(1 / u.s, 1 * u.km / u.s, 1e6 * u.km) with pytest.raises(sbe.RequiredPackageUnavailable): test._iK0(1) @@ -383,87 +385,240 @@ def test_missing_scipy(self, monkeypatch): class TestVectorialModel: + def test_small_vphoto(self): + """ + The other test using water as parent and hydroxyl as fragment have a + v_photo > v_outflow, but the model has a slightly different case for + v_photo < v_outflow + """ + + # Across python, fortran, and rust models, we get very very close to + # this number of fragments + num_fragments_grid = 1.2162140e33 + + base_q = 1.0e28 * 1 / u.s + + # Parent molecule is H2O + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) + # Fragment molecule is OH, but v_photo is modified to be smaller than + # v_outflow + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 0.5 * u.km / u.s} + ) + + coma = VectorialModel( + base_q=base_q, parent=parent, fragment=fragment + ) + + assert np.isclose( + coma.vmr.num_fragments_grid, num_fragments_grid, rtol=0.02 + ) + + def test_time_dependent_function(self): + """ + Test handing off a time dependence to the model with zero additional + time-dependent production from q_t: results should match a model with + steady production specified by base_q + Also uses a model with print_progress=true to avoid the code coverage + tests being polluted with trivial branches about the model + conditionally printing + """ + def q_t(t): + # for all times, return zero additional production + return t * 0 + + base_q = 1.0e28 * 1 / u.s + + # Parent molecule is H2O + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) + # Fragment molecule is OH + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + ) + + coma_steady = VectorialModel( + base_q=base_q, parent=parent, fragment=fragment + ) + + coma_q_t = VectorialModel( + base_q=base_q, parent=parent, fragment=fragment, q_t=q_t, + print_progress=True + ) + + assert np.isclose( + coma_steady.vmr.num_fragments_grid, coma_q_t.vmr.num_fragments_grid, rtol=0.02 + ) + + def test_binned_production_one_element_list(self): + """ + Initialize a comet with the fortran-version style of specifying time + dependent production and make it essentially steady production, and + test against a comet with the same steady production but initialized + differently + This also tests the model dealing with times in the past farther back + than is specified in the variation list 'ts': it extends the oldest + production value (here, 1.0e28) back infinitely into the past + """ + # production from 1 day ago until the present, + ts = [1] * u.day + # is a steady value of 1e28 + qs = [1.0e28] / u.s + + base_q = 1.0e28 * 1 / u.s + + # Parent molecule is H2O + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) + # Fragment molecule is OH + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + ) + + coma_binned = VectorialModel.binned_production( + qs=qs, ts=ts, parent=parent, fragment=fragment + ) + + coma_steady = VectorialModel( + base_q=base_q, parent=parent, fragment=fragment + ) + + assert np.isclose( + coma_steady.vmr.num_fragments_grid, coma_binned.vmr.num_fragments_grid, rtol=0.02 + ) + + def test_binned_production_multi_element_list(self): + """ + Initialize a comet with the fortran-version style of specifying time + dependent production and make it steady production, then test against a + comet with the same steady production but initialized differently + """ + # Specify that at multiple points in time, + ts = [60, 50, 40, 30, 20, 10] * u.day + # production is a steady value of 1e28 + qs = [1.0e28, 1.0e28, 1.0e28, 1.0e28, 1.0e28, 1.0e28] / u.s + + base_q = 1.0e28 * 1 / u.s + + # Parent molecule is H2O + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) + # Fragment molecule is OH + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + ) + + coma_binned = VectorialModel.binned_production( + qs=qs, ts=ts, parent=parent, fragment=fragment + ) + + coma_steady = VectorialModel( + base_q=base_q, parent=parent, fragment=fragment + ) + + assert np.isclose( + coma_steady.vmr.num_fragments_grid, coma_binned.vmr.num_fragments_grid, rtol=0.02 + ) def test_grid_count(self): """ - Compute theoretical number of fragments vs. integrated value from - grid. This is currently only a good estimate for steady production - due to our method for determining the theoretical count of - fragments + Compute theoretical number of fragments vs. integrated value from + grid. This is currently only a good estimate for steady production + due to our method for determining the theoretical count of + fragments """ - base_q = 1.e28 * 1 / u.s + base_q = 1.0e28 * 1 / u.s # Parent molecule is H2O - parent = Phys.from_dict({ - 'tau_T': 86430 * u.s, - 'tau_d': 101730 * u.s, - 'v_outflow': 1 * u.km / u.s, - 'sigma': 3e-16 * u.cm ** 2 - }) + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) # Fragment molecule is OH - fragment = Phys.from_dict({ - 'tau_T': photo_timescale('OH') * 0.93, - 'v_photo': 1.05 * u.km / u.s - }) + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + ) - coma = VectorialModel(base_q=base_q, - parent=parent, - fragment=fragment) + coma = VectorialModel(base_q=base_q, parent=parent, fragment=fragment) assert np.isclose( - coma.vmr.num_fragments_theory, - coma.vmr.num_fragments_grid, - rtol=0.02 - ) + coma.vmr.num_fragments_theory, coma.vmr.num_fragments_grid, rtol=0.02 + ) def test_total_number_large_aperture(self): """ - Compare theoretical number of fragments vs. integration of column - density over a large aperture + Compare theoretical number of fragments vs. integration of column + density over a large aperture """ base_q = 1e28 * 1 / u.s # Parent molecule is H2O - parent = Phys.from_dict({ - 'tau_T': 86430 * u.s, - 'tau_d': 101730 * u.s, - 'v_outflow': 1 * u.km / u.s, - 'sigma': 3e-16 * u.cm ** 2 - }) + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) # Fragment molecule is OH - fragment = Phys.from_dict({ - 'tau_T': photo_timescale('OH') * 0.93, - 'v_photo': 1.05 * u.km / u.s - }) + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + ) - coma = VectorialModel(base_q=base_q, - parent=parent, - fragment=fragment) + coma = VectorialModel(base_q=base_q, parent=parent, fragment=fragment) ap = core.CircularAperture(coma.vmr.max_grid_radius) assert np.isclose( - coma.vmr.num_fragments_theory, - coma.total_number(ap), - rtol=0.02 - ) + coma.vmr.num_fragments_theory, coma.total_number(ap), rtol=0.02 + ) def test_model_symmetry(self): """ - The symmetry of the model allows the parent production to be - treated as an overall scaling factor, and does not affect the - features of the calculated densities. - - If we assume an arbitrary fixed count inside an aperture, we can - use this to calculate what the production would need to be to - produce this count after running the model with a 'dummy' value for - the production. - - If we then run another model at this calculated production and use - the same aperture, we should recover the number of counts assumed - in the paragraph above. If we do not, we have broken our model - somehow and lost the symmetry during our calculations. + The symmetry of the model allows the parent production to be + treated as an overall scaling factor, and does not affect the + features of the calculated densities. + + If we assume an arbitrary fixed count inside an aperture, we can + use this to calculate what the production would need to be to + produce this count after running the model with a 'dummy' value for + the production. + + If we then run another model at this calculated production and use + the same aperture, we should recover the number of counts assumed + in the paragraph above. If we do not, we have broken our model + somehow and lost the symmetry during our calculations. """ base_production = 1e28 @@ -474,21 +629,22 @@ def test_model_symmetry(self): assumed_count = 1e32 # Parent molecule is H2O - parent = Phys.from_dict({ - 'tau_T': 86430 * u.s, - 'tau_d': 101730 * u.s, - 'v_outflow': 1 * u.km / u.s, - 'sigma': 3e-16 * u.cm ** 2 - }) + parent = Phys.from_dict( + { + "tau_T": 86430 * u.s, + "tau_d": 101730 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) # Fragment molecule is OH - fragment = Phys.from_dict({ - 'tau_T': photo_timescale('OH') * 0.93, - 'v_photo': 1.05 * u.km / u.s - }) + fragment = Phys.from_dict( + {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + ) - coma = VectorialModel(base_q=base_production * (1 / u.s), - parent=parent, - fragment=fragment) + coma = VectorialModel( + base_q=base_production * (1 / u.s), parent=parent, fragment=fragment + ) model_count = coma.total_number(ap) calculated_q = (assumed_count / model_count) * base_production @@ -508,20 +664,23 @@ def test_model_symmetry(self): # coma_check = VectorialModel(base_q=calculated_q * (1 / u.s), # parent=parent_check, # fragment=fragment_check) - coma_check = VectorialModel(base_q=calculated_q * (1 / u.s), - parent=parent, - fragment=fragment) + coma_check = VectorialModel( + base_q=calculated_q * (1 / u.s), parent=parent, fragment=fragment + ) count_check = coma_check.total_number(ap) assert np.isclose(count_check, assumed_count, rtol=0.001) - @pytest.mark.parametrize("rh,delta,flux,g,Q", ( - [1.2912, 0.7410, 337e-14, 2.33e-4, 1.451e28], - [1.2949, 0.7651, 280e-14, 2.60e-4, 1.228e28], - [1.3089, 0.8083, 480e-14, 3.36e-4, 1.967e28], - [1.3200, 0.8353, 522e-14, 3.73e-4, 2.025e28], - [1.3366, 0.8720, 560e-14, 4.03e-4, 2.035e28], - )) + @pytest.mark.parametrize( + "rh,delta,flux,g,Q", + ( + [1.2912, 0.7410, 337e-14, 2.33e-4, 1.451e28], + [1.2949, 0.7651, 280e-14, 2.60e-4, 1.228e28], + [1.3089, 0.8083, 480e-14, 3.36e-4, 1.967e28], + [1.3200, 0.8353, 522e-14, 3.73e-4, 2.025e28], + [1.3366, 0.8720, 560e-14, 4.03e-4, 2.035e28], + ), + ) def test_festou92(self, rh, delta, flux, g, Q): """Compare to Festou et al. 1992 production rates of comet 6P/d'Arrest. @@ -540,7 +699,7 @@ def test_festou92(self, rh, delta, flux, g, Q): # add units rh = rh * u.au delta = delta * u.au - flux = flux * u.erg / u.s / u.cm ** 2 + flux = flux * u.erg / u.s / u.cm**2 g = g / u.s Q = Q / u.s @@ -548,18 +707,18 @@ def test_festou92(self, rh, delta, flux, g, Q): L_N = g / (rh / u.au) ** 2 * const.h * const.c / (3086 * u.AA) # Parent molecule is H2O - parent = Phys.from_dict({ - 'tau_T': 65000 * (rh / u.au) ** 2 * u.s, - 'tau_d': 72500 * (rh / u.au) ** 2 * u.s, - 'v_outflow': 0.85 * u.km / u.s, - 'sigma': 3e-16 * u.cm ** 2, - - }) + parent = Phys.from_dict( + { + "tau_T": 65000 * (rh / u.au) ** 2 * u.s, + "tau_d": 72500 * (rh / u.au) ** 2 * u.s, + "v_outflow": 0.85 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) # Fragment molecule is OH - fragment = Phys.from_dict({ - 'tau_T': 160000 * (rh / u.au) ** 2 * u.s, - 'v_photo': 1.05 * u.km / u.s - }) + fragment = Phys.from_dict( + {"tau_T": 160000 * (rh / u.au) ** 2 * u.s, "v_photo": 1.05 * u.km / u.s} + ) # https://pds.nasa.gov/ds-view/pds/viewInstrumentProfile.jsp?INSTRUMENT_ID=LWP&INSTRUMENT_HOST_ID=IUE # Large-Aperture Length(arcsec) 22.51+/-0.40 @@ -576,7 +735,7 @@ def test_festou92(self, rh, delta, flux, g, Q): coma = VectorialModel(base_q=Q0, parent=parent, fragment=fragment) N0 = coma.total_number(lwp, eph=delta) - Q_model = (Q0 * flux / (L_N * N0) * 4 * np.pi * delta ** 2).to(Q.unit) + Q_model = (Q0 * flux / (L_N * N0) * 4 * np.pi * delta**2).to(Q.unit) # absolute tolerance: Table 2 has 3 significant figures # @@ -585,14 +744,17 @@ def test_festou92(self, rh, delta, flux, g, Q): atol = 1.01 * 10 ** (np.floor(np.log10(Q0.value)) - 2) * Q.unit assert u.allclose(Q, Q_model, atol=atol, rtol=0.14) - @pytest.mark.parametrize("rh,delta,N,Q", ( - [1.8662, 0.9683, 0.2424e32, 1.048e29], - [0.8855, 0.9906, 3.819e32, 5.548e29], - [0.9787, 0.8337, 1.63e32, 3.69e29], - [1.0467, 0.7219, 0.8703e32, 2.813e29], - [1.9059, 1.4031, 1.07e32, 2.76e29], - [2.0715, 1.7930, 1.01e32, 1.88e29] - )) + @pytest.mark.parametrize( + "rh,delta,N,Q", + ( + [1.8662, 0.9683, 0.2424e32, 1.048e29], + [0.8855, 0.9906, 3.819e32, 5.548e29], + [0.9787, 0.8337, 1.63e32, 3.69e29], + [1.0467, 0.7219, 0.8703e32, 2.813e29], + [1.9059, 1.4031, 1.07e32, 2.76e29], + [2.0715, 1.7930, 1.01e32, 1.88e29], + ), + ) def test_combi93(self, rh, delta, N, Q): """Compare to results of Combi et al. 1993. @@ -611,18 +773,19 @@ def test_combi93(self, rh, delta, N, Q): aper = core.RectangularAperture((10, 15) * u.arcsec) # Parent molecule is H2O - parent = Phys.from_dict({ - 'tau_T': 8.2e4 * 0.88 * (rh / u.au) ** 2 * u.s, - 'tau_d': 8.2e4 * (rh / u.au) ** 2 * u.s, - 'v_outflow': 1 * u.km / u.s, - 'sigma': 3e-16 * u.cm ** 2, - }) + parent = Phys.from_dict( + { + "tau_T": 8.2e4 * 0.88 * (rh / u.au) ** 2 * u.s, + "tau_d": 8.2e4 * (rh / u.au) ** 2 * u.s, + "v_outflow": 1 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) # Fragment molecule is OH - fragment = Phys.from_dict({ - 'tau_T': 2.0e5 * (rh / u.au) ** 2 * u.s, - 'v_photo': 1.05 * u.km / u.s - }) + fragment = Phys.from_dict( + {"tau_T": 2.0e5 * (rh / u.au) ** 2 * u.s, "v_photo": 1.05 * u.km / u.s} + ) Q0 = 2e29 / u.s coma = VectorialModel(base_q=Q0, parent=parent, fragment=fragment) @@ -685,33 +848,36 @@ def test_vm_fortran(self): """ - eph = {'rh': 2.469 * u.au, 'delta': 1.514 * u.au} + eph = {"rh": 2.469 * u.au, "delta": 1.514 * u.au} # Parent molecule is H2O - parent = Phys.from_dict({ - 'tau_T': 86430 * (eph['rh'] / u.au) ** 2 * u.s, - 'tau_d': 101730 * (eph['rh'] / u.au) ** 2 * u.s, - 'v_outflow': 0.514 * u.km / u.s, - 'sigma': 3e-16 * u.cm ** 2, - - }) + parent = Phys.from_dict( + { + "tau_T": 86430 * (eph["rh"] / u.au) ** 2 * u.s, + "tau_d": 101730 * (eph["rh"] / u.au) ** 2 * u.s, + "v_outflow": 0.514 * u.km / u.s, + "sigma": 3e-16 * u.cm**2, + } + ) # Fragment molecule is OH - fragment = Phys.from_dict({ - 'tau_T': 129000 * (eph['rh'] / u.au) ** 2 * u.s, - 'v_photo': 1.05 * u.km / u.s - }) + fragment = Phys.from_dict( + { + "tau_T": 129000 * (eph["rh"] / u.au) ** 2 * u.s, + "v_photo": 1.05 * u.km / u.s, + } + ) # test values are copy-pasted from wm.f output - collision_sphere_radius = 0.14E+07 * u.cm + collision_sphere_radius = 0.14e07 * u.cm collision_sphere_radius_atol = 0.011e7 * u.cm - N_fragments_theory = 0.668E+33 + N_fragments_theory = 0.668e33 # N_fragments_theory_atol = 0.0011e33 N_fragments_theory_rtol = 0.001 - N_fragments = 0.657E+33 + N_fragments = 0.657e33 # N_fragments_atol = 0.0011e33 N_fragments_rtol = 0.003 - fragment_volume_density = ''' + fragment_volume_density = """ 0.97E+03 0.43E+03 0.68E+04 0.59E+02 0.13E+05 0.31E+02 0.18E+05 0.21E+02 0.24E+05 0.15E+02 0.31E+05 0.11E+02 0.43E+05 0.79E+01 0.54E+05 0.59E+01 0.66E+05 0.46E+01 0.78E+05 0.38E+01 0.90E+05 0.31E+01 0.11E+06 0.24E+01 @@ -724,8 +890,8 @@ def test_vm_fortran(self): 0.16E+07 0.44E-02 0.17E+07 0.31E-02 0.19E+07 0.23E-02 0.20E+07 0.17E-02 0.22E+07 0.12E-02 0.24E+07 0.78E-03 0.27E+07 0.51E-03 0.29E+07 0.34E-03 0.31E+07 0.23E-03 0.34E+07 0.15E-03 0.37E+07 0.90E-04 0.41E+07 0.53E-04 -0.44E+07 0.32E-04 0.48E+07 0.20E-04''' - fragment_column_density = ''' +0.44E+07 0.32E-04 0.48E+07 0.20E-04""" + fragment_column_density = """ 0.970E+03 4.77E+11 1.088E+03 4.69E+11 1.221E+03 4.61E+11 1.370E+03 4.53E+11 1.537E+03 4.44E+11 1.724E+03 4.34E+11 1.935E+03 4.22E+11 2.171E+03 4.15E+11 2.436E+03 4.00E+11 2.733E+03 3.85E+11 3.066E+03 3.74E+11 3.441E+03 3.67E+11 @@ -744,22 +910,21 @@ def test_vm_fortran(self): 9.697E+05 4.25E+09 1.088E+06 3.19E+09 1.221E+06 2.34E+09 1.370E+06 1.68E+09 1.537E+06 1.19E+09 1.724E+06 8.29E+08 1.935E+06 5.67E+08 2.171E+06 3.78E+08 2.436E+06 2.45E+08 2.733E+06 1.54E+08 3.066E+06 9.34E+07 3.441E+06 5.39E+07 -''' +""" # convert strings to arrays - x = np.fromstring(fragment_volume_density, sep=' ') + x = np.fromstring(fragment_volume_density, sep=" ") n0_rho = x[::2] * u.km - n0 = x[1::2] / u.cm ** 3 + n0 = x[1::2] / u.cm**3 # absolute tolerance: 2 significant figures n0_atol = 1.1 * 10 ** (np.floor(np.log10(n0.value)) - 1) * n0.unit n0_atol_revised = n0_atol * 7 - x = np.fromstring(fragment_column_density, sep=' ') + x = np.fromstring(fragment_column_density, sep=" ") sigma0_rho = x[::2] * u.km - sigma0 = x[1::2] / u.cm ** 2 + sigma0 = x[1::2] / u.cm**2 # absolute tolerance: 3 significant figures - sigma0_atol = (1.1 * 10 ** (np.floor(np.log10(sigma0.value)) - 2) - * sigma0.unit) + sigma0_atol = 1.1 * 10 ** (np.floor(np.log10(sigma0.value)) - 2) * sigma0.unit sigma0_atol_revised = sigma0_atol * 40 # evaluate the model @@ -769,15 +934,21 @@ def test_vm_fortran(self): sigma = [coma.column_density(r) for r in sigma0_rho] # compare results - assert u.isclose(coma.vmr.collision_sphere_radius, - collision_sphere_radius, - atol=collision_sphere_radius_atol) + assert u.isclose( + coma.vmr.collision_sphere_radius, + collision_sphere_radius, + atol=collision_sphere_radius_atol, + ) - assert np.isclose(coma.vmr.num_fragments_theory, - N_fragments_theory, rtol=N_fragments_theory_rtol) + assert np.isclose( + coma.vmr.num_fragments_theory, + N_fragments_theory, + rtol=N_fragments_theory_rtol, + ) - assert np.isclose(coma.vmr.num_fragments_grid, - N_fragments, rtol=N_fragments_rtol) + assert np.isclose( + coma.vmr.num_fragments_grid, N_fragments, rtol=N_fragments_rtol + ) assert u.allclose(n, n0, atol=n0_atol_revised)