<resource schema="gdr3spec" resdir=".">
	<macDef name="pubDIDBase">ivo://\getConfig{ivoa}{authority}/~?\rdId/</macDef>
	<!-- This sampling is reproduced in dr3_to_sampled; if you ever changed
	this, you'd have fix it there and re-run it.
	-->
	<macDef name="spectralPoints">400.0 410.0 420.0 430.0 440.0 450.0 460.0 470.0
	480.0 490.0 500.0 510.0 520.0 530.0 540.0 550.0 560.0 570.0 580.0 590.0 600.0
	610.0 620.0 630.0 640.0 650.0 660.0 670.0 680.0 690.0 700.0 710.0 720.0 730.0
	740.0 750.0 760.0 770.0 780.0 790.0 800.0</macDef>

	<meta name="title">Gaia DR3 RP/BP (XP) Monte Carlo sampled spectra</meta>
	<meta name="description" format="rst">
		This is a re-publication the Gaia DR3 RP/BP spectra in the IVOA Spectral
		Data Model.  It presents the continous spectra in sampled form, using a
		Monte Carlo scheme to decorrelate errors, elaborated in this resource's
		reference URL.  The underlying tables are also available for querying
		through TAP, which opens some powerful methods for mass-analysing the data.
	</meta>
	<meta name="creator">Demleitner, M.;Andrae, R.</meta>
	<meta name="content">
		<meta name="type">Catalog</meta>
	</meta>
	<meta name="coverage.waveband">Optical</meta>
	<meta name="creationDate">2022-06-20T10:00:00Z</meta>
	<meta name="schema-rank">20</meta>

	<meta name="subject">stars</meta>
	<meta name="subject">surveys</meta>
	<meta name="subject">spectroscopy</meta>

	<meta name="source">2022arXiv220800211G</meta>
	<meta name="doi">10.21938/W:hWbPah3wBabuqewQULTA</meta>

	<meta name="facility">Gaia</meta>
	<meta name="instrument">Gaia BP/BP Spectrometer</meta>

	<meta name="copyright" format="rst">
		This data is derived from public Gaia DR3 data.  Please take note of
		`ESAC's guide`_ on how to acknowledge and cite Gaia results.

		.. _ESAC's guide: https://gea.esac.esa.int/archive/documentation/GDR3/Miscellaneous/sec_credit_and_citation_instructions/
	</meta>

	<meta name="ssap.creationType">archival</meta>
	<meta name="ssap.dataSource">survey</meta>

	<meta name="_longdoc" format="rst"><![CDATA[
		.. role:: raw-html(raw)
			:format: html

		:raw-html:`<style type="text/css">@import url(/static/css/math.css);</style>`

		In Gaia's DR3, most BP/RP spectra come in “continuous” form only, that is,
		as coefficients of Gauss-Hermite polynomials.  These can be turned into
		a “sampled” representation using GaiaXPy_; however, since the errors are
		given in the form of covariance matrices for the polynomial coefficients,
		the errors in the resulting spectra are strongly correleated, which can
		sometimes result in artefacts in the signal.

		.. _GaiaXPy: https://github.com/gaia-dpci/GaiaXPy.git

		To get approximately decorrelated errors and hence sampled spectra usable
		with less caution, we apply a scheme of Monte Carlo-sampling different
		realisations from the error model of the coefficients.  Specifically, given
		the covariance matrix :math:`C` defined through the Xp_coefficient_errors
		and Xp_coefficient_correlations column in the DR3
		xp_continuous_mean_spectrum table, and noting that for a unit
		normal-distributed random variable :math:`u`

		.. math::
			\\langle u.u^T\\rangle = 1

		holds (:math:`\\langle x\\rangle` denotes the expectation), we can re-write
		the covariance matrix using a Cholesky decomposition into :math:`LL^T` as

		.. math::
				C =& L L^T \\\\
				  =& L \\langle u.u^T\\rangle L^T\\\\
				  =& \\langle L u . u^T L^T\\rangle \\\\
				  =& \\langle x.x^T\\rangle

		Hence, :math:`x=L u` is a realisation of the errors satisfying the
		covariance matrix.  To come up with a sampled spectrum, we now
		draw (in this case) 10 samples of the coefficients and have GaiaXPy
		convert them to a sampled spectrum.

		The source code we used for that is `dr3_to_mcsampled.py`_.

		.. _dr3_to_mcsampled.py: /gaia/s3/preview/static/dr3_to_mcsampled.py

		To be on the conservative side of the resolution and the bandwidth,
		and also to keep storage requirements modest, we have chosen a
		relatively rough grid over the optical band, that is, bins of 10 nm
		over the spectral range between 400 and 800 nm.
	]]></meta>

	<coverage>	
		<temporal>2014-07-25 2017-05-28</temporal>
		<spatial>0/0-11</spatial>
		<spectral>1.986e-19 4.966e-19</spectral>
	</coverage>

	<table id="spectra" onDisk="True" primary="source_id" adql="True"
			nrows="219196404">
		<meta name="title">Monte Carlo sampled DR3 XP spectra</meta>
		<meta name="description">This table contains the sampled
		spectra, their errors (as the standard deviation of the samples
		between the different realisations), and the Gaia DR3 source_id.
		Join this table on source_id with gaia.dr3lite to obtain information
		on the sources.</meta>

		<dm>
			(gavo-spec:spectrum) {
				spectralAxis: @spectral
				fluxAxis: @flux
			}
		</dm>
		<param name="spectral" type="real[41]"
			unit="nm" ucd="em.wl"
			description="Spectral points at which flux and flux_error are
				being sampled">\spectralPoints</param>

		<column original="gaia/q3#dr3lite.source_id"/>
		<column name="flux" type="real[41]"
			unit="W.m**-2.nm**-1" ucd="phot.flux;em.opt"
			description="mean BP + RP combined spectrum flux"
			verbLevel="1"/>
		<column name="flux_error" type="real[41]"
			unit="W.m**-2.nm**-1" ucd="stat.error;phot.flux;em.opt"
			description="mean BP + RP combined spectrum flux error"
			verbLevel="1"/>
	</table>

	<data id="import_fromcont">
		<sources pattern="data3/xp_sampled_computed.txt.gz"/>
		<directGrammar id="specboost"
			cBooster="res/dr3xpbooster.c" preFilter="zcat"
			type="split" splitChar="\t" recordSize="30000"/>
		<make table="spectra"/>
	</data>

	<table id="withpos" adql="True" namePath="gaia/q3#dr3lite"
			onDisk="True" nrows="219196404">
		<meta name="title">Monte Carlo Sampled DR3 XP Spectra with Basic Object
		  Information</meta>
		<meta name="description">This table contains the data from
		\schema.spectra plus position and photometry from gaia.dr3lite.
		It is a view, and there is generally no advantage to using it
		instead of manually performing the join.</meta>

		<property name="forceStats">present</property>
		<param original="spectra.spectral"/>

		<LOOP>
			<listItems>
				source_id ra dec ra_error dec_error phot_g_mean_mag
				phot_bp_mean_mag phot_rp_mean_mag spectra.flux spectra.flux_error
			</listItems>
			<events>
				<column original="\item"/>
			</events>
		</LOOP>

		<viewStatement>
			CREATE VIEW \qName AS (
				SELECT \colNames FROM (
					\schema.spectra
					JOIN gaia.dr3lite
					USING (source_id)))
		</viewStatement>
	</table>

	<macDef name="copy_from_withpos"
		>phot_g_mean_mag phot_bp_mean_mag phot_rp_mean_mag source_id ra dec</macDef>

	<table id="ssameta" onDisk="True" namePath="withpos" adql="True"
			nrows="219196404">
		<meta name="description">SSA Metadata for the Monte Carlo-sampled
		  Gaia DR3 XP spectra</meta>
		<LOOP listItems="\copy_from_withpos">
			<events>
				<column original="\item"/>
			</events>
		</LOOP>
		<column name="preview" type="text"
			ucd="meta.ref.url;meta.preview" tablehead="Preview"
			description="URL of a preview for the dataset"
			displayHint="type=product" verbLevel="15"/>

		<mixin
			accref="'\getConfig{web}{serverURL}/\rdId/sdl/dlget?ID=' || source_id"
			accsize="20000"
			copiedcolumns="\copy_from_withpos"
			embargo="NULL"
			mime="'application/x-votable+xml'"
			owner="NULL"
			sourcetable="withpos"
			ssa_aperture="0.0005"
			ssa_bandpass="'Optical'"
			ssa_binSize="10e-9"
			ssa_collection="'Gaia DR3 XP'"
			ssa_dateObs="57382.0"
			ssa_dstitle="'GDR3 XP MC ' || source_id"
			ssa_fluxcalib="'ABSOLUTE'"
			ssa_fluxunit="'W.m**-2.nm**-1'"
			ssa_length="41"
			ssa_location="spoint(radians(ra),\+ radians(dec))"
			ssa_pubDID="'ivo://org.gavo.dc/gaia/s3/sdl?' || source_id"
			ssa_region="NULL"
			ssa_speccalib="'ABSOLUTE'"
			ssa_specend="8e-7"
			ssa_specext="4e-7"
			ssa_specmid="6e-7"
			ssa_specres="10e-9"
			ssa_specstart="4e-7"
			ssa_spectralucd="'em.wl'"
			ssa_spectralunit="'nm'"
			customcode=", '\getConfig{web}{serverURL}/\rdId/preview/qp/' || source_id
				AS preview"
		>//ssap#view</mixin>

<!--		<mixin
			calibLevel="2"
			coverage="ssa_region"
			sResolution="ssa_spaceRes"
			oUCD="ssa_fluxucd"
			emUCD="ssa_spectralucd"
			>//obscore#publishSSAPMIXC</mixin> -->
	</table>

	<data id="make_ssa" recreateAfter="make_second_view">
		<make table="withpos"/>
	</data>
	
	<data id="make_second_view" auto="False">
		<make table="ssameta"/>
	</data>

	<table id="instance" onDisk="False">
		<meta name="description">Gaia DR3 XP spectrum, Monte Carlo-smoothed
			GaiaXPy sampling</meta>
		<mixin ssaTable="ssameta"
			spectralDescription="Wavelength"
			fluxDescription="Flux Density"
			>//ssap#sdm-instance</mixin>
		<column name="flux_error" type="real"
			ucd="stat.error;phot.flux.density;em.wl"
			description="Error in flux reconstructed as the standard deviation
				of the Monte Carlo-sampled flux values at this point."/>
	</table>

	<data id="build_spectrum">
		<!-- builds an SDM instance for an XP spectrum; sourceToken is
		a DR3 source_id as integer or str. -->
		<embeddedGrammar>
			<iterator>
				<setup><code>
					SAMPLES = [float(v) for v in """\spectralPoints""".split()]
				</code></setup>
				<code>
				with base.getTableConn() as conn:
					source_id = self.sourceToken["source_id"]
					res = list(conn.query(
						"select flux, flux_error"
						" from \schema.spectra"
						" where source_id=%(source_id)s",
						locals()))
					if not res:
						raise UnknownURI(f"No spectrum available for '{source_id}'")
					
					for rec in zip(SAMPLES, *res[0]):
						yield {
							"spectral": rec[0],
							"flux": rec[1],
							"flux_error": rec[2]}
				</code>
			</iterator>
		</embeddedGrammar>
		<make table="instance">
			<parmaker>
				<apply procDef="//ssap#feedSSAToSDM"/>
				<apply name="update_metadata">
					<code>
						sourceId = vars["parser_"].sourceToken["source_id"]
						targetTable.setMeta("description",
							base.getMetaText(targetTable, "description")
							+" for Gaia DR3 object {}".format(sourceId))
						targetTable.setMeta("name", str(sourceId))
					</code>
				</apply>
			</parmaker>
			<rowmaker idmaps="*"/>
		</make>
	</data>

	<service id="sdl" allowed="dlmeta,dlget">
		<meta name="title">Gaia DR3 XP spectrum datalink</meta>
		<datalinkCore>
			<descriptorGenerator>
				<setup imports="gavo.protocols.datalink, gavo.protocols.soda,
						gavo.rsc, gavo.protocols.ssap">
					<code>
						class SpecDescriptor(datalink.DatalinkDescriptorMixin):
							suppressAutoLinks = True
							data = None
							forSemantics = soda.DEFAULT_SEMANTICS

							def __init__(self, pubDID, metadataRow):
								self.pubDID, self.metadata = pubDID, metadataRow

							@classmethod
							def fromDB(cls, did, conn):
								ssaTD = rd.getById("ssameta")
								t = rsc.TableForDef(ssaTD, connection=conn
									).getTableForQuery(
									ssaTD,
									"source_id=%(source_id)s",
									{"source_id": did})

								if not t.rows:
									return DatalinkFault.NotFoundFault(did,
										"No spectrum with this source_id known here")
								
								return cls(did, t.rows[0])
					</code>
				</setup>
				<code>
					with base.getTableConn() as conn:
						return SpecDescriptor.fromDB(pubDID, conn)
				</code>
			</descriptorGenerator>

			<metaMaker semantics="#preview">
				<code>
					yield descriptor.makeLink(
						descriptor.metadata['preview'],
						description="Preview image",
						contentType='image/png')
				</code>
			</metaMaker>

			<metaMaker semantics="#this">
				<code>
					yield descriptor.makeLink(
						descriptor.metadata['accref'],
						description="Low-resolution, Monte Carlo-sampled spectrum",
						contentType='appliation/x-votable+xml',
						contentQualifier="#spectrum")
				</code>
			</metaMaker>

			<metaMaker semantics="#coderived">
				<code>
					yield descriptor.makeLink(
						"https://gaia.ari.uni-heidelberg.de/tap/sync?LANG=ADQL&amp;"
							"REQUEST=doQuery&amp;QUERY="
							+urllib.parse.quote("SELECT * FROM gaiadr3.gaia_source"
								" WHERE source_id={}".format(descriptor.metadata[
									"source_id"])),
						description="The associated Gaia DR3 object",
						contentType='appliation/x-votable+xml')
				</code>
			</metaMaker>

			<dataFunction>
				<setup imports="gavo.protocols.sdm"/>
				<code>
					descriptor.data = sdm.makeSDMDataForSSARow(
						descriptor.metadata,
						rd.getById("build_spectrum"))
				</code>
			</dataFunction>

			<FEED source="//soda#sdm_format"/>
		</datalinkCore>
	</service>

	<service id="preview" allowed="qp,static">
		<meta name="title">Gaia DR3 XP spectra preview maker</meta>
		<property name="queryField">source_id</property>
		<property name="staticData">bin</property>

		<pythonCore>
			<inputTable>
				<inputKey original="spectra.source_id"/>
			</inputTable>
			<coreProc>
				<setup imports="gavo.svcs, numpy, gavo.helpers.processing,
					matplotlib, matplotlib.figure, io">
					<code>

					</code>
				</setup>
				<code>
					with base.getTableConn() as conn:
						res = list(conn.query("SELECT flux"
							" FROM \schema.spectra"
							" WHERE source_id=%(source_id)s",
							inputTable.args))

					if not res:
						raise svcs.UnknownURI("No data for this id known here")

					fig = figure.Figure((1, 0.4), 200)
					ax = fig.add_axes([0,0,1,1], frameon=False)
					ax.plot(res[0][0])

					rendered = io.BytesIO()
					fig.savefig(rendered, format="png")
					return ("image/png", rendered.getvalue())
				</code>
			</coreProc>
		</pythonCore>
	</service>

	<service id="web" defaultRenderer="form">
		<meta name="shortName">\schema Web</meta>

		<dbCore queriedTable="ssameta">
			<condDesc buildFrom="ssa_location"/>
		</dbCore>

		<outputTable>
			<outputField name="spectrum"
				select="array[accref, preview]"
				tablehead="Sampled Spectrum"
				description="Merged BP/RP spectrum from Gaia DR3, using a
					local Monte Carlo technique for conversion from the
					upstream continuous form">
				<formatter>
					return T.a(href=data[0])[
						T.img(src=data[1], alt="Sketch of plot", loading="lazy")]
				</formatter>
			</outputField>
			<FEED source="//ssap#atomicCoords"/>
			<LOOP listItems="\copy_from_withpos">
				<events><outputField original="\item"/></events>
			</LOOP>
		</outputTable>
	</service>

	<data id="pub" auto="False">
		<meta>
			date: 2023-11-08
			date.role: ExportRequested
		</meta>
		<publish/>
		<make table="spectra"/>
		<make table="withpos"/>
		<make table="ssameta"/>
	</data>

	<service id="ssa" allowed="form,ssap.xml">
		<meta name="title">Gaia DR3 MC sampled XP spectra SSA</meta>
		<meta name="shortName">\schema SSAP</meta>
		<meta name="ssap.complianceLevel">full</meta>
		<meta name="ssap.testQuery">MAXREC=1</meta>

		<publish render="ssap.xml" sets="ivo_managed"/>
		<publish render="form" sets="ivo_managed,local" service="web"/>

		<ssapCore queriedTable="ssameta">
			<FEED source="//ssap#hcd_condDescs">
				<PRUNE id="coneCond"/>
			</FEED>
			<!-- we make BAND ourselves here so we can use a q3c index -->
			<condDesc id="coneCond" combining="True" joiner="AND">
				<inputKey name="POS" type="text" description="ICRS position of target
					object" unit="deg" std="True" multiplicity="single"
					utype="ssa:Char.SpatialAxis.Coverage.Location.Value"/>
				<inputKey name="SIZE" description="Size of the region of
					interest around POS" std="True" multiplicity="single"
					unit="deg"
					utype="ssa:Char.SpatialAxis.Coverage.Bounds.Extent"/>
				<phraseMaker><code>
					try:
						posKey = inputKeys[0].name
						sizeKey = inputKeys[1].name
					except IndexError:
						raise base.ValidationError("Operator error: the cone condition"
							" is lacking input keys.", "query")
					parsedPos = pql.PQLPositionPar.fromLiteral(
						inPars.get(posKey, None), posKey)
					size = inputKeys[1]._parse(inPars.get(sizeKey, None))
					if parsedPos is not None and size is not None:
						yield parsedPos.getQ3CSQL("ra", "dec", outPars, size)
				</code></phraseMaker>
			</condDesc>
		</ssapCore>
	</service>

	<regSuite>
		<regTest title="gdr3spec.spectra has data.">
			<url parSet="TAP" query="SELECT * FROM gdr3spec.spectra
				WHERE source_id=2062598017447447680">/tap/sync</url>
			<code>
				self.assertHasStrings(
					'value="400.0 410.0 420.0 430.0 440.0 450.0', # the spectral param
				)
				row = self.getFirstVOTableRow()
				self.assertEqual(row["source_id"], 2062598017447447680)
				self.assertEqual(len(row["flux"]), 41)
				self.assertEqual(len(row["flux_error"]), 41)
				self.assertAlmostEqual(row["flux"][0]/1.7412900492695593e-18, 1)
				self.assertAlmostEqual(row["flux_error"][-1]/1.447246961197112e-19, 1)
			</code>
		</regTest>

		<regTest title="gdr3spec SSA serves some data (in time)">
			<url POS="303.274,40.971" SIZE="0.002" REQUEST="queryData"
			>ssa/ssap.xml</url>
			<code>
				res = self.getVOTableRows()
				self.assertEqual(len(res), 1)
				self.assertTrue(res[0]["accref"],
					"/gaia/s3/sdl/dlget?ID=2062598395398892160")
				self.assertTrue(res[0]["preview"],
					"/gaia/s3/preview/qp/2062598395398892160")
				self.assertTrue(res[0]["ssa_dateObs"],
					57382.0)
			</code>
		</regTest>

		<regTest title="gdr3spec preview rendered">
			<url>preview/qp/2062598395398892160</url>
			<code>
				self.assertHasStrings("PNG",
					"nNAf"  # part of the data -- will probably change with mpl versions
				)
			</code>
		</regTest>

		<regTest title="gdr3spec dlmeta returns stuff">
			<url ID="2062598395398892160">sdl/dlmeta</url>
			<code>
				bySemantics = dict((row["semantics"], row)
					for row in self.getVOTableRows())
				self.assertTrue(bySemantics["#this"]["access_url"]
					.endswith("/gaia/s3/sdl/dlget?ID=2062598395398892160"))
				self.assertTrue(bySemantics["#this"]["content_qualifier"],
					"#spectrum")
				self.assertTrue(bySemantics["#preview"]["access_url"]
					.endswith("/gaia/s3/preview/qp/2062598395398892160"))
				self.assertEqual(bySemantics["#coderived"]["access_url"],
					'https://gaia.ari.uni-heidelberg.de/tap/sync?LANG=ADQL&amp;'
					'REQUEST=doQuery&amp;QUERY=SELECT%20%2A%20FROM%20gaiadr3.'
					'gaia_source%20WHERE%20source_id%3D2062598395398892160')
			</code>
		</regTest>

		<regTest title="gdr3spec dlget returns FITSes">
			<url ID="2062598395398892160" FORMAT="application/fits">sdl/dlget</url>
			<code>
				self.assertHasStrings(
					"NAXIS2  =                   41",
					"TUNIT2  = 'W.m**-2.nm**-1'",
					b"\\x40\\x79\\0\\0",  # start of FITS binary data
				)
			</code>
		</regTest>

		<regTest title="gdr3spec dlget SDM VOTable">
			<url ID="2062598395398892160">sdl/dlget</url>
			<code>
				self.assertHasStrings(
					"sampling for Gaia\\nDR3 object 2062598395398892160"
				)
				row = self.getFirstVOTableRow(rejectExtras=False)
				self.assertEqual(row["spectral"], 400.0)
				self.assertAlmostEqual(row["flux"], 1.568678e-18)
				self.assertAlmostEqual(row["flux_error"],  5.658210072955931e-19)
			</code>
		</regTest>
	</regSuite>
</resource>
