<resource schema="robott" resdir=".">
	<meta name="title">Robotic Bochum Twin Telescope RoBoTT</meta>
	<meta name="description">
		The Robotic Bochum Twin Telescope (RoBoTT) operated from December 2008 to
		September 2019 at the Universitaetssternwarte Bochum near Cerro Armazones
		in the Chilean Atacama desert. It consists of two refractor telescopes
		(15 cm aperture, 2°42'' × 2°42'' FoV CCDs) attached to the same mount. The
		fields were observed in Johnson UVBRI, Sloan ugriz, and the
		narrowband OIII, NB, Halpha, and SII filters.

		The first batch contains fields that were observed in Johnson U and
		Sloan z; further RoBoTT fields will be added at a later date.

		Bochum Galactic Disk Survey (BGDS) images, which were also taken with the
		RoBoTT, are available separately from ivo://org.gavo.dc/bgds/q/sia.
	</meta>
	<meta name="creationDate">2025-12-15T12:00:00</meta>
	<meta name="schema-rank">20</meta>

	<meta name="subject">surveys</meta>
	<meta name="subject">galaxy-planes</meta>
	<meta name="subject">milky-way-galaxy</meta>
	<meta name="subject">variable-stars</meta>
	<meta name="subject">broad-band-photometry</meta>

	<meta name="creator">Blex, J.; Ramolla, M.; Westhues, C.; Demleitner, M.;
		Hackstein, M.; Chini, R.</meta>
	<meta name="instrument">Robotic Bochum Twin Telescope (RoBoTT)</meta>
	<meta name="facility">Universitätssternwarte Bochum near Cerro
		Armazones</meta>

	<meta name="source">2026AN....34770087B</meta>
	<meta name="contentLevel">Research</meta>
	<meta name="type">Survey</meta>

	<meta name="copyright" format="rst">
		If you use RoBoTT data, please cite
		:bibcode:`2026AN....34770087B`.
	</meta>

	<meta name="coverage">
		<meta name="waveband">Optical</meta>
	</meta>
	
	<table id="data" onDisk="true">
		<mixin have_bandpass_id="True">//siap2#pgs</mixin>
		<index columns="field"/>
		<index columns="bandpass_id"/>
		<index columns="em_min"/>
		<index columns="em_max"/>
		<index columns="t_min"/>
		<index columns="t_max"/>

		<meta name="_associatedDatalinkService">
			<meta name="serviceId">dl</meta>
			<meta name="idColumn">obs_publisher_did</meta>
		</meta>

		<mixin preview="access_url || '?preview=True'"
    	>//obscore#publishObscoreLike</mixin>
	
		<column name="field" type="text"
			ucd="meta.id;obs.field"
			tablehead="Field"
			description="Survey field observed."
			verbLevel="15"/>
		<column name="airmass"
			ucd="obs.airMass"
			tablehead="Airmass"
			description="Airmass at mean epoch"
			verbLevel="21"/>
		<column name="moondist"
			unit="deg" ucd="obs.param"
			tablehead="Moon dist."
			description="Moon distance at mean epoch"
			verbLevel="25"/>
	</table>

	<data id="import">
		<property key="previewDir">previews</property>

		<sources recurse="True"
			pattern="data/robott/RoBoTT_0.*px_*/*.fits.fz"/>

		<fitsProdGrammar qnd="False">
			<rowfilter procDef="//products#define">
				<bind key="accref">\inputRelativePath{True}</bind>
				<bind key="table">"\schema.data"</bind>
				<bind key="preview_mime">"image/jpeg"</bind>
				<bind key="preview">\splitPreviewPath{.jpeg}</bind>
			</rowfilter>
		</fitsProdGrammar>

		<make table="data">
			<rowmaker>
				<apply procDef="//procs#dictMap">
					<bind name="default">"UNKNOWN"</bind>
					<bind name="mapping">{
						'B': "Johnson B",
						'B_j': "Johnson B",
						'Halpha': "Astrodon Halpha",
						'Ha': "Astrodon Halpha",
						'SII': "Astrodon SII",
						'OII': "Astrodon OIII",
						'OIII': "Astrodon OIII",
						'NB': "Astrodon NB",
						'i': "SDSS i'",
						'i_s': "SDSS i'",
						'g_s': "SDSS g'",
						'r': "SDSS r'",
						'r_s': "SDSS r'",
						'u_s': "SDSS u'",
						'R': "Johnson R",
						'R_j': "Johnson R",
						'U': "Johnson U",
						'U_j': "Johnson U",
						'V_j': "Johnson V",
						'I_j': "Johnson I",
						'V': "Johnson V",
						'z': "SDSS z'",
						'z_s': "SDSS z'",
					}</bind>
					<bind name="key">"FILTER"</bind>
				</apply>

				<apply procDef="//siap2#setMeta">
					<bind name="bandpass_id">@FILTER.strip()</bind>
					<bind name="calib_level">2</bind>
					<bind name="dataproduct_type">"image"</bind>
					<bind name="dateObs">parseISODT(@DATE_OBS)</bind>
					<bind name="obs_collection">"RoBoTT"</bind>
					<bind name="obs_title">"RoBoTT %s %s %s"%(
						@TARGET.strip(), @FILTER.strip(), @DATE_OBS)</bind>
					<bind name="t_exptime">@EXPTIME</bind>
					<bind name="t_xel">None</bind>
				</apply>
				<apply procDef="//siap2#computePGS"/>
				<apply procDef="//siap2#getBandFromFilter"/>

				<map key="field">@TARGET</map>
				<map key="airmass">@AIRMASS</map>
				<map key="moondist">@MOONDIST</map>
			</rowmaker>
		</make>
	</data>

	<service id="dl" allowed="dlget,dlmeta,static">
		<meta name="title">RoBoTT Datalink</meta>
		<meta name="description">This service lets you access cutouts
			from RoBoTT images and retrieve scaled versions.</meta>
		<property name="staticData">data/robott_raw</property>

		<datalinkCore>
			<descriptorGenerator procDef="//soda#fits_genDesc" name="genFITSDesc">
				<bind key="accrefPrefix">'bgds/data/robott'</bind>
				<bind key="qnd">False</bind>
				<code>
					# Check for extra, local and magic pseudo-id coming from
					# the time series (l_robott.rd, not added yet): MAGIC/field/band/mjd
					if pubDID.startswith("MAGIC"):
						# translate to the accref of the closest image
						with base.getTableConn() as conn:
							field, filter, dateObs = pubDID.split("/")[1:]
							if filter.startswith("SDSS ") and not filter.endswith("'"):
								filter = filter+"'"

							dateObs = float(dateObs)
							field = "GDS_"+field

							res = list(conn.query("select access_url from \schema.data"
								" where bandpass_id=%(filter)s"
								"   and field=%(field)s"
								"   and t_min between %(dateObs)s-0.007"
								"     and %(dateObs)s+0.007",
								locals()))
							if len(res)!=1:
								raise ValueError("%s matches for %s"%(
									len(res), pubDID))

						pubDID = getStandardPubDID(res[0][0])

					return getFITSDescriptor(pubDID, accrefPrefix, cls=descClass)
				</code>
			</descriptorGenerator>

			<FEED source="//soda#fits_standardDLFuncs" stcs=" "/>

			<metaMaker semantics="#progenitor">
				<setup imports="os, glob, gavo.utils.pyfits"><code>
					def get_progenitors(prodpath, field):
						"""returns lists of raw, bias, and flat frames for a
						given robott/bgds path.

						Pass in the resdir-relative path of the file itself.

						This was largely contributed by Julia.
						"""
						raw, bias, flat = [], [], []

						date_path = os.path.basename(prodpath).split(".comb")[0]
						parts = prodpath.split("/")
						filter_path, field_path = parts[3], parts[-1]
						tel_map = {
							"VYSOS6_a": "_V6a",
							"VYSOS6_b": "_V6b",
							"VYSOS6" : "",
						}

						with pyfits.open(rd.getAbsPath(prodpath)) as hdul:
							hdr = hdul[1].header
							tel = hdr.get("TELESCOP")
							tel_path = tel_map[tel]

							for i in range(1, 100):
								raw_key = f"IMCMB{i:03d}"
								raw_value = hdr.get(raw_key)
								if raw_value is None:
									break

								count_night = raw_value[11:17]
								pattern = rd.getAbsPath(
									f"data/robott_raw/FLAT/{filter_path}/{field_path}/"
									f"{date_path}_{count_night}_LIGHT*.fit.fz")
								raw.extend(sorted(glob.glob(pattern)))

							pattern = rd.getAbsPath(
								"data/robott_raw/BIAS/"
								f"{date_path}_*_BIAS{tel_path}.fit.fz")
							bias.extend(sorted(glob.glob(pattern)))

							pattern = rd.getAbsPath(
								"data/robott_raw/FLAT"
								+"/"+filter_path+"/*/"
								f"{date_path}_*_FLAT*.fit.fz")
							flat.extend(sorted(glob.glob(pattern)))
								
						return raw, bias, flat
				</code></setup>

				<code>
					raw, bias, flat = get_progenitors(
						"/".join(descriptor.accref.split("/")[1:]),
						descriptor.hdr["TARGET"])
					for path in raw:
						yield descriptor.makeLinkFromFile(path,
							description="A raw frame the co-added image was made from")
					for path in bias:
						yield descriptor.makeLinkFromFile(path,
							description="A bias frame contributing to the master bias"
								" frame applied to #this",
								contentType="application/fits")
					for path in flat:
						yield descriptor.makeLinkFromFile(path,
							description="A flat field contributing to the master flat"
								" applied to #this",
								contentType="application/fits")
				</code>
			</metaMaker>

			<metaMaker semantics="#documentation">
				<code>yield descriptor.makeLinkFromFile(
					"static/RoBoTT_Doc.pdf",
					description="Pipeline documentation",
					service=base.resolveCrossId("bgds/l2#tsdl"))
				</code>
			</metaMaker>
		</datalinkCore>
	</service>


	<service id="web">
		<meta name="shortName">robott web</meta>
		<meta name="_related" title="RoBoTT SIAP service"
			>/bgds/q_robott/sia/info</meta>
		<meta name="_related" title="RoBoTT ObsTAP">/tap</meta>
		<meta name="title">Web access to Robotic Bochum Twin Telescope (RoBoTT)
			images</meta>
		<meta name="_intro" format="rst">You can obtain individual cutouts through
			the datalinks given; for mass operations, prefer the Virtual Observatory
			access modes (SIAP_ or the ivoa.obscore table in `our TAP service`_; the
			RoBoTT collection).

			.. _SIAP: https://dc.g-vo.org/bgds/q_robott/sia/info
			.. _our TAP service: https://dc.g-vo.org/tap
			</meta>

		<dbCore queriedTable="data" id="siacore">
			<condDesc>
				<inputKey original="field" showItems="10">
					<values fromdb="field from \schema.data
						where field!=''
						order by field"/>
				</inputKey>
			</condDesc>
			<condDesc buildFrom="t_min"/>
			<condDesc>
				<inputKey original="bandpass_id">
					<values fromdb="bandpass_id from \schema.data order by bandpass_id"/>
				</inputKey>
			</condDesc>
			<condDesc original="//siap2#humanInput"/>
		</dbCore>
	
		<outputTable>
			<outputField original="access_url" tablehead="Image"
				displayHint="type=product"/>
			<outputField name="dlurl" select="obs_publisher_did"
				tablehead="Datalink Access"
				description="URL of a datalink document for the dataset
					(cutouts, different formats, etc)">
				<formatter>
					yield T.a(href=getDatalinkMetaLink(
						rd.getById("dl"), data)
						)["Datalink"]
				</formatter>
				<property name="targetType"
					>application/x-votable+xml;content=datalink</property>
				<property name="targetTitle">Datalink</property>
			</outputField>
			<outputField original="t_min" displayHint="type=humanDate"/>
			<outputField original="bandpass_id"/>
			<outputField name="plateCenter" type="text"
				select="array[s_ra,s_dec]"
				tablehead="Plate Center"
				description="Plate Canter RA and Dec">
				<formatter>
					return "%.2f,%+.2f"%(data[0], data[1])
				</formatter>
			</outputField>
			<outputField original="obs_title"/>
		</outputTable>
	</service>

	<service id="sia" allowed="siap2.xml">
		<publish render="siap.xml" sets="ivo_managed"/>
		<publish render="form" sets="ivo_managed,local" service="web"/>
		<meta name="shortName">robott sia</meta>
		<meta name="title">Robotic Bochum Twin Telescope (RoBoTT) images</meta>

		<dbCore queriedTable="data">
			<FEED source="//siap2#parameters"/>
		</dbCore>
	
		<meta name="sia.type">Pointed</meta>
		<meta name="testQuery">
			<meta>
				pos.ra:66.59
				pos.dec:8.59
				size.ra: 0.01
				size.dec: 0.01
			</meta>
		</meta>
	</service>

	<regSuite title="RoBoTT functionality">
		<regTest title="RoBoTT web returns plausible info.">
			<url parSet="form"
				t_min="2018-03-23T08:00:00 ..  2018-03-23T09:00:00 "
				>web/form</url>
			<code>
				self.assertHasStrings(
					# datalink link
					"ID=ivo%3A%2F%2Forg.gavo.dc%2F~%3Fbgds%2Fdata%2Frobott%2FRoBoTT_0.80px_1541-0712%2FU_j%2Feq600000ms%2F20180322.comb_avg.0001.fits.fz",
					# filter name properly mapped, title generation
					"RoBoTT RoBoTT_0.80px_1541-0712 Johnson U 2018-03-23T08:31:35",
					# filter selector generated
					">Johnson U&lt;")
			</code>
		</regTest>

		<regTest title="RoBoTT SIAP works, and preview is available">
			<url POS="CIRCLE 235.1 -7.2 0.2"
				BAND="4e-7 5e-7">sia/siap2.xml</url>
			<code>
				row = self.getFirstVOTableRow()
				self.assertTrue(row["t_exptime"] is not None, "Empty exposure time?")
				previewData = utils.urlopenRemote(row["access_url"]
					+"?preview=True").read()
				self.assertTrue(b"JFIF" in previewData, "No preview?")
			</code>
		</regTest>
		
		<regTest title="robott datalink" tags="bigserver">
			<url ID="ivo://org.gavo.dc/~?bgds/data/robott/RoBoTT_0.75px_0006+2012/B_j/eq030000ms/20171201.comb_avg.0001.fits.fz"
				>dl/dlmeta</url>
			<code>
				links = self.datalinkBySemantics()
				self.assertEqual(
					set(links.keys()),
					{'#preview', '#this', '#progenitor', '#documentation', '#proc'})
				self.assertEqual(
					links['#documentation'][0]["content_type"],
					 'application/pdf')
				progs = set(r["access_url"].split("/")[-1]
					for r in links["#progenitor"])
				self.assertTrue('20171201_000050_LIGHT_V6b.fit.fz' in progs)
				self.assertTrue('20171201_000009_BIAS_V6b.fit.fz' in progs)
			</code>
		</regTest>
	</regSuite>
</resource>

