diff --git a/.agents/skills/celestial-skill/references/implementation-map.md b/.agents/skills/celestial-skill/references/implementation-map.md index d54fd8bf391..0810a1504ca 100644 --- a/.agents/skills/celestial-skill/references/implementation-map.md +++ b/.agents/skills/celestial-skill/references/implementation-map.md @@ -228,7 +228,7 @@ supports a requested handoff, not an automatic stop for authorized implementatio | A hot giant nobody has measured | `new-object --expected-glow ...` paints the same color at the equilibrium temperature a paper prints, as the "Expected glow" dataset | HAT-P-14 b `thermal` (1,624 K, Southworth 2012) | `packages/telescope-cli/src/new-object/planet-glow.test.mts` | An estimate, said as one everywhere; only giants of 0.77 Jupiter radii or more, the population its test covers ([estimates](scientific-faithfulness.md#estimates)) | | A measured day side that cannot be a glow (under 1,000 K, or an eccentric orbit) | `new-object --thermal` and `--thermal-entries` write the `measured-dayside` dataset (`packages/telescope-cli/src/new-object/thermal/dayside-dataset.mts`): the day hemisphere in false color at the printed temperature on the scale all such planets share, the night hemisphere blank | WASP-80 b `dayside` (888 K at 4.5 µm) | `packages/telescope-cli/src/new-object/planet-glow.test.mts` | No glow, no pattern; "at secondary eclipse" for an eccentric orbit, nothing past e 0.6 | | A small hot planet of a red dwarf nobody has measured | `new-object --expected-glow` paints the bare-rock maximum from the star's temperature and a/R*, for planets like the nine rocks of Coy et al. 2025 | GJ 806 b `thermal` (1,204 K) | `packages/telescope-cli/src/new-object/planet-glow.test.mts` | An estimate; the archive's insolation is the check; rocks of hotter stars and sub-Neptunes are not covered | -| A published simulation of a named body | `telescope simulations OBJECT` lists the Zenodo records that name it (`packages/telescope-cli/src/simulations/simulations.mts`). `new-object --simulation entries.json` (`packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts`) adds one from an entry written after reading the paper, as the default dataset where the body had only one color or the neutral shape: it checks the record's license, name and file, fetches by range the two parts of the file it reads (the header with the coordinates, and the selected grid) and writes a `terrestrial-scientific` dataset with `format: netcdf-lonlat-field` (`packages/bake/src/objects/raster/netcdf/`), labelled as a model with its scenario. Conditions: [published simulations](scientific-faithfulness.md#published-simulations) | `trappist-1e` (ExoCAM surface temperature), `trappist-1d` (Generic PCM outgoing thermal radiation, from a 3.5 GB file); WASP-103b's hand-made table is the earlier example | `packages/bake/src/objects/raster/netcdf/*.test.mts`, `packages/telescope-cli/src/simulations/simulations.test.mts`, `packages/telescope-cli/src/new-object/simulation/simulation-dataset.test.mts` | Classic NetCDF only: a NetCDF-4 release is refused by name. A file inside a zip is not read. The variable's longitude and latitude must be its last two dimensions and its coordinates must not be record variables | +| A published simulation of a named body | `telescope simulations OBJECT` lists the Zenodo records that name it (`packages/telescope-cli/src/simulations/simulations.mts`). `new-object --simulation entries.json` (`packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts`) adds one from an entry written after reading the paper, as the default dataset where the body had only one color or the neutral shape: it checks the record's license, name and file, fetches by range the two parts of the file it reads (the header with the coordinates, and the selected grid) and writes a `terrestrial-scientific` dataset with `format: netcdf-lonlat-field` (`packages/bake/src/objects/raster/netcdf/`), labelled as a model with its scenario. An entry with `member` names a NetCDF-4 model file inside a ZIP release, kept whole and restored by a `zip-member` step, and may add `isobar` to read the field at one pressure. Conditions: [published simulations](scientific-faithfulness.md#published-simulations) | `trappist-1e` (ExoCAM surface temperature), `trappist-1d` (Generic PCM outgoing thermal radiation, from a 3.5 GB file), `wasp-94-a-b` (Met Office UM temperature at 1 millibar, from a NetCDF-4 file in a ZIP); WASP-103b's hand-made table is the earlier example | `packages/bake/src/objects/raster/netcdf/*.test.mts`, `packages/telescope-cli/src/simulations/simulations.test.mts`, `packages/telescope-cli/src/new-object/simulation/simulation-dataset.test.mts` | NetCDF-4 is read only in its plain kind: a chunked or compressed variable, or one inside a group, is refused by name, and the file is kept whole. In a classic release the variable's longitude and latitude must be its last two dimensions and its coordinates must not be record variables | | A planet of another star with a published longitude-latitude map | Astronomy record `classification: exoplanet` with a `hostedOrbit` block around a placed star (`packages/astronomy/src/hostedOrbits.ts`), rotation `cssearth-synchronous-rotation@1` (`packages/bake/src/objects/scene/authored-rotation.ts`), and a `terrestrial-scientific` dataset with `format: npy-dictionary-map` on the lit route, as every planet with a map is drawn (`packages/bake/src/objects/raster/numpy/npy-pickle.ts`, `tar-member.mts`; `new-object --star-lit ...` moves a planet built self-luminous to the lit route, `packages/telescope-cli/src/new-object/map/star-lit.mts`); paint the atlas from `outputLongitudeOrigin: -90` | WASP-43b `temperature`: Challener et al. (2024) JWST NIRSpec eclipse map, Zenodo tar read in place | `packages/bake/src/objects/raster/eclipse-map/phase-curve.ts` turns the map with the package's orbit and rotation; `default-view.test.mts` pins the substellar view | Photometry cannot tell north from south; the orbit's node on the sky is a display convention | | A wide companion star whose orbit is not measured | Its own Gaia placement with `boundTo` and a `sources.binary` citation of the measurement that binds it (`packages/astronomy/cli/lib/generator-records.mts`); no orbit is drawn, and preparation carries the pair's centre of mass into the world context. A LOFTI fit (`packages/bake/authoring/hd-189733-companion/lofti-fit.py`) may be kept as evidence only: the candidate-orbit renderer was removed because the accepted orbits span a factor of ten in size | HD 189733 B | No dedicated test | The companion lies on no orbit | | A directly imaged planet whose paper publishes its orbit's inputs but not one orbit | `packages/telescope-cli/src/new-object/hosted-orbits/orbitize-fit.py` reruns orbitize! (the papers' own tool) on the paper's measurements, priors and sampler (a `fit.json` beside the measurements, corrections such as astrometric jitter or the shift to a pair's centre of mass declared there); the maximum of orbitize!'s posterior, refined with Nelder-Mead from many starts (`--refine`), becomes the `hostedOrbit`, kept at its fitted angular size and placed at the Gaia distance. A planet around a pair is fitted about the pair's centre of mass (`centreOfMass` in `fit.json`) and drawn around the primary. The drawn orbit uses the gravitational parameter the recorded period and size imply (`prepare-solar-geometry.mts`), so a pair-mass fit draws correctly around one star. Short-arc orbits are drawn as one of the orbits the measurements allow; the README gives the published range | GQ Lup b (Venkatesan et al. 2025); VHS 1256-1257 b, DH Tau b, ROXs 42B b | `packages/astronomy/src/hostedOrbits.test.ts` puts each planet on the paper's measured positions | The medians printed by the run are compared with the paper's table before the sample is used | diff --git a/.gitignore b/.gitignore index cac80081b34..b486ade986a 100644 --- a/.gitignore +++ b/.gitignore @@ -402,6 +402,7 @@ src/objects/hd-181327/source/photometry/tess/ src/objects/hd-189733/source/photometry/tess/ src/objects/hd-189733b/source/science/lally-2025/output_E.npy src/objects/wasp-17b/source/science/valentine-2024/tmap.pkl +src/objects/wasp-94-a-b/source/science/mukherjee-2025/WASP-94Ab_GCM.nc src/objects/hd-189733b/source/illustration/HD_189733_b.jpg src/objects/hd-29615/source/science/willamo-2022/*.dat src/objects/hd-35296/source/science/willamo-2022/*.dat diff --git a/docs/surface-preparation.md b/docs/surface-preparation.md index e1d44fcc664..fb006631b13 100644 --- a/docs/surface-preparation.md +++ b/docs/surface-preparation.md @@ -37,7 +37,7 @@ prepared background size and position; the runtime needs no special path. | Prepare solid-body imagery, scientific layers and meshes | [prepareTerrestrialLayers](../packages/bake/src/objects/layers/terrestrial/terrestrial-layers.ts) | | Compare retrieved atmospheric profiles with credible intervals | [Retrieved profile chart recipe](retrieved-profile-charts.md) | | Sample a pressure level from a numeric longitude/latitude table | [CSV slice reader](../packages/bake/src/objects/raster/lonlat-slice-table.ts): `lonlat-slice-table`, one-based `columns`, an exact `slice`, and a coordinate rounding tolerance. It validates periodic longitude and complete cells; latitude coverage ends at the released samples. [WASP-103 b](../src/objects/wasp-103b/README.md) is the climate-model example. | -| Read one field of a released model run from NetCDF | [NetCDF field reader](../packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.ts): `netcdf-lonlat-field`, a `variable`, its two `coordinates`, a `select` index for every other dimension, the `sourceUnits` the file must state and `longitudeZeroAt`, the grid longitude of the body's zero meridian. It reads classic NetCDF (CDF-1, CDF-2, CDF-5) from the file's own byte ranges and refuses NetCDF-4 by name. A release of gigabytes is kept as two exact byte ranges the manifest declares: `path` is its first bytes (the header and coordinates) and `field` the bytes of the selected grid, which the header must place at the declared range. It validates periodic longitude, reading once a first meridian the grid stores at both ends; latitude coverage ends at the released rows and cells at the fill value stay without a value. `new-object --simulation` writes the recipe from an entry ([simulation-dataset.mts](../packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts)). | +| Read one field of a released model run from NetCDF | [NetCDF field reader](../packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.ts): `netcdf-lonlat-field`, a `variable`, its two `coordinates`, a `select` index for every other dimension, the `sourceUnits` the file must state and `longitudeZeroAt`, the grid longitude of the body's zero meridian. It reads classic NetCDF (CDF-1, CDF-2, CDF-5) and the plain kind of NetCDF-4 (every variable in the root group, stored whole and unfiltered; a chunked or compressed variable is refused by name), each from the file's own byte ranges. A classic release of gigabytes is kept as two exact byte ranges the manifest declares: `path` is its first bytes (the header and coordinates) and `field` the bytes of the selected grid, which the header must place at the declared range. A NetCDF-4 file spreads its structure through itself and is kept whole. `isobar` reads the field at one pressure instead of on one level, between the two levels either side of it in each column ([netcdf-isobar.ts](../packages/bake/src/objects/raster/netcdf/netcdf-isobar.ts)), for a model whose levels are heights. It validates periodic longitude, reading once a first meridian the grid stores at both ends; latitude coverage ends at the released rows and cells at the fill value stay without a value. `new-object --simulation` writes the recipe from an entry ([simulation-dataset.mts](../packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts)). | | Add or restyle scientific charts | [Chart recipes catalog](chart-recipes.md) | | Record which inputs each dataset reads | [Lineage bindings](../packages/bake/src/objects/lineage/lineage-recipes.ts) and [the body reader](../packages/bake/src/objects/lineage/body-lineage.ts) | diff --git a/packages/astronomy/data/bodies/cd-34-14724-b.json b/packages/astronomy/data/bodies/cd-34-14724-b.json new file mode 100644 index 00000000000..764e6c9c8ef --- /dev/null +++ b/packages/astronomy/data/bodies/cd-34-14724-b.json @@ -0,0 +1,32 @@ +{ + "id": "cd-34-14724-b", + "classification": "star", + "order": 5780, + "physical": { + "name": "CD-34 14724B", + "horizonsCode": null, + "meanRadiusKm": 995546.7, + "gravitationalParameterKm3PerS2": 149965057247.39148, + "parent": null, + "effectiveTemperatureK": 6049.7 + }, + "physicalNotes": "Radius 1.431 +/- 0.068 solar radii from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the radius of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467): 995,547 km at 695,700 km per solar radius. Mass 1.13 +/- 0.149 solar masses from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the mass of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467); GM is that mass times the JPL solar GM. Temperature 6049.7 +/- 126.5 K from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467). presentationUp: the display axis is a sky-plane convention; the spin axis's position angle on the sky is not measured.", + "star": { + "rightAscensionDegrees": 313.7882976030805, + "declinationDegrees": -34.135730289765554, + "positionEpochJulianYear": 2016, + "distanceParsecs": 211.82964258100284, + "properMotionRaMasPerYear": 26.19129770882488, + "properMotionDecMasPerYear": -44.70168046944481, + "radialVelocityKmPerS": -8.3041315, + "presentationUp": "display-axis", + "boundTo": "wasp-94-a", + "sources": { + "position": "Gaia DR3 source 6780546169633474944 (Gaia Collaboration 2023, A&A 674, A1), ICRS at epoch J2016.0, from the archived row src/objects/cd-34-14724-b/source/photometry/gaia-dr3-source.csv", + "distance": "Gaia DR3 parallax 4.7208 +/- 0.0165 mas (same row, RUWE 0.81): 211.83 pc, no zero-point correction", + "properMotion": "Gaia DR3 (same row): 26.191, -44.702 mas/yr", + "radialVelocity": "Gaia DR3 (same row): -8.30 +/- 0.20 km/s", + "binary": "El-Badry, Rix & Heintz (2021, MNRAS 506, 2269) list WASP-94 A and CD-34 14724B in their Gaia EDR3 wide-binary catalogue (VizieR J/MNRAS/506/2269, source_id 6780546169634170496 and 6780546169633474944), 3,171 AU apart on the sky, with a chance-alignment probability of 8.4e-6: the pair is bound. No orbit of the pair is published, and the Gaia astrometry alone does not constrain one, so the star is placed by its own astrometry and carries no orbit." + } + } +} diff --git a/packages/astronomy/data/bodies/wasp-94-a-b.json b/packages/astronomy/data/bodies/wasp-94-a-b.json new file mode 100644 index 00000000000..969fc8406a3 --- /dev/null +++ b/packages/astronomy/data/bodies/wasp-94-a-b.json @@ -0,0 +1,28 @@ +{ + "id": "wasp-94-a-b", + "classification": "exoplanet", + "order": 5779, + "physical": { + "name": "WASP-94 A b", + "horizonsCode": null, + "meanRadiusKm": 112957.4, + "gravitationalParameterKm3PerS2": 63343265.95, + "parent": "wasp-94-a" + }, + "physicalNotes": "Radius 1.58 Jupiter radii from Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract): 112,957.4 km at 71,492 km per Jupiter radius. GM from the mass 0.5 Jupiter masses (Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive, https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract) times JPL's Jupiter GM. A sphere: no oblateness is measured.", + "hostedOrbit": { + "periodDays": 3.9502001, + "semiMajorAxisStellarRadii": 8.44, + "inclinationDegrees": 88.7, + "eccentricity": 0, + "transitTimeBmjdTdb": 58300.14763, + "ascendingNodePositionAngleDegrees": 0, + "sources": { + "period": "Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): P 3.9502001 d", + "shape": "Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): a/R* 8.44; Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): inclination 88.7 degrees", + "eccentricity": "Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): e 0", + "phase": "Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): transit mid-time 2458300.64763 BJD, taken as BJD_TDB; with its period, the row that predicts 2026-01-01 best (1 sigma 1 min)", + "orientation": "Display convention: transit photometry does not measure the orbit's position angle on the sky, so the ascending node is set at position angle 0 (celestial north)." + } + } +} diff --git a/packages/astronomy/data/bodies/wasp-94-a.json b/packages/astronomy/data/bodies/wasp-94-a.json new file mode 100644 index 00000000000..8d073d683bb --- /dev/null +++ b/packages/astronomy/data/bodies/wasp-94-a.json @@ -0,0 +1,30 @@ +{ + "id": "wasp-94-a", + "classification": "star", + "order": 5778, + "physical": { + "name": "WASP-94 A", + "horizonsCode": null, + "meanRadiusKm": 1029636, + "gravitationalParameterKm3PerS2": 221629774870.03876, + "parent": null, + "effectiveTemperatureK": 6170 + }, + "physicalNotes": "Radius 1.48 +/- 0.12 solar radii from Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract): 1,029,636 km at 695,700 km per solar radius. Mass 1.67 +/- 0.64 solar masses from Stassun et al. 2017, the stellar mass of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract); GM is that mass times the JPL solar GM. Temperature 6170 +/- 80 K from Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract). presentationUp: the display axis is a sky-plane convention; the spin axis's position angle on the sky is not measured.", + "star": { + "rightAscensionDegrees": 313.7832437626676, + "declinationDegrees": -34.135757506210496, + "positionEpochJulianYear": 2016, + "distanceParsecs": 210.53462213743273, + "properMotionRaMasPerYear": 26.500228153109326, + "properMotionDecMasPerYear": -44.970822510565206, + "radialVelocityKmPerS": -8.361371, + "presentationUp": "display-axis", + "sources": { + "position": "Gaia DR3 source 6780546169634170496 (Gaia Collaboration 2023, A&A 674, A1), ICRS at epoch J2016.0, from the archived row src/objects/wasp-94-a/source/photometry/gaia-dr3-source.csv", + "distance": "Gaia DR3 parallax 4.7498 +/- 0.0242 mas (same row, RUWE 0.98): 210.53 pc, no zero-point correction", + "properMotion": "Gaia DR3 (same row): 26.500, -44.971 mas/yr", + "radialVelocity": "Gaia DR3 (same row): -8.36 +/- 0.19 km/s" + } + } +} diff --git a/packages/bake/AGENTS.md b/packages/bake/AGENTS.md index 1b9a7620444..df1b4692f40 100644 --- a/packages/bake/AGENTS.md +++ b/packages/bake/AGENTS.md @@ -164,7 +164,7 @@ cycle. Share anything else through `@cssearth/core` or another package. Prepared sphere is written with. The solar geometry is generated into the checkout (`src/platform/solar-geometry.mts`) after the packages build, so callers pass `SolarGeometry` or use `loadSolarGeometry` from `@cssearth/bake/objects/scene` to load and validate the generated file. The prepared sky and Sun contracts belong to `@cssearth/objects`; their authored standards and preparers belong to `presentation`, which the scene imports as a lower topic. - - `objects/raster`: scientific surfaces from PDS, ISIS, FITS, GeoTIFF, VTK, NumPy, HEALPix, Tecplot and classic NetCDF products, + - `objects/raster`: scientific surfaces from PDS, ISIS, FITS, GeoTIFF, VTK, NumPy, HEALPix, Tecplot, classic NetCDF and NetCDF-4 products, categorical geology and symbols, exoplanet eclipse and published phase-curve maps, observed color rasters and their photometric composition, a planet's whole-disc color record and the band-ratio tie to it, the source records they read, the WISE atlas mosaic grid, FITS binary tables (OIFITS and archive tables) and a star's limb darkening fitted to TESS transits. It imports `objects/scene`, diff --git a/packages/bake/src/objects/acquisition/operations-acquisition.ts b/packages/bake/src/objects/acquisition/operations-acquisition.ts index 31bf7d898a1..c4f2e0bd2e6 100644 --- a/packages/bake/src/objects/acquisition/operations-acquisition.ts +++ b/packages/bake/src/objects/acquisition/operations-acquisition.ts @@ -5,7 +5,7 @@ import { createWriteStream } from 'node:fs'; import { basename, dirname, resolve } from 'node:path'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; -import { execFile } from 'node:child_process'; +import { execFile, spawn } from 'node:child_process'; import { promisify } from 'node:util'; import { gzipSync } from 'node:zlib'; import { containedPath, declaredDownloadBytes, publishPinnedSource, publishPinnedSourceStream, sourceCacheUrl, withIdleTimeout } from '../sources/index.ts'; @@ -88,8 +88,16 @@ export async function executeAcquisition({sourceRoot,manifest,plan,group='refres const temporary=`${archivePath}.partial-${process.pid}`; try{await pipeline(Readable.fromWeb(response.body as never),createWriteStream(temporary));await rename(temporary,archivePath);}finally{await rm(temporary,{force:true});} } - const {stdout}=await promisify(execFile)('unzip',['-p',archivePath,step.member],{encoding:'buffer',maxBuffer:512*1024*1024}); - await publish(step.path,stdout); + const entry=[...manifest.inputs,...manifest.generatedIntermediates,...manifest.documents].find(entry=>entry.path===step.path);if(!entry)throw new Error(`Undeclared acquisition target: ${step.path}.`); + // unzip writes the member straight into the pinned file, so one of hundreds of megabytes is never held in memory. A + // member unzip could not read whole fails the stream before the file takes its place. + const child=spawn('unzip',['-p',archivePath,step.member],{stdio:['ignore','pipe','pipe']}); + let complaint='';child.stderr.on('data',(chunk:Buffer)=>{complaint+=chunk.toString();}); + const exit=new Promise((done,fail)=>{child.on('close',done);child.on('error',fail);}); + await publishPinnedSourceStream({sourceRoot,entry,stream:Readable.from((async function*(){ + for await(const chunk of child.stdout)yield chunk as Buffer; + if(await exit!==0)throw new Error(`unzip could not read ${step.member} from ${step.url}: ${complaint.trim().split('\n')[0]??'no message'}.`); + })())}); } else if(step.kind==='tar-gz-member'){ const cache=resolve('.local/source-archives');await mkdir(cache,{recursive:true}); diff --git a/packages/bake/src/objects/raster/index.ts b/packages/bake/src/objects/raster/index.ts index 4538d2ac847..5ac6f2fef0f 100644 --- a/packages/bake/src/objects/raster/index.ts +++ b/packages/bake/src/objects/raster/index.ts @@ -20,6 +20,8 @@ export * from './latitude-belt-map.ts'; export * from './lonlat/lonlat-grid.ts'; export * from './lonlat-slice-table.ts'; export * from './netcdf/classic-netcdf.ts'; +export * from './netcdf/hdf5-netcdf.ts'; +export * from './netcdf/netcdf-isobar.ts'; export * from './netcdf/netcdf-lonlat-field.ts'; export * from './numpy/npy-dictionary-map.ts'; export { readNpy, type SpinFrameTransfer, spinFrameTransfer, loadSpinFrameTransfer, decodeNpyLonLatGrid, loadNpyLonLatGrid, npyLonLatGridDependencies } from './numpy/npy-lonlat-grid.ts'; diff --git a/packages/bake/src/objects/raster/netcdf/fixtures/README.md b/packages/bake/src/objects/raster/netcdf/fixtures/README.md index b3081f1438a..0debe0d5882 100644 --- a/packages/bake/src/objects/raster/netcdf/fixtures/README.md +++ b/packages/bake/src/objects/raster/netcdf/fixtures/README.md @@ -1,6 +1,6 @@ -# Classic NetCDF fixtures +# NetCDF fixtures -Three small files for the reader tests beside this directory. They were written by the reference library, libnetcdf 4.9.3 +Three small classic files and two NetCDF-4 files for the reader tests beside this directory. The classic three first. They were written by the reference library, libnetcdf 4.9.3 (through netCDF4-python 1.7.4), from the definitions below, so the tests compare the TypeScript reader with that library and not with itself. `ncgen` writes the same content from each definition: `ncgen -k nc6 -o field-cdf2.nc field.cdl`, with `-k nc3` for the CDF-1 file and `-k cdf5` for the CDF-5 file. @@ -74,3 +74,55 @@ variables: :count = 7LL ; } ``` + +## `field-nc4.nc` (20,950 bytes, NetCDF-4) + +A model file in miniature in the format newer models write, an HDF5 container. Written by the same reference library, +libnetcdf 4.9.3 over HDF5 1.14.6 (through netCDF4-python 1.7.4); `ncgen -k nc4 -o field-nc4.nc field-nc4.cdl` writes the +same content. Its twelve variables are more than a group keeps in its header, so their links are in a heap. + +``` +netcdf field-nc4 { +dimensions: + time = 2 ; lev = 3 ; lat = 4 ; lon = 8 ; chars = 4 ; +variables: + float lat(lat) ; lat:units = "degrees_north" ; // -67.5, -22.5, 22.5, 67.5 + float lon(lon) ; lon:units = "degrees_east" ; // 22.5 to 337.5 by 45 + double lev(lev) ; lev:units = "m" ; // 1000, 2000, 3000 + double time(time) ; time:units = "hours since 1970-01-01 00:00:00" ; // 10.5, 11.5 + double T(time, lev, lat, lon) ; T:units = "K" ; T:standard_name = "air_temperature" ; + string T:note = "a variable-length string" ; + double P(time, lev, lat, lon) ; P:units = "Pa" ; + float TS(time, lat, lon) ; TS:units = "K" ; + short PACKED(lat, lon) ; PACKED:_FillValue = -32767s ; PACKED:units = "K" ; + PACKED:scale_factor = 0.5 ; PACKED:add_offset = 200. ; + float SQUEEZED(lat, lon) ; SQUEEZED:units = "K" ; SQUEEZED:_ChunkSizes = 2, 4 ; SQUEEZED:_DeflateLevel = 4 ; + uint COUNTS(lon) ; // 0, 1, 2, 3, 3000000000, 4294967294, 6, 7 + double UNWRITTEN(lat) ; UNWRITTEN:units = "K" ; // declared, never written + char label(chars) ; // "abcd" +// global attributes: + :title = "cssEarth NetCDF-4 reader fixture" ; +} +``` + +With `r` the time, `k` the level, `i` the latitude index and `j` the longitude index, all counted from 0: + +- `T[r, k, i, j] = 1000 - 100 k + 10 i + j + 500 r` +- `P[r, k, i, j] = 1000 * 10^-k * (1 + j / 8)`: pressure falls tenfold from one level to the next, and differs from column + to column, so one pressure lies between different levels in different columns +- `TS[r, i, j] = 200 + 100 r + 10 i + j` +- `PACKED[i, j]` stores `10 i + j`, and the fill value at `i = 1, j = 2` +- `SQUEEZED[i, j] = 10 i + j`, chunked and compressed: the reader refuses it by name + +## `field-nc4-superblock0.nc` (14,320 bytes, NetCDF-4 in HDF5's oldest file layout) + +libnetcdf wrote HDF5's first superblock (version 0) until its release 4.6 and writes version 2 since, and a model release +this reader was written for is of the older kind. The current library cannot write it, so this file was written by the HDF5 +reference library itself, 1.14.6 through h5py 3.16, in its oldest layout (`libver="earliest"`) with creation order tracked +in the file and every dataset, as libnetcdf tracks it, and with dimension scales attached as libnetcdf attaches them. + +- `lat(4)`: -67.5, -22.5, 22.5, 67.5 in `degrees_north`; `lon(8)`: 0 to 315 by 45 in `degrees_east`; `time(2)`: 10.5, 11.5. + Each is a dimension scale. +- `TS(time, lat, lon)`, double, in `K`: `TS[r, i, j] = 200 + 100 r + 10 i + j`. It carries `units`, `note1` to `note8` + ("attribute 1" to "attribute 8") and its dimension list: ten attributes, more than a header keeps, so they are in a heap. +- `V1` to `V6`, int: `Vn` holds `n, 10 n, 100 n`. With them the group has ten members, whose links are in a heap too. diff --git a/packages/bake/src/objects/raster/netcdf/fixtures/field-nc4-superblock0.nc b/packages/bake/src/objects/raster/netcdf/fixtures/field-nc4-superblock0.nc new file mode 100644 index 00000000000..b0e53192238 Binary files /dev/null and b/packages/bake/src/objects/raster/netcdf/fixtures/field-nc4-superblock0.nc differ diff --git a/packages/bake/src/objects/raster/netcdf/fixtures/field-nc4.nc b/packages/bake/src/objects/raster/netcdf/fixtures/field-nc4.nc new file mode 100644 index 00000000000..c5a44f48bfc Binary files /dev/null and b/packages/bake/src/objects/raster/netcdf/fixtures/field-nc4.nc differ diff --git a/packages/bake/src/objects/raster/netcdf/hdf5-netcdf.test.mts b/packages/bake/src/objects/raster/netcdf/hdf5-netcdf.test.mts new file mode 100644 index 00000000000..1692cb8b127 --- /dev/null +++ b/packages/bake/src/objects/raster/netcdf/hdf5-netcdf.test.mts @@ -0,0 +1,81 @@ +/** The NetCDF-4 reader against two files the reference libraries wrote (fixtures/README.md): every value below is the one + * the fixture's definition states, so the reader is compared with libnetcdf and libhdf5, not with itself. */ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import test from 'node:test'; +import { openHdf5Netcdf } from '@cssearth/bake/objects/raster'; + +const fixture = (name: string) => resolve(import.meta.dirname, 'fixtures', name); +/** The default fill value libnetcdf gives a float or double variable nobody set one for. */ +const DEFAULT_FILL = 9.969209968386869e+36; + +test('a file libnetcdf wrote gives its variables, their dimensions and attributes, and every value', async () => { + const file = await openHdf5Netcdf(fixture('field-nc4.nc')); + try { + // `chars` is a dimension no variable was declared for: the library stores it as a dataset, and it is not a variable. + assert.deepEqual([...file.variables.keys()].sort(), ['COUNTS', 'P', 'PACKED', 'SQUEEZED', 'T', 'TS', 'UNWRITTEN', 'label', 'lat', 'lev', 'lon', 'time']); + const air = file.variables.get('T')!; + assert.deepEqual([air.dimensions, air.shape, air.type], [['time', 'lev', 'lat', 'lon'], [2, 3, 4, 8], 'double']); + // The library's own bookkeeping beside the attributes is left out; a variable-length string is read from its heap. + assert.deepEqual(air.attributes, { units: 'K', standard_name: 'air_temperature', note: 'a variable-length string', _FillValue: [DEFAULT_FILL] }); + assert.deepEqual([file.variables.get('TS')!.dimensions, file.variables.get('TS')!.type], [['time', 'lat', 'lon'], 'float']); + assert.deepEqual(file.variables.get('label')!.dimensions, ['chars']); + assert.deepEqual([...await file.values('lat')], [-67.5, -22.5, 22.5, 67.5]); + assert.deepEqual([...await file.values('lon')], [22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5]); + assert.deepEqual([...await file.values('lev')], [1000, 2000, 3000]); + assert.deepEqual([...await file.values('time')], [10.5, 11.5]); + const t = await file.values('T'), p = await file.values('P'), ts = await file.values('TS'); + for (let r = 0; r < 2; r++) for (let i = 0; i < 4; i++) for (let j = 0; j < 8; j++) { + assert.equal(ts[r * 32 + i * 8 + j], 200 + 100 * r + 10 * i + j, `TS[${r},${i},${j}]`); + for (let k = 0; k < 3; k++) { + assert.equal(t[r * 96 + k * 32 + i * 8 + j], 1000 - 100 * k + 10 * i + j + 500 * r, `T[${r},${k},${i},${j}]`); + assert.equal(p[r * 96 + k * 32 + i * 8 + j], 1000 * 10 ** -k * (1 + j / 8), `P[${r},${k},${i},${j}]`); + } + } + // A run of values is read from its own bytes: T at the second time, third level, last latitude. + assert.deepEqual([...await file.slice('T', 96 + 64 + 24, 8)], [1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337]); + // Stored values come back as stored: the scale, offset and fill value are attributes for the caller. + const packed = file.variables.get('PACKED')!; + assert.deepEqual([packed.type, packed.attributes.scale_factor, packed.attributes.add_offset, packed.attributes._FillValue], ['short', [0.5], [200], [-32767]]); + assert.deepEqual([...(await file.values('PACKED')).subarray(8, 12)], [10, 11, -32767, 13]); + assert.deepEqual([file.variables.get('COUNTS')!.type, [...await file.values('COUNTS')]], ['uint', [0, 1, 2, 3, 3000000000, 4294967294, 6, 7]]); + } finally { await file.close(); } +}); + +test('what the reader does not read is refused by name', async () => { + const file = await openHdf5Netcdf(fixture('field-nc4.nc')); + try { + await assert.rejects(file.values('SQUEEZED'), /variable SQUEEZED is stored chunked; only a variable stored whole and unfiltered is read/u); + await assert.rejects(file.values('UNWRITTEN'), /variable UNWRITTEN was never written/u); + await assert.rejects(file.values('label'), /variable label is text, not numbers/u); + await assert.rejects(file.values('TZ'), /has no variable TZ; it has /u); + await assert.rejects(file.slice('lat', 2, 3), /variable lat has 4 values, and 3 from value 2 were asked for/u); + } finally { await file.close(); } + await assert.rejects(openHdf5Netcdf(fixture('field-cdf2.nc')), /field-cdf2\.nc is not an HDF5 file, and so not NetCDF-4/u); + // A download that stopped early is shorter than the length the file states for itself. + const directory = await mkdtemp(join(tmpdir(), 'cssearth-netcdf4-')); + try { + await writeFile(join(directory, 'short.nc'), (await readFile(fixture('field-nc4.nc'))).subarray(0, 12000)); + await assert.rejects(openHdf5Netcdf(join(directory, 'short.nc')), /short\.nc is cut short: it holds 12000 of the 20950 bytes it states for itself/u); + } finally { await rm(directory, { recursive: true }); } +}); + +test('the oldest file layout, with links and attributes kept in heaps, reads the same way', async () => { + // Superblock version 0, as libnetcdf wrote until 4.6 and as the Met Office model release this reader was written for is. + assert.equal((await readFile(fixture('field-nc4-superblock0.nc')))[8], 0); + const file = await openHdf5Netcdf(fixture('field-nc4-superblock0.nc')); + try { + // Ten members: more than a group's header holds, so their links are in a heap behind a B-tree. + assert.deepEqual([...file.variables.keys()].sort(), ['TS', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'lat', 'lon', 'time']); + const surface = file.variables.get('TS')!; + // TS carries ten attributes, more than its header holds: its dimension list and the nine below are in a heap too. + assert.deepEqual([surface.dimensions, surface.shape, surface.type], [['time', 'lat', 'lon'], [2, 4, 8], 'double']); + assert.deepEqual(surface.attributes, { units: 'K', ...Object.fromEntries(Array.from({ length: 8 }, (_, n) => [`note${n + 1}`, `attribute ${n + 1}`])) }); + assert.deepEqual([...await file.values('lon')], [0, 45, 90, 135, 180, 225, 270, 315]); + const ts = await file.values('TS'); + for (let r = 0; r < 2; r++) for (let i = 0; i < 4; i++) for (let j = 0; j < 8; j++) assert.equal(ts[r * 32 + i * 8 + j], 200 + 100 * r + 10 * i + j, `TS[${r},${i},${j}]`); + for (let n = 1; n <= 6; n++) assert.deepEqual([...await file.values(`V${n}`)], [n, 10 * n, 100 * n]); + } finally { await file.close(); } +}); diff --git a/packages/bake/src/objects/raster/netcdf/hdf5-netcdf.ts b/packages/bake/src/objects/raster/netcdf/hdf5-netcdf.ts new file mode 100644 index 00000000000..f7442aced7d --- /dev/null +++ b/packages/bake/src/objects/raster/netcdf/hdf5-netcdf.ts @@ -0,0 +1,195 @@ +/** + * NetCDF-4, the format newer climate models write: an HDF5 container (its structures are read in `hdf5-objects.ts`). This + * reads the plain kind the Met Office Unified Model's files come out of the Iris library as: every variable in the root + * group, stored whole and unfiltered (the contiguous layout) as little-endian numbers, under any of the four superblock + * versions (libnetcdf wrote version 0 until 4.6 and writes version 2 since). + * + * Anything else is refused by name with what was met: a chunked or compressed variable, a nested group, big-endian + * numbers. A variable's dimensions are the dimension scales its DIMENSION_LIST attribute points at, as the NetCDF-4 + * convention writes them. Structure is read from its own bytes and a variable's values from theirs, so a file of hundreds + * of megabytes is never loaded whole. The file must be whole all the same: the library spreads its structure through the + * file as variables are added, so no first part of it stands alone, and a file shorter than the length it states for + * itself is refused. + */ +import { open } from 'node:fs/promises'; +import type { NetcdfAttribute, NetcdfType, NetcdfVariable } from './classic-netcdf.ts'; +import { address, HDF5_MESSAGE, hdf5AttributeMessages, hdf5GlobalHeapObject, hdf5GroupMembers, hdf5Messages, optionalAddress, type Hdf5Message, type Read } from './hdf5-objects.ts'; + +const SIGNATURE = Buffer.from([0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a]); +/** The largest variable read whole: 64 million values are 512 MB as float64. */ +const MAXIMUM_VALUES = 64_000_000; +/** What the NetCDF-4 library writes for its own use beside a variable's attributes, and does not show as attributes. */ +const BOOKKEEPING = new Set(['CLASS', 'NAME', 'DIMENSION_LIST', 'REFERENCE_LIST', '_Netcdf4Dimid', '_Netcdf4Coordinates', '_nc3_strict']); +const DIMENSION_ONLY = 'This is a netCDF dimension but not a netCDF variable.'; + +/** What a datatype message says of one element. `references` and `strings` are variable-length values held in a global heap. */ +interface ElementType { readonly kind: 'number' | 'text' | 'references' | 'strings' | 'other'; readonly bytes: number; readonly type?: NetcdfType; readonly littleEndian: boolean } +interface RawAttribute { readonly name: string; readonly element: ElementType; readonly shape: readonly number[]; readonly data: Buffer } +interface Stored { readonly variable: NetcdfVariable; readonly element: ElementType; readonly address: number | undefined; readonly inline: Buffer | undefined; readonly layout: string } + +const product = (shape: readonly number[]) => shape.reduce((total, length) => total * length, 1); + +function elementType(data: Buffer, label: string): ElementType { + const kind = data[0]! & 0x0f, bits = data[1]!, bytes = data.readUInt32LE(4), littleEndian = (bits & 1) === 0; + if (kind === 1) { + if (bytes !== 4 && bytes !== 8) throw new TypeError(`${label}: a ${bytes}-byte floating-point type is not read.`); + return { kind: 'number', bytes, type: bytes === 4 ? 'float' : 'double', littleEndian }; + } + if (kind === 0) { + const names: Readonly> = { 1: ['byte', 'ubyte'], 2: ['short', 'ushort'], 4: ['int', 'uint'], 8: ['int64', 'uint64'] }; + const name = names[bytes]; + if (!name) throw new TypeError(`${label}: a ${bytes}-byte integer type is not read.`); + return { kind: 'number', bytes, type: name[bits & 8 ? 0 : 1], littleEndian }; + } + if (kind === 3) return { kind: 'text', bytes, type: 'char', littleEndian: true }; + // A variable-length type: text when its class bits say so, otherwise a list, here of object references. + if (kind === 9) return { kind: (bits & 0x0f) === 1 ? 'strings' : (data[8]! & 0x0f) === 7 ? 'references' : 'other', bytes, littleEndian: true }; + return { kind: 'other', bytes, littleEndian: true }; +} + +function shapeOf(data: Buffer, label: string): readonly number[] { + const version = data[0]!; + if (version !== 1 && version !== 2) throw new TypeError(`${label}: dataspace version ${version} is not read.`); + // A version 2 dataspace of type 2 is the null dataspace: no elements. + if (version === 2 && data[3] === 2) return [0]; + return Array.from({ length: data[1]! }, (_, i) => address(data, (version === 1 ? 8 : 4) + 8 * i, label)); +} + +function numbers(bytes: Buffer, element: ElementType, label: string): Float64Array { + if (element.kind !== 'number') throw new TypeError(`${label} is not numbers.`); + if (!element.littleEndian) throw new TypeError(`${label}: big-endian numbers are not read.`); + if (bytes.length % element.bytes) throw new TypeError(`${label}: ${bytes.length} bytes are not a whole number of ${element.bytes}-byte values.`); + const type = element.type; + return Float64Array.from({ length: bytes.length / element.bytes }, (_, index) => { + const at = index * element.bytes; + return type === 'double' ? bytes.readDoubleLE(at) : type === 'float' ? bytes.readFloatLE(at) : type === 'int' ? bytes.readInt32LE(at) : type === 'short' ? bytes.readInt16LE(at) + : type === 'byte' ? bytes.readInt8(at) : type === 'ubyte' ? bytes.readUInt8(at) : type === 'ushort' ? bytes.readUInt16LE(at) : type === 'uint' ? bytes.readUInt32LE(at) + : type === 'int64' ? Number(bytes.readBigInt64LE(at)) : Number(bytes.readBigUInt64LE(at)); + }); +} + +function attribute(data: Buffer, label: string): RawAttribute { + const version = data[0]!; + if (version < 1 || version > 3) throw new TypeError(`${label}: attribute version ${version} is not read.`); + if (version > 1 && (data[1]! & 3)) throw new TypeError(`${label}: an attribute whose type or shape is stored in another object is not read.`); + const nameSize = data.readUInt16LE(2), typeSize = data.readUInt16LE(4), spaceSize = data.readUInt16LE(6), pad = (length: number) => version === 1 ? Math.ceil(length / 8) * 8 : length; + const nameAt = version === 3 ? 9 : 8, typeAt = nameAt + pad(nameSize), spaceAt = typeAt + pad(typeSize), dataAt = spaceAt + pad(spaceSize); + const name = data.subarray(nameAt, nameAt + nameSize).toString('utf8').replace(/\0+$/u, ''); + return { name, element: elementType(data.subarray(typeAt, typeAt + typeSize), `${label}, attribute ${name}`), shape: shapeOf(data.subarray(spaceAt, spaceAt + spaceSize), `${label}, attribute ${name}`), data: data.subarray(dataAt) }; +} + +/** A variable-length element: how many items, and the global heap object that holds them. */ +const heapRef = (data: Buffer, at: number, label: string) => ({ length: data.readUInt32LE(at), collection: address(data, at + 4, label), index: data.readUInt32LE(at + 12) }); + +/** Text and numbers; an attribute of any other type (a reference list, a compound) has no NetCDF value and is left out. */ +async function attributeValue(read: Read, raw: RawAttribute, label: string): Promise { + if (raw.element.kind === 'text') return raw.data.subarray(0, raw.element.bytes).toString('utf8').replace(/\0.*$/su, ''); + if (raw.element.kind === 'number') return [...numbers(raw.data.subarray(0, product(raw.shape) * raw.element.bytes), raw.element, `${label}, attribute ${raw.name}`)]; + if (raw.element.kind === 'strings' && product(raw.shape) === 1) { + const ref = heapRef(raw.data, 0, label); + return (await hdf5GlobalHeapObject(read, ref.collection, ref.index, label)).subarray(0, ref.length).toString('utf8'); + } + return undefined; +} + +export interface Hdf5Netcdf { + readonly variables: ReadonlyMap; + /** Every value of a numeric variable, in storage order (the last dimension varying fastest), as stored: no scale, offset or fill is applied. */ + values(name: string): Promise; + /** `count` of those values from the `start`-th, read from their own bytes alone. */ + slice(name: string, start: number, count: number): Promise; + close(): Promise; +} + +/** Open a NetCDF-4 file of the plain kind this reader takes: its structure is read now, a variable's values when asked for. */ +export async function openHdf5Netcdf(path: string): Promise { + const file = await open(path, 'r'); + try { + const { size } = await file.stat(); + const read: Read = async (offset, length) => { + if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > size) throw new TypeError(`${path}: its structure points at ${length} bytes from byte ${offset}, outside its ${size} bytes.`); + const bytes = Buffer.alloc(length), { bytesRead } = await file.read(bytes, 0, length, offset); + if (bytesRead !== length) throw new TypeError(`${path} ends inside the ${length} bytes at ${offset}.`); + return bytes; + }; + const superblock = await read(0, Math.min(size, 100)); + if (!superblock.subarray(0, 8).equals(SIGNATURE)) throw new TypeError(`${path} is not an HDF5 file, and so not NetCDF-4.`); + const version = superblock[8]!; + if (version > 3) throw new TypeError(`${path}: HDF5 superblock version ${version} is not read.`); + // Versions 0 and 1 give the sizes after five version bytes and end on the root group's symbol table entry, whose second + // field is the root's object header; versions 2 and 3 give the sizes first and the root's object header directly. + const sizesAt = version < 2 ? 13 : 9, fields = version === 0 ? 24 : version === 1 ? 28 : 12; + if (superblock[sizesAt] !== 8 || superblock[sizesAt + 1] !== 8) throw new TypeError(`${path}: ${superblock[sizesAt]}-byte offsets and ${superblock[sizesAt + 1]}-byte lengths are not read; 8 and 8 are.`); + if (address(superblock, fields, path) !== 0) throw new TypeError(`${path}: a base address other than 0 is not read.`); + const stated = address(superblock, fields + 16, path), root = address(superblock, fields + (version < 2 ? 40 : 24), path); + if (size < stated) throw new TypeError(`${path} is cut short: it holds ${size} of the ${stated} bytes it states for itself.`); + const members = await hdf5GroupMembers(read, await hdf5Messages(read, root, path), path); + + const byAddress = new Map([...members].map(([name, at]) => [at, name])), stored = new Map(), groups: string[] = []; + for (const [name, at] of members) { + const label = `${path}, ${name}`, all = await hdf5Messages(read, at, label); + const one = (type: number): Hdf5Message | undefined => all.find(message => message.type === type); + if (one(HDF5_MESSAGE.linkInfo)) { groups.push(name); continue; } + const type = one(HDF5_MESSAGE.datatype), space = one(HDF5_MESSAGE.dataspace), layout = one(HDF5_MESSAGE.layout)?.data; + if (!type || !space || !layout) continue; + if (type.shared || space.shared) throw new TypeError(`${label}: a type or shape stored in another object is not read.`); + const element = elementType(type.data, label), shape = shapeOf(space.data, label); + if (element.kind !== 'number' && element.kind !== 'text') continue; + const raw = (await hdf5AttributeMessages(read, all, label)).map(data => attribute(data, label)); + const scale = raw.find(entry => entry.name === 'NAME' && entry.element.kind === 'text')?.data.toString('latin1'); + // A dimension no variable was declared for is still written as a dataset, with a NAME that says so. + if (scale?.startsWith(DIMENSION_ONLY)) continue; + const attributes: Record = {}; + for (const entry of raw) { + const value = BOOKKEEPING.has(entry.name) ? undefined : await attributeValue(read, entry, label); + if (value !== undefined) attributes[entry.name] = value; + } + // The fill value message stands in when the variable carries no attribute of its own for it. + const fill = one(HDF5_MESSAGE.fillValue)?.data; + if (fill && attributes._FillValue === undefined && element.kind === 'number') { + const defined = fill[0] === 3 ? (fill[1]! & 0x20) !== 0 : fill[0] === 2 && fill[3] === 1, valueAt = fill[0] === 3 ? 6 : 8; + if (defined && fill.readUInt32LE(valueAt - 4) === element.bytes) attributes._FillValue = [...numbers(fill.subarray(valueAt, valueAt + element.bytes), element, label)]; + } + // Dimensions: the scales the DIMENSION_LIST attribute points at; a scale is its own dimension. + const list = raw.find(entry => entry.name === 'DIMENSION_LIST' && entry.element.kind === 'references'); + const dimensions: string[] = []; + for (let i = 0; i < shape.length; i++) { + let dimension: string | undefined; + if (list) { + const ref = heapRef(list.data, i * 16, label); + if (ref.length > 0) dimension = byAddress.get(address(await hdf5GlobalHeapObject(read, ref.collection, ref.index, label), 0, label)); + } + dimensions.push(dimension ?? (shape.length === 1 && scale !== undefined ? name : `${name}#${i}`)); + } + if (layout[0] !== 3 && layout[0] !== 4) throw new TypeError(`${label}: data layout version ${layout[0]} is not read.`); + const kind = layout[1] === 1 ? 'contiguous' : layout[1] === 0 ? 'compact' : layout[1] === 2 ? 'chunked' : `in layout class ${layout[1]}`; + stored.set(name, { variable: { name, dimensions, shape, type: element.type!, attributes }, element, layout: kind, + // No address is a variable no value was ever written to. + address: kind === 'contiguous' ? optionalAddress(layout, 2, label) : undefined, + inline: kind === 'compact' ? layout.subarray(4, 4 + layout.readUInt16LE(2)) : undefined }); + } + if (groups.length) throw new TypeError(`${path} holds the groups ${groups.join(', ')}; variables inside groups are not read.`); + + const entryOf = (name: string) => { + const entry = stored.get(name); + if (!entry) throw new TypeError(`${path} has no variable ${name}; it has ${[...stored.keys()].join(', ')}.`); + if (entry.element.kind !== 'number') throw new TypeError(`${path}: variable ${name} is text, not numbers.`); + return entry; + }; + const slice = async (name: string, start: number, count: number) => { + const entry = entryOf(name), total = product(entry.variable.shape), bytes = entry.element.bytes; + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(count) || start < 0 || count <= 0 || start + count > total) + throw new RangeError(`${path}: variable ${name} has ${total} values, and ${count} from value ${start} were asked for.`); + if (entry.inline) return numbers(entry.inline.subarray(start * bytes, (start + count) * bytes), entry.element, `${path}, ${name}`); + if (entry.layout !== 'contiguous') throw new TypeError(`${path}: variable ${name} is stored ${entry.layout}; only a variable stored whole and unfiltered is read.`); + if (entry.address === undefined) throw new TypeError(`${path}: variable ${name} was never written.`); + return numbers(await read(entry.address + start * bytes, count * bytes), entry.element, `${path}, ${name}`); + }; + return { variables: new Map([...stored].map(([name, entry]) => [name, entry.variable])), slice, close: () => file.close(), + async values(name: string) { + const total = product(entryOf(name).variable.shape); + if (total > MAXIMUM_VALUES) throw new RangeError(`${path}: variable ${name} has ${total} values, more than the ${MAXIMUM_VALUES} read at once.`); + return slice(name, 0, total); + } }; + } catch (error) { await file.close(); throw error; } +} diff --git a/packages/bake/src/objects/raster/netcdf/hdf5-objects.ts b/packages/bake/src/objects/raster/netcdf/hdf5-objects.ts new file mode 100644 index 00000000000..d7ba1d7af9e --- /dev/null +++ b/packages/bake/src/objects/raster/netcdf/hdf5-objects.ts @@ -0,0 +1,163 @@ +/** + * The parts of an HDF5 file a NetCDF-4 reader walks, laid out by the HDF5 file format specification + * (https://docs.hdfgroup.org/hdf5/v1_14/_f_m_t3.html), in the layouts the NetCDF-4 library writes: an object's version 2 + * header messages, a group's links (kept in the header, or in a fractal heap indexed by a version 2 B-tree once there are + * more than a few), and the two heaps attribute values live in. + * + * Every function reads only the bytes its structure names. What is not read is refused by name: a filtered heap, a heap + * object stored outside the heap's own blocks, a nested indirect heap block, a soft or external link, and the older + * version 1 headers and symbol-table groups, which NetCDF-4 does not write. + */ +export type Read = (offset: number, length: number) => Promise; +export interface Hdf5Message { readonly type: number; readonly data: Buffer; /** Stored in another object, not here. */ readonly shared: boolean } + +export const HDF5_MESSAGE = { dataspace: 0x01, linkInfo: 0x02, datatype: 0x03, fillValue: 0x05, link: 0x06, layout: 0x08, attribute: 0x0c, continuation: 0x10, attributeInfo: 0x15 } as const; +const UNDEFINED = 0xffffffffffffffffn; + +const signature = (bytes: Buffer) => bytes.subarray(0, 4).toString('latin1'); +/** An unsigned little-endian integer of `size` bytes. */ +export function uint(bytes: Buffer, at: number, size: number, label: string): number { + let value = 0; + for (let i = size - 1; i >= 0; i--) value = value * 256 + bytes[at + i]!; + if (!Number.isSafeInteger(value)) throw new RangeError(`${label}: a ${size}-byte number is beyond what is read.`); + return value; +} +/** An 8-byte file address; undefined when HDF5 wrote its all-ones "no address". */ +export const optionalAddress = (bytes: Buffer, at: number, label: string) => bytes.readBigUInt64LE(at) === UNDEFINED ? undefined : uint(bytes, at, 8, label); +export function address(bytes: Buffer, at: number, label: string): number { + const value = optionalAddress(bytes, at, label); + if (value === undefined) throw new TypeError(`${label}: an address the structure needs was never written.`); + return value; +} +/** How many bytes hold a number up to `maximum`, as HDF5 sizes its variable-width fields. */ +const bytesFor = (maximum: number) => Math.max(1, Math.ceil(Math.log2(maximum + 1) / 8)); + +/** An object header's messages, following its continuation blocks. */ +export async function hdf5Messages(read: Read, at: number, label: string): Promise { + const head = await read(at, 16), out: Hdf5Message[] = []; + if (signature(head) !== 'OHDR') throw new TypeError(`${label}: no version 2 object header at byte ${at}. NetCDF-4 tracks the order its contents were made in, which writes that version.`); + if (head[4] !== 2) throw new TypeError(`${label}: object header version ${head[4]} is not read.`); + // Version 2: optional times and attribute thresholds, then the first chunk's size; each later chunk carries a signature and a checksum. + const flags = head[5]!, sizeAt = 6 + (flags & 0x20 ? 16 : 0) + (flags & 0x10 ? 4 : 0), sizeBytes = 1 << (flags & 3), prefix = flags & 0x04 ? 6 : 4; + const first = await read(at, sizeAt + sizeBytes), blocks: [number, number][] = [[at + sizeAt + sizeBytes, uint(first, sizeAt, sizeBytes, label)]]; + for (let block = blocks.shift(); block; block = blocks.shift()) { + const bytes = await read(block[0], block[1]); + for (let p = 0; p + prefix <= bytes.length;) { + const type = bytes[p]!, size = bytes.readUInt16LE(p + 1), data = bytes.subarray(p + prefix, p + prefix + size); + if (type === HDF5_MESSAGE.continuation) blocks.push([address(data, 0, label) + 4, address(data, 8, label) - 8]); + out.push({ type, data, shared: (bytes[p + 3]! & 2) !== 0 }); + p += prefix + size; + } + } + return out; +} + +/** A fractal heap's managed objects, by heap ID: where a group keeps its links and an object its attributes once there are many. */ +async function fractalHeap(read: Read, at: number, label: string): Promise<{ readonly idLength: number; object(id: Buffer): Buffer }> { + const head = await read(at, 142); + if (signature(head) !== 'FRHP' || head[4] !== 0) throw new TypeError(`${label}: no fractal heap at byte ${at}.`); + if (head.readUInt16LE(7)) throw new TypeError(`${label}: a filtered fractal heap is not read.`); + const idLength = head.readUInt16LE(5), width = head.readUInt16LE(110), starting = address(head, 112, label), largest = address(head, 120, label); + const offsetBytes = Math.ceil(head.readUInt16LE(128) / 8), lengthBytes = bytesFor(Math.min(largest, head.readUInt32LE(10))), rows = head.readUInt16LE(140); + const directRows = Math.log2(largest) - Math.log2(starting) + 2, root = optionalAddress(head, 132, label), blocks: { offset: number; bytes: Buffer }[] = []; + const direct = async (where: number, size: number) => { + const bytes = await read(where, size); + if (signature(bytes) !== 'FHDB') throw new TypeError(`${label}: no heap block at byte ${where}.`); + blocks.push({ offset: uint(bytes, 13, offsetBytes, label), bytes }); + }; + if (root !== undefined && rows === 0) await direct(root, starting); + else if (root !== undefined) { + const bytes = await read(root, 13 + offsetBytes + rows * width * 8); + if (signature(bytes) !== 'FHIB') throw new TypeError(`${label}: no heap index at byte ${root}.`); + for (let row = 0, p = 13 + offsetBytes; row < rows; row++) for (let column = 0; column < width; column++, p += 8) { + const child = optionalAddress(bytes, p, label); + if (child === undefined) continue; + if (row >= directRows) throw new TypeError(`${label}: a fractal heap with nested index blocks is not read.`); + await direct(child, row < 2 ? starting : starting * 2 ** (row - 1)); + } + } + return { idLength, object(id) { + if ((id[0]! & 0x30) !== 0) throw new TypeError(`${label}: a heap object stored outside the heap's blocks is not read.`); + // A managed object's offset counts through the heap's blocks, their headers included. + const offset = uint(id, 1, offsetBytes, label), length = uint(id, 1 + offsetBytes, lengthBytes, label); + const block = blocks.find(entry => offset >= entry.offset && offset + length <= entry.offset + entry.bytes.length); + if (!block) throw new TypeError(`${label}: the heap at byte ${at} has no block holding ${length} bytes at ${offset}.`); + return block.bytes.subarray(offset - block.offset, offset - block.offset + length); + } }; +} + +/** Every record of a version 2 B-tree, in key order. */ +async function btreeRecords(read: Read, at: number, label: string): Promise<{ readonly type: number; readonly records: Buffer[] }> { + const head = await read(at, 34); + if (signature(head) !== 'BTHD' || head[4] !== 0) throw new TypeError(`${label}: no version 2 B-tree at byte ${at}.`); + const type = head[5]!, nodeSize = head.readUInt32LE(6), recordSize = head.readUInt16LE(10), depth = head.readUInt16LE(12), root = optionalAddress(head, 16, label), records: Buffer[] = []; + // How many records a node of each level can hold sets the width of the counts beside its child pointers. + const most = [Math.floor((nodeSize - 10) / recordSize)], cumulative = [most[0]!], countBytes = bytesFor(most[0]!); + const pointerBytes = (level: number) => 8 + countBytes + (level > 1 ? bytesFor(cumulative[level - 1]!) : 0); + for (let level = 1; level <= depth; level++) { + most[level] = Math.floor((nodeSize - 10 - pointerBytes(level)) / (recordSize + pointerBytes(level))); + cumulative[level] = (most[level]! + 1) * cumulative[level - 1]! + most[level]!; + } + const node = async (where: number, count: number, level: number): Promise => { + const bytes = await read(where, nodeSize); + if (signature(bytes) !== (level ? 'BTIN' : 'BTLF')) throw new TypeError(`${label}: no B-tree node at byte ${where}.`); + const own = Array.from({ length: count }, (_, i) => bytes.subarray(6 + i * recordSize, 6 + (i + 1) * recordSize)); + if (!level) { records.push(...own); return; } + for (let child = 0, p = 6 + count * recordSize; child <= count; child++, p += pointerBytes(level)) { + await node(address(bytes, p, label), uint(bytes, p + 8, countBytes, label), level - 1); + if (child < count) records.push(own[child]!); + } + }; + if (root !== undefined) await node(root, head.readUInt16LE(24), depth); + return { type, records }; +} + +/** The objects a fractal heap holds, found through its name index. A record keeps its heap ID after a link's name hash, and first for an attribute. */ +async function indexedObjects(read: Read, heapAt: number, indexAt: number, label: string): Promise { + const heap = await fractalHeap(read, heapAt, label), { type, records } = await btreeRecords(read, indexAt, label); + if (type !== 5 && type !== 8) throw new TypeError(`${label}: the B-tree at byte ${indexAt} is of type ${type}, not a name index.`); + return records.map(record => heap.object(record.subarray(type === 5 ? 4 : 0, (type === 5 ? 4 : 0) + heap.idLength))); +} + +function link(data: Buffer, label: string): [string, number] { + if (data[0] !== 1) throw new TypeError(`${label}: link message version ${data[0]} is not read.`); + const flags = data[1]!, type = flags & 0x08 ? data[2]! : 0, sizeBytes = 1 << (flags & 3); + const sizeAt = 2 + (flags & 0x08 ? 1 : 0) + (flags & 0x04 ? 8 : 0) + (flags & 0x10 ? 1 : 0), length = uint(data, sizeAt, sizeBytes, label); + const name = data.subarray(sizeAt + sizeBytes, sizeAt + sizeBytes + length).toString('utf8'); + if (type !== 0) throw new TypeError(`${label}: ${name} is a soft or external link, which is not followed.`); + return [name, address(data, sizeAt + sizeBytes + length, label)]; +} + +/** A group's members by name, each with its object header's address, from the group's own header messages. */ +export async function hdf5GroupMembers(read: Read, messages: readonly Hdf5Message[], label: string): Promise> { + const members = new Map(); + const info = messages.find(message => message.type === HDF5_MESSAGE.linkInfo); + if (!info) throw new TypeError(`${label}: its root group keeps no link information, as a NetCDF-4 file's does.`); + for (const message of messages) if (message.type === HDF5_MESSAGE.link) members.set(...link(message.data, label)); + const heapAt = optionalAddress(info.data, info.data[1]! & 1 ? 10 : 2, label); + if (heapAt !== undefined) for (const data of await indexedObjects(read, heapAt, address(info.data, info.data[1]! & 1 ? 18 : 10, label), label)) members.set(...link(data, label)); + return members; +} + +/** An object's attribute messages: those in its header, and those its attribute info keeps in a fractal heap. */ +export async function hdf5AttributeMessages(read: Read, messages: readonly Hdf5Message[], label: string): Promise { + const out = messages.filter(message => message.type === HDF5_MESSAGE.attribute).map(message => message.data); + const info = messages.find(message => message.type === HDF5_MESSAGE.attributeInfo); + const heapAt = info && optionalAddress(info.data, info.data[1]! & 1 ? 4 : 2, label); + if (info && heapAt !== undefined) out.push(...await indexedObjects(read, heapAt, address(info.data, info.data[1]! & 1 ? 12 : 10, label), label)); + return out; +} + +/** One object of a global heap collection: where HDF5 keeps the elements of variable-length values. */ +export async function hdf5GlobalHeapObject(read: Read, collection: number, index: number, label: string): Promise { + const head = await read(collection, 16); + if (signature(head) !== 'GCOL') throw new TypeError(`${label}: no global heap at byte ${collection}.`); + const bytes = await read(collection, address(head, 8, label)); + for (let p = 16; p + 16 <= bytes.length;) { + const id = bytes.readUInt16LE(p), size = address(bytes, p + 8, label); + if (id === index) return bytes.subarray(p + 16, p + 16 + size); + if (id === 0) break; + p += 16 + Math.ceil(size / 8) * 8; + } + throw new TypeError(`${label}: the global heap at byte ${collection} holds no object ${index}.`); +} diff --git a/packages/bake/src/objects/raster/netcdf/netcdf-isobar.ts b/packages/bake/src/objects/raster/netcdf/netcdf-isobar.ts new file mode 100644 index 00000000000..16e84aa7514 --- /dev/null +++ b/packages/bake/src/objects/raster/netcdf/netcdf-isobar.ts @@ -0,0 +1,39 @@ +/** + * A model's field on a surface of constant pressure. A climate model stores its fields on its own levels, and where those + * are heights the pressure on one level differs between the day side and the night side; its papers draw their maps at one + * pressure. The recipe names the level dimension, the variable holding each cell's pressure, and the pressure to read at: + * + * "isobar": { "along": "level_height_0", "pressure": "air_pressure", "pressureUnits": "Pa", "at": 100 } + * + * - `pressure` lies on the field's own grid and `pressureUnits` is its `units` attribute, letter for letter. + * - `at` is in those units. + * + * In each column the value is taken between the two levels whose pressures lie either side of `at`, linearly in the + * logarithm of pressure. A column whose levels do not reach `at`, or that has no value on one of those two levels, has + * none: nothing is extrapolated. A model's pressure can waver from level to level near its top; that is accepted away from + * `at`, and a column whose pressure passes `at` more than once is refused, because no one surface there has that pressure. + */ +import { requireFiniteNumber, requireRecord, requireString } from '@cssearth/core'; + +export interface Isobar { readonly along: string; readonly pressure: string; readonly pressureUnits: string; readonly at: number } + +export function parseIsobar(value: unknown, label: string): Isobar { + const isobar = requireRecord(value, label), at = requireFiniteNumber(isobar.at, `${label}.at`); + if (!(at > 0)) throw new RangeError(`${label}.at must be a pressure above zero.`); + return { along: requireString(isobar.along, `${label}.along`), pressure: requireString(isobar.pressure, `${label}.pressure`), pressureUnits: requireString(isobar.pressureUnits, `${label}.pressureUnits`), at }; +} + +/** One column's value at the pressure `at`, or NaN when it has none. `field` gives NaN for a level without a value. */ +export function isobarValue(levels: number, pressure: (level: number) => number, field: (level: number) => number, at: number, label: string): number { + let value = NaN, crossings = 0; + for (let level = 0, here = pressure(0); level < levels; level++) { + const next = level + 1 < levels ? pressure(level + 1) : here; + if (!(here > 0 && Number.isFinite(here))) throw new TypeError(`${label}: level ${level} of a column has no pressure.`); + // On a level exactly, the value is that level's own; between two, it is taken along the logarithm of pressure. + if (here === at) { crossings++; value = field(level); } + else if (next !== at && (here - at) * (next - at) < 0) { crossings++; value = field(level) + (field(level + 1) - field(level)) * Math.log(at / here) / Math.log(next / here); } + here = next; + } + if (crossings > 1) throw new TypeError(`${label}: a column's pressure passes ${at} ${crossings} times along its levels, so no one surface there has that pressure.`); + return value; +} diff --git a/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.test.mts b/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.test.mts index b3fd5d90127..44158557317 100644 --- a/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.test.mts +++ b/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.test.mts @@ -122,6 +122,67 @@ test('a grid that ends on its first longitude again is read without the repeat', await assert.rejects(decodeNetcdfLonLatField(source(7), recipe, 'lmd.nc'), /lmd\.nc: tsurf ends on its first longitude again, and row 0 holds 0 there and 7 360 degrees on/u); }); +/** The NetCDF-4 fixture's fields (fixtures/README.md): its longitudes start at 22.5, and T and P lie on three levels. */ +const netcdf4 = (overrides: Record = {}) => field({ path: 'field-nc4.nc', ...overrides }); +const isobar = (at: number, overrides: Record = {}) => netcdf4({ variable: 'T', select: { time: 0 }, + isobar: { along: 'lev', pressure: 'P', pressureUnits: 'Pa', at, ...overrides } }); + +test('a NetCDF-4 file is read through the same recipe', async () => { + const map = await loadNetcdfLonLatField(root, netcdf4()); + for (let i = 0; i < 4; i++) for (let j = 0; j < 8; j++) assert.equal(map.sample(22.5 + j * 45, -67.5 + i * 45), surface(1, i, j), `node ${i},${j}`); + assert.deepEqual([map.report.format, map.report.width, map.report.height, map.report.longitudeRange, map.report.missing], ['netcdf-lonlat-field', 8, 4, [22.5, 337.5], 0]); + const packed = await loadNetcdfLonLatField(root, netcdf4({ variable: 'PACKED', select: {} })); + assert.equal(packed.sample(157.5, -22.5), 200 + 0.5 * 13); + assert.equal(packed.sample(112.5, -22.5), null, 'the cell at the fill value has no value'); + await assert.rejects(loadNetcdfLonLatField(root, netcdf4({ variable: 'SQUEEZED', select: {} })), /variable SQUEEZED is stored chunked/u); + await assert.rejects(loadNetcdfLonLatField(root, netcdf4({ field: 'field.TS-time1.dat' })), /field-nc4\.nc is NetCDF-4, which is kept whole/u); +}); + +test('a field is read at one pressure, between the two levels either side of it', async () => { + // T[k, i, j] = 1000 - 100 k + 10 i + j at the first time, and P[k, j] = 1000 * 10^-k * (1 + j / 8) Pa: the pressure of 150 Pa + // lies between the first two levels in the columns j < 4, on the second level exactly at j = 4, and above it beyond. + const map = await loadNetcdfLonLatField(root, isobar(150)); + for (let i = 0; i < 4; i++) for (let j = 0; j < 8; j++) { + const k = j < 4 ? 0 : 1, lower = 1000 * 10 ** -k * (1 + j / 8), share = Math.log(150 / lower) / Math.log(0.1); + const expected = 1000 - 100 * k + 10 * i + j - 100 * share, value = map.sample(22.5 + j * 45, -67.5 + i * 45)!; + assert.ok(Math.abs(value - expected) < 1e-9, `column ${i},${j}: ${value} for ${expected}`); + } + assert.equal(map.sample(22.5 + 4 * 45, -67.5), 904, 'on a level exactly, the value is that level\'s own'); + assert.deepEqual([map.report.select, map.report.isobar, map.report.missing], [{ time: 0 }, { along: 'lev', pressure: 'P', pressureUnits: 'Pa', at: 150, levels: 3 }, 0]); + assert.equal((await loadNetcdfLonLatField(root, isobar(150, {}))).sample(22.5, -67.5), 1000 - 100 * Math.log(0.15) / Math.log(0.1)); + // 1200 Pa is deeper than the first level of the columns j < 2, which hold 1000 and 1125 Pa there: they have no value. + const deep = await loadNetcdfLonLatField(root, isobar(1200)); + assert.equal(deep.report.missing, 8); + assert.equal(deep.sample(22.5, -67.5), null, 'nothing is extrapolated below the first level'); + assert.ok(Math.abs(deep.sample(22.5 + 2 * 45, -67.5)! - (1002 - 100 * Math.log(1200 / 1250) / Math.log(0.1))) < 1e-9); + await assert.rejects(loadNetcdfLonLatField(root, isobar(5000)), /every cell of T is a missing value/u); +}); + +test('an isobar the file cannot give is refused with the reason', async () => { + await assert.rejects(loadNetcdfLonLatField(root, isobar(0)), /isobar\.at must be a pressure above zero/u); + await assert.rejects(loadNetcdfLonLatField(root, isobar(150, { along: 'lat' })), /isobar\.along is lat, which is not a level dimension of T; it varies along time, lev, lat, lon/u); + await assert.rejects(loadNetcdfLonLatField(root, isobar(150, { along: 'height' })), /isobar\.along is height, which is not a level dimension of T/u); + await assert.rejects(loadNetcdfLonLatField(root, netcdf4({ variable: 'T', select: { time: 0, lev: 1 }, isobar: { along: 'lev', pressure: 'P', pressureUnits: 'Pa', at: 150 } })), + /select names lev, which T is not selected along/u); + await assert.rejects(loadNetcdfLonLatField(root, isobar(150, { pressure: 'PRES' })), /field-nc4\.nc has no pressure variable PRES/u); + await assert.rejects(loadNetcdfLonLatField(root, isobar(150, { pressure: 'TS' })), /TS varies along time, lat, lon, not on the grid of T \(time, lev, lat, lon\)/u); + await assert.rejects(loadNetcdfLonLatField(root, isobar(150, { pressureUnits: 'hPa' })), /isobar\.pressureUnits is "hPa", and the file gives P the units "Pa"/u); + // A model's pressure can waver near its top. Away from the pressure asked for that changes nothing; a column that passes + // it more than once has no one surface at that pressure. + const variable = (name: string, dimensions: string[], shape: number[], units: string): NetcdfVariable => ({ name, dimensions, shape, type: 'double', attributes: { units } }); + const pressures = [100, 10, 1, 1.2, 0.9], temperatures = [500, 400, 300, 200, 100]; + const source = { variables: new Map([['T', variable('T', ['level', 'lat', 'lon'], [5, 2, 2], 'K')], ['P', variable('P', ['level', 'lat', 'lon'], [5, 2, 2], 'Pa')], + ['lon', variable('lon', ['lon'], [2], 'degrees_east')], ['lat', variable('lat', ['lat'], [2], 'degrees_north')]]), + values: async (name: string) => name === 'lon' ? Float64Array.of(0, 180) : name === 'lat' ? Float64Array.of(-45, 45) + : Float64Array.from({ length: 20 }, (_, index) => (name === 'P' ? pressures : temperatures)[Math.floor(index / 4)]!), + slice: async () => { throw new Error('an isobar reads every level'); } }; + const wavering = (at: number) => decodeNetcdfLonLatField(source, { variable: 'T', coordinates: { longitude: 'lon', latitude: 'lat' }, select: {}, isobar: { along: 'level', pressure: 'P', pressureUnits: 'Pa', at }, + sourceUnits: 'K', longitudeZeroAt: 0, coordinateToleranceDegrees: 0.005 }, 'model.nc'); + assert.equal((await wavering(10)).sample(0, 45), 400); + assert.ok(Math.abs((await wavering(31.622776601683793)).sample(0, 45)! - 450) < 1e-9, 'halfway between two levels in the logarithm of pressure'); + await assert.rejects(wavering(1.1), /model\.nc: a column's pressure passes 1\.1 3 times along its levels, so no one surface there has that pressure/u); +}); + test('the scientific raster interpreter reads the format', async () => { const map = await loadScienceSurface(root, { kind: 'terrestrial-scientific', id: 'surface-temperature', label: 'Surface temperature', consumer: 'fixture', units: 'K', minimum: 200, maximum: 340, colors: ['#000004', '#fcffa4'], ...field() }); diff --git a/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.ts b/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.ts index 0e05be488a6..c80dc1af3ce 100644 --- a/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.ts +++ b/packages/bake/src/objects/raster/netcdf/netcdf-lonlat-field.ts @@ -1,7 +1,8 @@ /** - * One scalar field on a model's own longitude/latitude grid, read from a classic NetCDF file a paper released - * (classic-netcdf.ts): a climate simulation's surface temperature, for example. The recipe states everything the file does - * not make certain, and each statement is checked against the file: + * One scalar field on a model's own longitude/latitude grid, read from a NetCDF file a paper released, classic + * (classic-netcdf.ts) or NetCDF-4 (hdf5-netcdf.ts), told apart by the file's first bytes: a climate simulation's surface + * temperature, for example. The recipe states everything the file does not make certain, and each statement is checked + * against the file: * * { "format": "netcdf-lonlat-field", "path": "science//.nc", "variable": "TS", * "coordinates": { "longitude": "lon", "latitude": "lat" }, "select": { "time": 0 }, @@ -13,8 +14,10 @@ * - `sourceUnits` is the variable's `units` attribute, letter for letter. * - `longitudeZeroAt` is the grid longitude, in degrees east, that the body's zero meridian falls on. For a planet that keeps * one face to its star this is the longitude where the model put the star overhead, as its paper states it. + * - `isobar`, when given, reads the field at one pressure instead of on one level (netcdf-isobar.ts). Its level dimension + * is then not in `select`. * - * A release too large to keep whole is kept as two of its byte ranges, exact bytes both, which the manifest declares: + * A classic release too large to keep whole is kept as two of its byte ranges, exact bytes both, which the manifest declares: * * { ..., "path": "science//.head.dat", "field": "science//.-.dat" } * @@ -22,17 +25,21 @@ * grid. The header says where that selection lies in the release, and the manifest must declare that very range for `field`. * This needs the longitude and latitude to be the variable's last two dimensions, so the selection is one run of bytes. * + * A NetCDF-4 file keeps its structure throughout itself, not in a header, so it is kept whole. + * * The grid must go all the way around in even longitude steps. A grid that ends on its first longitude again, 360 degrees * on, must hold the same values in both columns; the column is read once. Latitude coverage ends at the released rows: nothing is * extrapolated to a pole. Values equal to the file's `_FillValue` or `missing_value` are cells without a value, and a sample * that would use one has none. `scale_factor` and `add_offset` are applied as the file states them. No averaging, fitting or * smoothing is done here: a time mean is whatever the release itself wrote. */ -import { readFile } from 'node:fs/promises'; +import { open, readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { requireArray, requireFiniteNumber, requireRecord, requireString } from '@cssearth/core'; import { periodicLonLatGrid } from '../lonlat/lonlat-grid.ts'; import { openClassicNetcdf, storedValues, valueRange, type NetcdfAttribute, type NetcdfVariable } from './classic-netcdf.ts'; +import { openHdf5Netcdf } from './hdf5-netcdf.ts'; +import { isobarValue, parseIsobar } from './netcdf-isobar.ts'; /** What the field reader needs of an opened file; a test passes its own. */ export interface NetcdfSource { @@ -71,23 +78,46 @@ export async function decodeNetcdfLonLatField(source: NetcdfSource, value: unkno const descending = latitude.nodes[0]! > latitude.nodes.at(-1)!, latitudes = descending ? [...latitude.nodes].reverse() : latitude.nodes; for (let i = 1; i < latitudes.length; i++) if (!(latitudes[i]! > latitudes[i - 1]!)) throw new TypeError(`${label}: latitudes must run one way from pole to pole.`); + const isobar = dataset.isobar === undefined ? undefined : parseIsobar(dataset.isobar, at('isobar')), along = isobar ? variable.dimensions.indexOf(isobar.along) : -1; + if (isobar && (along < 0 || along === longitude.position || along === latitude.position)) + throw new TypeError(`${at('isobar.along')} is ${isobar.along}, which is not a level dimension of ${name}; it varies along ${variable.dimensions.join(', ')}.`); // Every other dimension takes the index the recipe states for it. const fixed = variable.dimensions.map((dimension, position) => { - if (position === longitude.position || position === latitude.position) return 0; + if (position === longitude.position || position === latitude.position || position === along) return 0; if (!(dimension in select)) throw new TypeError(`${at('select')} gives no index along ${dimension}; ${name} varies along ${variable.dimensions.join(', ')}.`); const index = requireFiniteNumber(select[dimension], at(`select.${dimension}`)); if (!Number.isSafeInteger(index) || index < 0 || index >= variable.shape[position]!) throw new RangeError(`${at(`select.${dimension}`)} is ${index}; ${dimension} has ${variable.shape[position]} entries, counted from 0.`); return index; }); - const unknown = Object.keys(select).filter(key => !variable.dimensions.includes(key) || key === variable.dimensions[longitude.position] || key === variable.dimensions[latitude.position]); + const unknown = Object.keys(select).filter(key => !variable.dimensions.includes(key) || key === variable.dimensions[longitude.position] || key === variable.dimensions[latitude.position] || key === isobar?.along); if (unknown.length) throw new TypeError(`${at('select')} names ${unknown.join(', ')}, which ${name} is not selected along.`); + const fill = [one(variable.attributes._FillValue), one(variable.attributes.missing_value)].filter((n): n is number => n !== undefined); const strides = variable.shape.map((_, position) => variable.shape.slice(position + 1).reduce((product, length) => product * length, 1)); const base = fixed.reduce((sum, index, position) => sum + index * strides[position]!, 0), last = variable.shape.length - 1; - // With longitude and latitude last, the selected grid is one run of the variable's values, and only that run is read. - const run = Math.min(longitude.position, latitude.position) === last - 1 && Math.max(longitude.position, latitude.position) === last; - const stored = run ? await source.slice(name, base, longitude.nodes.length * latitude.nodes.length) : await source.values(name), origin = run ? 0 : base; - const cell = (row: number, column: number) => stored[origin + row * strides[latitude.position]! + column * strides[longitude.position]!]!; + const step = { row: strides[latitude.position]!, column: strides[longitude.position]! }, columns = longitude.nodes.length; + let cell: (row: number, column: number) => number; + if (isobar) { + const pressure = source.variables.get(isobar.pressure); + if (!pressure) throw new TypeError(`${label} has no pressure variable ${isobar.pressure}.`); + if (pressure.dimensions.join() !== variable.dimensions.join() || pressure.shape.join() !== variable.shape.join()) + throw new TypeError(`${label}: ${isobar.pressure} varies along ${pressure.dimensions.join(', ')}, not on the grid of ${name} (${variable.dimensions.join(', ')}).`); + if (pressure.attributes.units !== isobar.pressureUnits) + throw new TypeError(`${at('isobar.pressureUnits')} is ${JSON.stringify(isobar.pressureUnits)}, and the file gives ${isobar.pressure} the units ${JSON.stringify(pressure.attributes.units ?? null)}.`); + const values = await source.values(name), pressures = await source.values(isobar.pressure), levels = variable.shape[along]!, rise = strides[along]!; + const absent = [one(pressure.attributes._FillValue), one(pressure.attributes.missing_value)]; + const surface = Float64Array.from({ length: latitude.nodes.length * columns }, (_, index) => { + const origin = base + Math.floor(index / columns) * step.row + (index % columns) * step.column; + return isobarValue(levels, level => { const stored = pressures[origin + level * rise]!; return absent.includes(stored) ? NaN : stored; }, + level => { const stored = values[origin + level * rise]!; return fill.includes(stored) ? NaN : stored; }, isobar.at, label); + }); + cell = (row, column) => surface[row * columns + column]!; + } else { + // With longitude and latitude last, the selected grid is one run of the variable's values, and only that run is read. + const run = Math.min(longitude.position, latitude.position) === last - 1 && Math.max(longitude.position, latitude.position) === last; + const stored = run ? await source.slice(name, base, columns * latitude.nodes.length) : await source.values(name), origin = run ? 0 : base; + cell = (row, column) => stored[origin + row * step.row + column * step.column]!; + } // The LMD models write the first meridian twice, at both ends of the row: the repeat is checked and left out. if (Math.abs(longitude.nodes.at(-1)! - longitude.nodes[0]! - 360) <= 2 * tolerance) { const end = longitude.nodes.length - 1; @@ -95,7 +125,6 @@ export async function decodeNetcdfLonLatField(source: NetcdfSource, value: unkno throw new TypeError(`${label}: ${name} ends on its first longitude again, and row ${row} holds ${cell(row, 0)} there and ${cell(row, end)} 360 degrees on.`); longitude.nodes.pop(); } - const fill = [one(variable.attributes._FillValue), one(variable.attributes.missing_value)].filter((n): n is number => n !== undefined); const scale = one(variable.attributes.scale_factor) ?? 1, offset = one(variable.attributes.add_offset) ?? 0; const width = longitude.nodes.length, height = latitudes.length, grid = new Float64Array(width * height); let minimum = Infinity, maximum = -Infinity, missing = 0; @@ -110,7 +139,8 @@ export async function decodeNetcdfLonLatField(source: NetcdfSource, value: unkno return { sample: (bodyLongitude: number, bodyLatitude: number) => sampler.sample(bodyLongitude + zeroAt, bodyLatitude), report: { format: 'netcdf-lonlat-field', variable: name, dimensions: variable.dimensions, select: Object.fromEntries(variable.dimensions.flatMap((dimension, position) => - position === longitude.position || position === latitude.position ? [] : [[dimension, fixed[position]!]])), units: sourceUnits, longitudeZeroAt: zeroAt, + position === longitude.position || position === latitude.position || position === along ? [] : [[dimension, fixed[position]!]])), + ...(isobar ? { isobar: { ...isobar, levels: variable.shape[along]! } } : {}), units: sourceUnits, longitudeZeroAt: zeroAt, width, height, longitudeRange: [longitude.nodes[0], longitude.nodes.at(-1)], latitudeRange: [latitudes[0], latitudes.at(-1)], minimum, maximum, missing, polarCoverage: 'No extrapolation beyond the released latitude samples.' }, }; @@ -134,10 +164,21 @@ const inside = (path: string) => { return path; }; +/** Whether a file opens with the HDF5 signature, the container of NetCDF-4. */ +async function isHdf5(path: string): Promise { + const file = await open(path, 'r'); + try { return (await file.read(Buffer.alloc(8), 0, 8, 0)).buffer.equals(Buffer.from([0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a])); } finally { await file.close(); } +} + /** Read the field a recipe names. `declared` gives the byte ranges of a release kept in two parts; the manifest's are read when it is left out. */ export async function loadNetcdfLonLatField(root: string, value: unknown, declared?: ReadonlyMap) { const recipe = requireRecord(value), path = inside(requireString(recipe.path, 'path')); const kept = recipe.field === undefined ? undefined : inside(requireString(recipe.field, 'field')); + if (await isHdf5(resolve(root, path))) { + if (kept !== undefined) throw new TypeError(`${path} is NetCDF-4, which is kept whole: its structure is spread through the file, so no header and field stand apart.`); + const netcdf4 = await openHdf5Netcdf(resolve(root, path)); + try { return await decodeNetcdfLonLatField(netcdf4, value, path); } finally { await netcdf4.close(); } + } const file = await openClassicNetcdf(resolve(root, path)); try { if (kept === undefined) return await decodeNetcdfLonLatField(file, value, path); diff --git a/packages/objects/src/registry/object-tree.test.ts b/packages/objects/src/registry/object-tree.test.ts index 29c02315954..139ce6c0178 100644 --- a/packages/objects/src/registry/object-tree.test.ts +++ b/packages/objects/src/registry/object-tree.test.ts @@ -32,6 +32,8 @@ describe('the object tree', () => { checkBoundStars(stars, [['companion', 'host']]); assert.throws(() => checkBoundStars(stars, [['stray', 'host']]), /src\/objects\/stray\/object\.json: stray is bound to host, so it is inside what host is inside \("host-system"\); its parent is "milky-way"/); assert.throws(() => checkBoundStars(stars, [['companion', 'nowhere']]), /companion is bound to nowhere, which is no object of the registry/); + // A pair just generated: the companion names the host's system before the systems step has written it and moved the host in. + checkBoundStars([...tree, { id: 'new-host', parent: 'milky-way' }, { id: 'new-companion', parent: 'new-host-system' }], [['new-companion', 'new-host']]); }); it('reads an object\'s ancestors from the root down and each object\'s children', () => { diff --git a/packages/objects/src/registry/object-tree.ts b/packages/objects/src/registry/object-tree.ts index 4e65db1517a..e41da1fd766 100644 --- a/packages/objects/src/registry/object-tree.ts +++ b/packages/objects/src/registry/object-tree.ts @@ -79,6 +79,9 @@ export function checkBoundStars(objects: readonly TreeNode[], bonds: Iterable 1 ? `It is one of ${count} planets known around ${hostname}.` : count === 1 ? `It is the only planet known around ${hostname}.` : ''; - const facts = `disc_year ${row.year ?? 'none'}, disc_facility ${row.facility ?? 'none'}, sy_pnum ${count || 'none'}`; + const facts = `disc_year ${row.year ?? 'none'}, disc_facility ${row.facility ?? 'none'}, sy_pnum ${system || 'none'}${system > count ? `, ${count} of them with hostname ${hostname}` : ''}`; const text = measured ? { card: fit(110, `${name} was found${discovered} by ${method}; it does not cross its star.`, `${name} was found by ${method}; it does not cross its star.`), introduction: fit(180, `${family} Its orbit, tilt included, follows ${defaultRow.label}'s fit${modelSize ? ", and its size is the archive's model from its mass" : ''}.`.trim(), diff --git a/packages/telescope-cli/src/new-object/generate.mts b/packages/telescope-cli/src/new-object/generate.mts index 8f31c2da924..6325f607418 100644 --- a/packages/telescope-cli/src/new-object/generate.mts +++ b/packages/telescope-cli/src/new-object/generate.mts @@ -27,7 +27,7 @@ import { gaiaCepheidClass } from '@cssearth/bake/photometry'; import { CEPHEID_GRAVITIES } from './archives/cepheids.mts'; import { writeLedger } from './ledger.mts'; import { adql, csv, SIMBAD_TAP } from './companions.mts'; -import { packageParent, withParent } from './package-parent.mts'; +import { ensureHostSystems, packageParent, withParent } from './package-parent.mts'; const SOLAR_RADIUS_KM = 695700, GM_SUN = 132712440041.93938; const GAIA_LICENSE = { license: 'Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained', licenseEvidence: ['https://www.cosmos.esa.int/web/gaia-users/credits'] }; @@ -515,6 +515,7 @@ export async function runNewObject(specPath: string, { root = checkoutProjectRoo const out = execFileSync(process.execPath, [resolve(import.meta.dirname, 'new-object-cli.mts'), '--hosted', HANDOFF], { cwd: root, stdio: ['ignore', 'pipe', 'inherit'] }).toString('utf8'); for (const result of JSON.parse(out) as NewObjectResult[]) { results.push(result); if (result.failed) progress(` ${result.id}: FAILED, not written: ${result.failed}`); } } + await ensureHostSystems(root, [...(hosted as { spec: { id: string; kind: string }; hostId: string }[]).map(record => ({ id: record.spec.id, host: record.hostId, planet: record.spec.kind === 'planet' })), ...specs.flatMap(spec => spec.boundTo ? [{ id: spec.id, host: spec.boundTo.host, planet: false }] : [])].filter(body => results.some(result => result.id === body.id && !result.failed)), progress); const failures = results.filter(result => result.failed); if (failures.length) progress(`${failures.length} of ${results.length} bodies failed and were not written: ${failures.map(result => result.id).join(', ')}`); return results; diff --git a/packages/telescope-cli/src/new-object/new-object.test.mts b/packages/telescope-cli/src/new-object/new-object.test.mts index 715410465f3..f45febd6a73 100644 --- a/packages/telescope-cli/src/new-object/new-object.test.mts +++ b/packages/telescope-cli/src/new-object/new-object.test.mts @@ -387,7 +387,13 @@ test('the archive draft of a host keeps only its confirmed transiting planets, s const text = planets[0]!.text as { card: string; introduction: string; locator: string }; assert.equal(text.card, 'HD 1 b was found in 2019 by K2 as it crossed its star.'); assert.match(text.introduction, /^It is one of 3 planets known around HD 1\. Its orbit and size follow One et al\. 2019's fit, the archive's default\.$/u); - assert.match(text.locator, /disc_year 2019, disc_facility K2, sy_pnum 3/u); + assert.match(text.locator, /disc_year 2019, disc_facility K2, sy_pnum 3;/u); + // The archive's sy_pnum counts the whole system. Where it counts another star's planet too (WASP-94 B b beside WASP-94 A b), + // the text counts the planets listed under this host, and the locator says both numbers. + const paired = await archiveSpec({ ...archive, async text(url: string) { return (await archive.text(url)).replaceAll(',K2,3', ',K2,4'); } }, 'HD 1', { ids: new Set(), names: new Map(), stars: [] }); + const pairedText = (paired.spec.planets as { text: { introduction: string; locator: string } }[])[0]!.text; + assert.match(pairedText.introduction, /^It is one of 3 planets known around HD 1\./u); + assert.match(pairedText.locator, /sy_pnum 4, 3 of them with hostname HD 1;/u); assert.equal((spec.text as { card: string }).card, '2 planets cross HD 1 as seen from Earth: b, c.'); assert.equal((spec.text as { introduction: string }).introduction, 'Its radius and temperature follow One et al. 2019.'); const filled = await archiveSpec({ ...archive, async text(url) { const query = decodeURIComponent(new URL(url).searchParams.get('query') ?? ''); return query.includes('st_teff') ? gapped : archive.text(url); } }, 'HD 1', { ids: new Set(), names: new Map(), stars: [] }); diff --git a/packages/telescope-cli/src/new-object/package-parent.mts b/packages/telescope-cli/src/new-object/package-parent.mts index 089ce6311cd..caeb2254f19 100644 --- a/packages/telescope-cli/src/new-object/package-parent.mts +++ b/packages/telescope-cli/src/new-object/package-parent.mts @@ -1,5 +1,6 @@ -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { OBJECT_SCHEMA } from '@cssearth/objects'; import { hostedParent } from './new-hosted-planet.mts'; /** The object a package this tool writes is inside (packages/objects/src/registry/object-tree.ts). A star bound to another is @@ -13,6 +14,31 @@ export async function packageParent(root: string, id: string, named: { readonly if (parent === undefined) throw new TypeError(`${id}: a new object names the object it is inside: give its spec a parent (its galaxy's id, "milky-way" for a star Gaia or Hipparcos places) or the star it is bound to (boundTo).`); return parent; } +/** The system a new host's bodies are inside, before the host's first bake. The bake's systems step writes every system from + * the placed world (site/build/prepare/system-packages.mts), but no tool reads a tree in which a parent is missing, that + * step included: a planet that names `-system` needs the package there. This writes it as the step will write it for + * a star with its bodies, where the host sat and with the host's own card, and moves the host inside. A system already + * there is kept. Returns whether it wrote one. */ +export async function ensureHostSystem(root: string, hostId: string, planets: boolean): Promise { + const system = hostedParent(hostId), path = (id: string) => resolve(root, `src/objects/${id}/object.json`); + const missing = (error: NodeJS.ErrnoException) => { if (error.code === 'ENOENT') return undefined; throw error; }; + if (await readFile(path(system), 'utf8').catch(missing) !== undefined) return false; + const host = JSON.parse(await readFile(path(hostId), 'utf8')) as { parent?: unknown; properties: { catalog: Record; worldFrame: unknown } }, catalog = host.properties.catalog; + if (typeof host.parent !== 'string' || host.parent === system) throw new TypeError(`src/objects/${hostId}/object.json: the host of ${system} names no parent, so the system has no place in the object tree.`); + await mkdir(dirname(path(system)), { recursive: true }); + await writeFile(path(system), `${JSON.stringify({ schema: OBJECT_SCHEMA, id: system, parent: host.parent, type: 'system', generator: 'site/build/prepare/system-packages.mts', + properties: { system: { host: hostId }, + catalog: { name: catalog.systemName, systemName: catalog.systemName, classification: planets ? 'planetary-system' : 'star-system', color: catalog.color, distanceAu: catalog.distanceAu, description: catalog.description }, + worldFrame: host.properties.worldFrame } }, null, 2)}\n`); + await writeFile(path(hostId), `${JSON.stringify({ ...host, parent: system }, null, 2)}\n`); + return true; +} +/** At the end of a run: the system of every host of the bodies it wrote (a planet or companion on its host, a star bound to + * another), so the tree is whole before the first bake reads it. */ +export async function ensureHostSystems(root: string, bodies: readonly { readonly host: string; readonly planet: boolean }[], progress: (line: string) => void): Promise { + for (const host of new Set(bodies.map(body => body.host))) + if (await ensureHostSystem(root, host, bodies.some(body => body.host === host && body.planet))) progress(`${host}: its system package written, with ${host} inside it`); +} /** `files` with the package's descriptor naming `parent`, written after its id. */ export function withParent>(files: Files, id: string, parent: string): Files { const path = `src/objects/${id}/object.json`, { schema, id: own, parent: _drafted, ...rest } = JSON.parse(String(files.get(path))) as Record; diff --git a/packages/telescope-cli/src/new-object/package-parent.test.mts b/packages/telescope-cli/src/new-object/package-parent.test.mts new file mode 100644 index 00000000000..2a0e8848b23 --- /dev/null +++ b/packages/telescope-cli/src/new-object/package-parent.test.mts @@ -0,0 +1,34 @@ +/** The system a new host's bodies are inside is written before the host's first bake (package-parent.mts). */ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { resolve } from 'node:path'; +import test from 'node:test'; +import { ensureHostSystem } from './package-parent.mts'; + +test('a new host is moved inside a system written where it sat, with its own card, and a system already there is kept', async () => { + const root = await mkdtemp(resolve(tmpdir(), 'host-system-')), path = (id: string) => resolve(root, `src/objects/${id}/object.json`); + const read = async (id: string) => JSON.parse(await readFile(path(id), 'utf8')) as Record; + try { + const worldFrame = { referenceFrame: 'sun-icrf', originM: [1, 2, 3] }; + const catalog = { name: 'HD 1', classification: 'star', color: '#fff7fd', distanceAu: 43425864.2, description: 'HD 1 b crosses HD 1 as seen from Earth.', systemName: 'HD 1 system', order: 7 }; + await mkdir(resolve(root, 'src/objects/hd-1'), { recursive: true }); + await writeFile(path('hd-1'), JSON.stringify({ schema: 'cssearth-object@2', id: 'hd-1', parent: 'milky-way', type: 'star', properties: { catalog, worldFrame } })); + assert.equal(await ensureHostSystem(root, 'hd-1', true), true); + // As the bake's systems step writes a star's system with its bodies (site/build/prepare/system-packages.mts). + assert.deepEqual(await read('hd-1-system'), { schema: 'cssearth-object@2', id: 'hd-1-system', parent: 'milky-way', type: 'system', generator: 'site/build/prepare/system-packages.mts', + properties: { system: { host: 'hd-1' }, catalog: { name: 'HD 1 system', systemName: 'HD 1 system', classification: 'planetary-system', color: '#fff7fd', distanceAu: 43425864.2, + description: 'HD 1 b crosses HD 1 as seen from Earth.' }, worldFrame } }); + const host = await read('hd-1'); + assert.deepEqual([host.parent, host.properties.catalog, Object.keys(host).slice(0, 3)], ['hd-1-system', catalog, ['schema', 'id', 'parent']]); + assert.equal(await ensureHostSystem(root, 'hd-1', true), false, 'a system already there is kept'); + // A host with only a star bound to it makes a star system; a host that names no parent has no place for one. + await mkdir(resolve(root, 'src/objects/hd-2'), { recursive: true }); + await writeFile(path('hd-2'), JSON.stringify({ schema: 'cssearth-object@2', id: 'hd-2', parent: 'milky-way', type: 'star', properties: { catalog: { ...catalog, systemName: 'HD 2 system' }, worldFrame } })); + await ensureHostSystem(root, 'hd-2', false); + assert.equal((await read('hd-2-system')).properties.catalog.classification, 'star-system'); + await mkdir(resolve(root, 'src/objects/hd-3'), { recursive: true }); + await writeFile(path('hd-3'), JSON.stringify({ schema: 'cssearth-object@2', id: 'hd-3', type: 'star', properties: { catalog, worldFrame } })); + await assert.rejects(ensureHostSystem(root, 'hd-3', true), /src\/objects\/hd-3\/object\.json: the host of hd-3-system names no parent/u); + } finally { await rm(root, { recursive: true, force: true }); } +}); diff --git a/packages/telescope-cli/src/new-object/planet-datasets.mts b/packages/telescope-cli/src/new-object/planet-datasets.mts index 59afdb91ff4..02f6b76d6db 100644 --- a/packages/telescope-cli/src/new-object/planet-datasets.mts +++ b/packages/telescope-cli/src/new-object/planet-datasets.mts @@ -25,7 +25,7 @@ import { HOSTED_PLANET_STYLESHEET } from './new-hosted-planet.mts'; import { installPhaseCurveDataset, type PhaseCurveEntry } from './phase-curve-dataset.mts'; import { daysideLine, installDaysideDataset } from './thermal/dayside-dataset.mts'; import { WORKSPACE } from '@cssearth/telescope/node'; -import { installSimulationDataset, restoreSimulationField, simulationPaths, simulationRecipe, simulationRelease, type SimulationEntry } from './simulation/simulation-dataset.mts'; +import { installSimulationDataset, restoreSimulationField, restoreSimulationMember, simulationPaths, simulationRecipe, simulationRelease, type SimulationEntry } from './simulation/simulation-dataset.mts'; import { loadNetcdfLonLatField } from '@cssearth/bake/objects/raster'; export const EMISSION_COLUMNS = 'plntname,centralwavelng,bandwidth,especlipdep,especlipdeperr1,especlipdeperr2,especlipdeplim,espbritemp,espbritemperr1,espbritemperr2,espbritemplim,facility,instrument,plntreflink'; @@ -365,9 +365,11 @@ export async function rebuildExistingDatasets(root: string, ids: readonly string let promoted = false; for (const entry of simulations.get(id) ?? []) { const release = await simulationRelease(archive, id, names, entry); - const ranges = await restoreSimulationField(resolve(o, 'source'), id, entry, release, fetcher), paths = simulationPaths(entry); - if (ranges.fetched) progress(`${id}: ${ranges.fetched.toLocaleString('en-US')} bytes of ${entry.file} fetched from Zenodo record ${release.doi}`); - const field = await loadNetcdfLonLatField(resolve(o, 'source'), simulationRecipe(entry), new Map([[paths.head, ranges.head], [paths.field, ranges.field]])); + // A classic release is kept as two of its byte ranges; a NetCDF-4 file inside a ZIP release is kept whole. + const ranges = entry.member === undefined ? await restoreSimulationField(resolve(o, 'source'), id, entry, release, fetcher) : undefined, paths = simulationPaths(entry); + const fetched = ranges ? ranges.fetched : await restoreSimulationMember(files, id, resolve(o, 'source'), entry, release, fetcher); + if (fetched) progress(`${id}: ${fetched.toLocaleString('en-US')} bytes of ${entry.file} fetched from Zenodo record ${release.doi}`); + const field = await loadNetcdfLonLatField(resolve(o, 'source'), simulationRecipe(entry), new Map(ranges ? [[paths.head, ranges.head], [paths.field, ranges.field]] : [])); const installed = installSimulationDataset(files, id, descriptor.displayName, entry, release, field.report, ranges); promoted ||= installed.promoted; lines.push(`${id}: ${entry.dataset} dataset from the ${entry.model} simulation of ${entry.credit}, ${installed.minimum}-${installed.maximum} ${entry.displayUnits ?? entry.units}, ${release.license.name}${installed.promoted ? '; the page now opens on it' : ''}`); progress(lines.at(-1)!); diff --git a/packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts b/packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts index 8cd5df45b3b..39c5f2caed2 100644 --- a/packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts +++ b/packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts @@ -15,11 +15,17 @@ * * A model writes files of gigabytes, and one map is a few kilobytes of one. The release is never fetched whole: two range * requests bring its first bytes (the header and the coordinate variables) and the bytes of the selected grid, and the - * package keeps those two parts as exact bytes of the release, each declared with its range in the source manifest. */ + * package keeps those two parts as exact bytes of the release, each declared with its range in the source manifest. + * + * A release that is a ZIP archive holding a NetCDF-4 model file adds `member`, the file's path inside the archive. NetCDF-4 + * spreads its structure through the file, so that file is kept whole, outside git, and restored by the package's own + * acquisition step. Such an entry may add `isobar` ({ along, pressure, pressureUnits, at }) to read the field at one + * pressure, as a paper draws a model whose levels are heights (netcdf-isobar.ts). */ import { stat } from 'node:fs/promises'; import { Readable } from 'node:stream'; import { requireArray, requireFiniteNumber, requireRecord, requireString } from '@cssearth/core'; -import { NetcdfHeaderIncomplete, parseClassicNetcdfHeader, valueRange, type ByteRange, type NetcdfHeader } from '@cssearth/bake/objects/raster'; +import { executeAcquisition, parseAcquisitionPlan } from '@cssearth/bake/objects/acquisition'; +import { NetcdfHeaderIncomplete, parseClassicNetcdfHeader, parseIsobar, valueRange, type ByteRange, type Isobar, type NetcdfHeader } from '@cssearth/bake/objects/raster'; import { containedPath, publishPinnedSourceStream } from '@cssearth/bake/objects/sources'; import { assertRangeResponse, rangeRequestHeader } from '@cssearth/objects/node'; import type { Archive } from '../archives/archives.mts'; @@ -55,6 +61,10 @@ export interface SimulationEntry { readonly others?: { readonly text: string; readonly url: string }; /** The legend's limits, when the paper's own color scale is used; the released range rounded outward otherwise. */ readonly range?: readonly [number, number]; + /** When `file` is a ZIP archive: the path inside it of the NetCDF-4 model file, which is kept whole at `path`. */ + readonly member?: string; + /** The pressure the field is read at, for a model whose levels are not pressures; only with `member`. */ + readonly isobar?: Isobar; } /** The inferno ramp WASP-103 b's hand-made climate simulation draws. */ @@ -67,7 +77,7 @@ export function simulationEntry(value: unknown, label: string): SimulationEntry if (!words) throw new TypeError(`${label}.${key} is empty.`); return words; }; - const known = new Set(['id', 'dataset', 'label', 'quantity', 'record', 'file', 'path', 'variable', 'coordinates', 'select', 'units', 'displayUnits', 'longitudeZeroAt', 'model', 'credit', 'url', 'scenario', 'time', 'detected', 'undetected', 'observed', 'others', 'range']); + const known = new Set(['id', 'dataset', 'label', 'quantity', 'record', 'file', 'path', 'variable', 'coordinates', 'select', 'units', 'displayUnits', 'longitudeZeroAt', 'model', 'credit', 'url', 'scenario', 'time', 'detected', 'undetected', 'observed', 'others', 'range', 'member', 'isobar']); const unknown = Object.keys(input).filter(key => !known.has(key)); if (unknown.length) throw new TypeError(`${label}: unknown fields ${unknown.join(', ')}.`); const dataset = text('dataset'), record = text('record'), file = text('file'), path = text('path'), url = text('url'); @@ -82,6 +92,10 @@ export function simulationEntry(value: unknown, label: string): SimulationEntry // The kept part is named after the variable and the selection, so both are plain names. for (const [field, name] of [['variable', variable], ...Object.keys(select).map(dimension => ['select', dimension])] as const) if (!/^[A-Za-z0-9_]+$/u.test(name)) throw new TypeError(`${label}.${field} ${name}: a NetCDF name of letters, digits and underscores.`); + const member = input.member === undefined ? undefined : text('member'), isobar = input.isobar === undefined ? undefined : parseIsobar(input.isobar, `${label}.isobar`); + if (member !== undefined && !(/\.zip$/u.test(file) && /^[A-Za-z0-9_][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_][A-Za-z0-9._-]*)*$/u.test(member))) + throw new TypeError(`${label}.member ${member}: the path of the model file inside ${file}, which must then be a ZIP archive.`); + if (isobar && member === undefined) throw new TypeError(`${label}.isobar reads every level of the model, which the two kept parts of a classic release do not hold; it needs a NetCDF-4 file named by member.`); const scenario = text('scenario'), time = text('time'); if (/[.!?]$/u.test(scenario)) throw new TypeError(`${label}.scenario is the rest of the sentence "The run assumes ..."; leave out its final period.`); if (/[.!?]$/u.test(time)) throw new TypeError(`${label}.time is the rest of the sentence "The map is ..."; leave out its final period.`); @@ -102,7 +116,8 @@ export function simulationEntry(value: unknown, label: string): SimulationEntry return { dataset, label: text('label'), quantity: text('quantity'), record, file, path, variable, coordinates: { longitude: requireString(coordinates.longitude, `${label}.coordinates.longitude`), latitude: requireString(coordinates.latitude, `${label}.coordinates.latitude`) }, select: select as Record, units: text('units'), ...(input.displayUnits === undefined ? {} : { displayUnits: text('displayUnits') }), longitudeZeroAt: requireFiniteNumber(input.longitudeZeroAt, `${label}.longitudeZeroAt`), model: text('model'), credit: text('credit'), url, scenario, time, - detected: input.detected, ...(input.undetected === undefined ? {} : { undetected: text('undetected') }), ...(observed ? { observed } : {}), ...(others ? { others } : {}), ...(range ? { range } : {}) }; + detected: input.detected, ...(input.undetected === undefined ? {} : { undetected: text('undetected') }), ...(observed ? { observed } : {}), ...(others ? { others } : {}), ...(range ? { range } : {}), + ...(member === undefined ? {} : { member }), ...(isobar ? { isobar } : {}) }; } /** `--simulation entries.json`: the entries for bodies already in the tree, by body id. */ @@ -202,10 +217,26 @@ export async function restoreSimulationField(sourceRoot: string, id: string, ent return { ...ranges, fetched }; } -/** What the field reader is asked for: the entry's statements about the file, its two kept parts, and the tolerance WASP-103 b's table uses. */ +/** The step that takes a ZIP release's model file whole into the package, as the package's acquisition plan declares it. */ +const memberStep = (entry: SimulationEntry, release: SimulationRelease) => ({ kind: 'zip-member', groups: ['restore', 'refresh'], path: entry.path, url: release.fileUrl, member: entry.member }); + +/** The model file of a ZIP release in the package's source directory, taken by the acquisition step the package will declare + * for it, read with the plan and manifest already in `files`. A file already there is kept. Returns the bytes fetched. */ +export async function restoreSimulationMember(files: PackageFiles, id: string, sourceRoot: string, entry: SimulationEntry, release: SimulationRelease, fetcher: typeof fetch = fetch): Promise { + const here = await stat(containedPath(sourceRoot, entry.path)).then(info => info.size > 0, (error: NodeJS.ErrnoException) => { if (error.code === 'ENOENT') return false; throw error; }); + if (here) return 0; + const read = (path: string) => requireRecord(JSON.parse(String(files.get(`src/objects/${id}/source/${path}`))), path); + await executeAcquisition({ sourceRoot, group: 'restore', plan: parseAcquisitionPlan({ ...read('preparation/acquisition.json'), operations: [memberStep(entry, release)] }), + manifest: { schema: requireString(read('manifest.json').schema, 'manifest schema'), inputs: [{ path: entry.path, origin: release.fileUrl }], generatedIntermediates: [], documents: [] }, + // The acquisition's own transport names this project to Zenodo; a test passes its own. + ...(fetcher === fetch ? {} : { transport: { fetch: (url: string, init?: RequestInit) => fetcher(url, init) } }) }); + return release.bytes; +} + +/** What the field reader is asked for: the entry's statements about the file, its two kept parts or its whole model file, and the tolerance WASP-103 b's table uses. */ export function simulationRecipe(entry: SimulationEntry) { - const paths = simulationPaths(entry); - return { format: 'netcdf-lonlat-field', path: paths.head, field: paths.field, variable: entry.variable, coordinates: entry.coordinates, select: entry.select, + const paths = simulationPaths(entry), kept = entry.member === undefined ? { path: paths.head, field: paths.field } : { path: entry.path }; + return { format: 'netcdf-lonlat-field', ...kept, variable: entry.variable, coordinates: entry.coordinates, select: entry.select, ...(entry.isobar ? { isobar: entry.isobar } : {}), sourceUnits: entry.units, longitudeZeroAt: entry.longitudeZeroAt, coordinateToleranceDegrees: 0.005 }; } @@ -220,11 +251,12 @@ export interface FieldReport { readonly minimum: number; readonly maximum: numbe /** Add the dataset to the package in `files`. It becomes the default where the default was one color or the neutral shape; a * default that is a measured map stays. Returns the range drawn and whether the default changed. */ -export function installSimulationDataset(files: PackageFiles, id: string, name: string, entry: SimulationEntry, release: SimulationRelease, field: FieldReport, ranges: SimulationRanges) { +export function installSimulationDataset(files: PackageFiles, id: string, name: string, entry: SimulationEntry, release: SimulationRelease, field: FieldReport, ranges?: SimulationRanges) { + if ((ranges === undefined) !== (entry.member !== undefined)) throw new TypeError(`${id}, dataset ${entry.dataset}: a classic release is installed with the ranges of its two kept parts, a ZIP member without.`); const o = `src/objects/${id}`, s = `${o}/source`, read = (path: string) => requireRecord(JSON.parse(String(files.get(path))), path); const others = (list: unknown, where: string, key: string, value: string) => requireArray(list, where).filter(item => requireRecord(item, where)[key] !== value); const [minimum, maximum] = entry.range ?? roundedRange(field.minimum, field.maximum), number = (value: number) => String(Number(value.toPrecision(12))); - const units = entry.displayUnits ?? entry.units, paths = simulationPaths(entry); + const units = entry.displayUnits ?? entry.units, paths = simulationPaths(entry), readFrom = ranges ? paths.head : entry.path; if (field.minimum < minimum || field.maximum > maximum) throw new RangeError(`${id}, dataset ${entry.dataset}: ${entry.variable} in ${entry.file} runs from ${field.minimum} to ${field.maximum} ${entry.units}, outside the range ${minimum} to ${maximum} the entry gives (field range).`); const labels = [minimum, (minimum + maximum) / 2, maximum].map(number); @@ -238,7 +270,7 @@ export function installSimulationDataset(files: PackageFiles, id: string, name: outputLongitudeOrigin: 0, units, minimum, maximum, colors: INFERNO, labels, description: `Simulated ${quantity} from the ${entry.model} model, a published simulation and not a measurement.${caps}${gaps}`, title: `${entry.label} · ${entry.model} simulation`, sourceUrl: entry.url }; raster.surfaces = [...others(raster.surfaces, `${id} raster surfaces`, 'id', entry.dataset), - { id: entry.dataset, output: `${id}-surface-{id}{suffix}.webp`, thumbnail: `${id}-dataset-{id}.webp`, source: paths.head, falseColor: true, science }]; + { id: entry.dataset, output: `${id}-surface-{id}{suffix}.webp`, thumbnail: `${id}-dataset-{id}.webp`, source: readFrom, falseColor: true, science }]; files.set(`${s}/preparation/raster.json`, json(raster)); const descriptor = read(`${o}/object.json`), recipe = requireRecord(requireRecord(descriptor.properties, `${id} properties`).recipe, `${id} recipe`); const surface = requireRecord(requireArray(recipe.surfaces, `${id} recipe surfaces`)[0], `${id} recipe surface`); @@ -256,7 +288,8 @@ export function installSimulationDataset(files: PackageFiles, id: string, name: files.set(`${s}/content/object.json`, json(content)); const text = read(`${o}/text.json`); text.datasets = { ...requireRecord(text.datasets ?? {}, `${id} text datasets`), [entry.dataset]: { title: `${entry.model} simulation`, detail: 'Published simulation', - summary: `${entry.quantity} in a published model, in false color. It shows one scenario, not a measurement.${short ? ' Gray caps lack released samples.' : ''}` } }; + // Two sentences, the summary's budget, with or without the caps. + summary: `${entry.quantity} in a published model, in false color. ${short ? 'One scenario, not a measurement; gray caps lack samples.' : 'It shows one scenario, not a measurement.'}` } }; files.set(`${o}/text.json`, json(text)); // The two kept parts are exact bytes of the release, each one declared with its range and restored by one range request. @@ -264,15 +297,20 @@ export function installSimulationDataset(files: PackageFiles, id: string, name: const shared = { origin: release.fileUrl, productId: entry.file, version: release.doi, sourceUrl: release.recordUrl, credit: `${entry.credit}; ${entry.model} simulation`, displayCredit: `${entry.credit} · ${entry.model}`, license: release.license.name, licenseEvidence: [release.recordUrl, release.license.url] }; const kept = { redistribution: `Exact bytes of the release; ${release.license.name} with attribution.`, consumers: [consumer] }; + const title = `${entry.quantity} (${entry.variable}) of ${name} in the ${entry.model} simulation: ${release.title}`; manifest.inputs = [...requireArray(manifest.inputs, `${id} manifest inputs`).filter(item => ![input, grid].includes(String(requireRecord(item, `${id} manifest input`).id))), - { id: input, path: paths.field, range: ranges.field, ...shared, title: `${entry.quantity} (${entry.variable}) of ${name} in the ${entry.model} simulation: ${release.title}`, + ...(ranges ? [{ id: input, path: paths.field, range: ranges.field, ...shared, title, acquisition: `One range request to Zenodo record ${release.doi}: the bytes of ${entry.variable}${selection} in ${entry.file}, where the file's header puts them. The ${sizeText(release.bytes)} file is never fetched whole. A model output, not an observation.`, ...kept }, { id: grid, path: paths.head, range: ranges.head, ...shared, title: `Header and coordinate variables of ${entry.file}, the ${entry.model} simulation of ${name}`, - acquisition: `One range request to Zenodo record ${release.doi}: the first ${ranges.head.length.toLocaleString('en-US')} bytes of ${entry.file}, its header and its coordinate variables.`, ...kept }]; + acquisition: `One range request to Zenodo record ${release.doi}: the first ${ranges.head.length.toLocaleString('en-US')} bytes of ${entry.file}, its header and its coordinate variables.`, ...kept }] + // A NetCDF-4 model file out of a ZIP release: one input, the whole member, which git does not hold. + : [{ id: input, path: entry.path, ...shared, productId: entry.member, title, + acquisition: `Restored through source/preparation/acquisition.json from Zenodo record ${release.doi}: the member ${entry.member} of ${entry.file} (${sizeText(release.bytes)}), unchanged and whole, because a NetCDF-4 file spreads its structure through itself. A model output, not an observation.`, + redistribution: `Not redistributed in git; restored from Zenodo. ${release.license.name} with attribution.`, consumers: [consumer] }])]; files.set(`${s}/manifest.json`, json(manifest)); - const plan = read(`${s}/preparation/acquisition.json`); - plan.operations = [...requireArray(plan.operations, `${id} acquisition operations`).filter(item => ![paths.field, paths.head].includes(String(requireRecord(item, `${id} acquisition operation`).path))), - ...[paths.field, paths.head].map(target => ({ kind: 'download', groups: ['restore', 'refresh'], path: target, url: release.fileUrl }))]; + const plan = read(`${s}/preparation/acquisition.json`), targets = ranges ? [paths.field, paths.head] : [entry.path]; + plan.operations = [...requireArray(plan.operations, `${id} acquisition operations`).filter(item => !targets.includes(String(requireRecord(item, `${id} acquisition operation`).path))), + ...(ranges ? targets.map(target => ({ kind: 'download', groups: ['restore', 'refresh'], path: target, url: release.fileUrl })) : [memberStep(entry, release)])]; files.set(`${s}/preparation/acquisition.json`, json(plan)); bindInputs(files, id); // A body opens on the best dataset it has: the simulation takes the default from one color or the neutral shape, never from diff --git a/packages/telescope-cli/src/new-object/simulation/simulation-dataset.test.mts b/packages/telescope-cli/src/new-object/simulation/simulation-dataset.test.mts index 1a93cd1cbf4..77104b7f6e1 100644 --- a/packages/telescope-cli/src/new-object/simulation/simulation-dataset.test.mts +++ b/packages/telescope-cli/src/new-object/simulation/simulation-dataset.test.mts @@ -3,7 +3,8 @@ * reference library's small NetCDF fixture standing in for the released file, served by range as Zenodo serves one. No dataset * is committed by this test. */ import assert from 'node:assert/strict'; -import { cp, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import { cp, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; import test from 'node:test'; @@ -11,7 +12,7 @@ import { loadNetcdfLonLatField } from '@cssearth/bake/objects/raster'; import { WORKSPACE } from '@cssearth/telescope/node'; import type { Archive } from '../archives/archives.mts'; import { rebuildExistingDatasets } from '../planet-datasets.mts'; -import { installSimulationDataset, parseSimulationEntries, restoreSimulationField, roundedRange, simulationPaths, simulationRecipe, simulationRelease, simulationSurvey } from './simulation-dataset.mts'; +import { installSimulationDataset, parseSimulationEntries, restoreSimulationField, restoreSimulationMember, roundedRange, simulationPaths, simulationRecipe, simulationRelease, simulationSurvey } from './simulation-dataset.mts'; const id = 'trappist-1f', o = `src/objects/${id}`; const fixture = resolve(WORKSPACE, 'packages/bake/src/objects/raster/netcdf/fixtures/field-cdf2.nc'); @@ -108,7 +109,7 @@ test('the dataset is labelled as a model with its scenario, its range is the fil 'The map is the mean state the release holds. Nobody has detected an atmosphere on TRAPPIST-1f. JWST has neither shown nor excluded one (Glidden et al. 2025). Three other models of the same case differ from it. Gray caps: no released grid samples beyond 67.5° north or south. ' + 'The false color runs from 300 to 340 K; it is not what an eye would see. With shadows on, the star\'s light darkens the night half.'); assert.deepEqual(after('text.json').datasets['climate-model'], { title: 'ExoCAM simulation', detail: 'Published simulation', - summary: 'Surface temperature in a published model, in false color. It shows one scenario, not a measurement. Gray caps lack released samples.' }); + summary: 'Surface temperature in a published model, in false color. One scenario, not a measurement; gray caps lack samples.' }); // The two kept parts are declared with their ranges, exact bytes of the release, and restored by range. const [input, grid] = after('source/manifest.json').inputs.slice(-2), url = 'https://zenodo.org/records/5532765/files/field-cdf2.nc?download=1'; assert.deepEqual([input.id, input.path, input.range, input.origin, input.productId, input.version, input.license, input.licenseEvidence, input.consumers], @@ -162,6 +163,49 @@ test('two range requests bring the header with the coordinates and the selected } finally { await rm(sourceRoot, { recursive: true, force: true }); } }); +test('a NetCDF-4 model file inside a ZIP release is kept whole, and its field can be read at one pressure', async () => { + // The reference library's NetCDF-4 fixture stands in for the model file, zipped as a release zips one. + const netcdf4 = resolve(fixture, '../field-nc4.nc'), member = { file: 'release.zip', member: 'run/field-nc4.nc', path: 'science/wolf-2022/field-nc4.nc', variable: 'T', select: { time: 0 }, + isobar: { along: 'lev', pressure: 'P', pressureUnits: 'Pa', at: 150 }, quantity: 'Temperature at 1.5 millibar' }; + const entry = entryOf(member); + assert.deepEqual(simulationRecipe(entry), { format: 'netcdf-lonlat-field', path: 'science/wolf-2022/field-nc4.nc', variable: 'T', coordinates: { longitude: 'lon', latitude: 'lat' }, select: { time: 0 }, + isobar: { along: 'lev', pressure: 'P', pressureUnits: 'Pa', at: 150 }, sourceUnits: 'K', longitudeZeroAt: 180, coordinateToleranceDegrees: 0.005 }); + assert.throws(() => entryOf({ ...member, file: 'field-nc4.nc' }), /member run\/field-nc4\.nc: the path of the model file inside field-nc4\.nc, which must then be a ZIP archive/u); + assert.throws(() => entryOf({ ...member, member: '../field-nc4.nc' }), /the path of the model file inside release\.zip/u); + assert.throws(() => entryOf({ isobar: member.isobar }), /isobar reads every level of the model, which the two kept parts of a classic release do not hold/u); + const scratch = await mkdtemp(resolve(tmpdir(), 'simulation-member-')), sourceRoot = resolve(scratch, 'source'), archives = resolve('.local/source-archives'); + const cached = async () => new Set(await readdir(archives).catch(() => [])); + const before = await cached(); + try { + await cp(netcdf4, resolve(scratch, 'run/field-nc4.nc')); + execFileSync('zip', ['-q', 'release.zip', 'run/field-nc4.nc'], { cwd: scratch }); + const zip = await readFile(resolve(scratch, 'release.zip')), asked: string[] = []; + const release = await simulationRelease(archive(await record(exocam => { exocam.files = [{ key: 'release.zip', size: zip.length }, ...exocam.files]; })), id, ['TRAPPIST-1f'], entry); + const files = new Map(await Promise.all(PACKAGE.map(async path => [`${o}/${path}`, await readFile(resolve(WORKSPACE, o, path), 'utf8')] as const))); + const zenodo = (async (url: unknown) => { asked.push(String(url)); return new Response(new Uint8Array(zip)); }) as typeof fetch; + // The archive is fetched once and the member taken out of it by the step the package then declares. + assert.equal(await restoreSimulationMember(files, id, sourceRoot, entry, release, zenodo), zip.length); + assert.deepEqual(asked, [release.fileUrl]); + assert.deepEqual(await readFile(resolve(sourceRoot, entry.path)), await readFile(netcdf4)); + assert.equal(await restoreSimulationMember(files, id, sourceRoot, entry, release, (async () => { throw new Error('a file already there is kept'); }) as typeof fetch), 0); + const field = await loadNetcdfLonLatField(sourceRoot, simulationRecipe(entry), new Map()); + // T = 1000 + 10 i + j on the first level and 100 K less on the second; 150 Pa lies between them where the first holds 1000 Pa. + assert.ok(Math.abs(field.sample(22.5 - 180, -67.5)! - (1000 - 100 * Math.log(0.15) / Math.log(0.1))) < 1e-9); + assert.throws(() => installSimulationDataset(files, id, 'TRAPPIST-1f', entry, release, field.report, RANGES), /a classic release is installed with the ranges of its two kept parts, a ZIP member without/u); + installSimulationDataset(files, id, 'TRAPPIST-1f', entry, release, field.report); + const after = (path: string) => JSON.parse(String(files.get(`${o}/${path}`))) as Record, surface = after('source/preparation/raster.json').surfaces.at(-1); + assert.deepEqual([surface.source, surface.science.path, surface.science.field, surface.science.isobar], ['science/wolf-2022/field-nc4.nc', 'science/wolf-2022/field-nc4.nc', undefined, member.isobar]); + const input = after('source/manifest.json').inputs.at(-1); + assert.deepEqual([input.id, input.path, input.range, input.origin, input.productId], ['trappist-1f-climate-model-simulation', 'science/wolf-2022/field-nc4.nc', undefined, release.fileUrl, 'run/field-nc4.nc']); + assert.equal(input.acquisition, `Restored through source/preparation/acquisition.json from Zenodo record 10.5281/zenodo.5532765: the member run/field-nc4.nc of release.zip (${(zip.length / 1000).toFixed(1)} kB), unchanged and whole, because a NetCDF-4 file spreads its structure through itself. A model output, not an observation.`); + assert.equal(input.redistribution, 'Not redistributed in git; restored from Zenodo. CC BY 4.0 with attribution.'); + assert.deepEqual(after('source/preparation/acquisition.json').operations.at(-1), { kind: 'zip-member', groups: ['restore', 'refresh'], path: 'science/wolf-2022/field-nc4.nc', url: release.fileUrl, member: 'run/field-nc4.nc' }); + } finally { + for (const name of await cached()) if (!before.has(name)) await rm(resolve(archives, name), { force: true }); + await rm(scratch, { recursive: true, force: true }); + } +}); + test('new-object --simulation writes the package of a body already in the tree, and refuses what the file does not hold', async () => { const root = await mkdtemp(resolve(tmpdir(), 'simulation-tree-')); try { diff --git a/public/navigation/body-cd-34-14724-b@2x.webp b/public/navigation/body-cd-34-14724-b@2x.webp new file mode 100644 index 00000000000..572f3b82cf5 Binary files /dev/null and b/public/navigation/body-cd-34-14724-b@2x.webp differ diff --git a/public/navigation/body-markers-09@2x.webp b/public/navigation/body-markers-09@2x.webp index 01fe0b9e71a..c09d84617e5 100644 Binary files a/public/navigation/body-markers-09@2x.webp and b/public/navigation/body-markers-09@2x.webp differ diff --git a/public/navigation/body-markers-10@2x.webp b/public/navigation/body-markers-10@2x.webp index 3b56fe72e11..891288120cf 100644 Binary files a/public/navigation/body-markers-10@2x.webp and b/public/navigation/body-markers-10@2x.webp differ diff --git a/public/navigation/body-markers-11@2x.webp b/public/navigation/body-markers-11@2x.webp index 612c1fca494..bcfddd1b261 100644 Binary files a/public/navigation/body-markers-11@2x.webp and b/public/navigation/body-markers-11@2x.webp differ diff --git a/public/navigation/body-markers-12@2x.webp b/public/navigation/body-markers-12@2x.webp index 0b39ed670f0..76a21976f3d 100644 Binary files a/public/navigation/body-markers-12@2x.webp and b/public/navigation/body-markers-12@2x.webp differ diff --git a/public/navigation/body-markers-13@2x.webp b/public/navigation/body-markers-13@2x.webp index c224228f256..b0cdf52927b 100644 Binary files a/public/navigation/body-markers-13@2x.webp and b/public/navigation/body-markers-13@2x.webp differ diff --git a/public/navigation/body-markers-14@2x.webp b/public/navigation/body-markers-14@2x.webp index d3ec4327f84..30f2cf502a8 100644 Binary files a/public/navigation/body-markers-14@2x.webp and b/public/navigation/body-markers-14@2x.webp differ diff --git a/public/navigation/body-markers-15@2x.webp b/public/navigation/body-markers-15@2x.webp index 523e8d41640..baa73dfc32e 100644 Binary files a/public/navigation/body-markers-15@2x.webp and b/public/navigation/body-markers-15@2x.webp differ diff --git a/public/navigation/body-markers-16@2x.webp b/public/navigation/body-markers-16@2x.webp index 527c7eed868..80519f3ec88 100644 Binary files a/public/navigation/body-markers-16@2x.webp and b/public/navigation/body-markers-16@2x.webp differ diff --git a/public/navigation/body-markers-17@2x.webp b/public/navigation/body-markers-17@2x.webp index 32887866016..4af7f37b2d4 100644 Binary files a/public/navigation/body-markers-17@2x.webp and b/public/navigation/body-markers-17@2x.webp differ diff --git a/public/navigation/body-wasp-94-a-b@2x.webp b/public/navigation/body-wasp-94-a-b@2x.webp new file mode 100644 index 00000000000..8d4d1db0554 Binary files /dev/null and b/public/navigation/body-wasp-94-a-b@2x.webp differ diff --git a/public/navigation/body-wasp-94-a@2x.webp b/public/navigation/body-wasp-94-a@2x.webp new file mode 100644 index 00000000000..7878eb68c11 Binary files /dev/null and b/public/navigation/body-wasp-94-a@2x.webp differ diff --git a/public/navigation/cd-34-14724-b-context.webp b/public/navigation/cd-34-14724-b-context.webp new file mode 100644 index 00000000000..74d15f330cc Binary files /dev/null and b/public/navigation/cd-34-14724-b-context.webp differ diff --git a/public/navigation/search/cd-34-14724-b@2x.webp b/public/navigation/search/cd-34-14724-b@2x.webp new file mode 100644 index 00000000000..62e46e4508c Binary files /dev/null and b/public/navigation/search/cd-34-14724-b@2x.webp differ diff --git a/public/navigation/search/wasp-94-a-b@2x.webp b/public/navigation/search/wasp-94-a-b@2x.webp new file mode 100644 index 00000000000..f4d8df62f28 Binary files /dev/null and b/public/navigation/search/wasp-94-a-b@2x.webp differ diff --git a/public/navigation/search/wasp-94-a@2x.webp b/public/navigation/search/wasp-94-a@2x.webp new file mode 100644 index 00000000000..0438a890416 Binary files /dev/null and b/public/navigation/search/wasp-94-a@2x.webp differ diff --git a/public/navigation/wasp-94-a-b-context.webp b/public/navigation/wasp-94-a-b-context.webp new file mode 100644 index 00000000000..438936fb209 Binary files /dev/null and b/public/navigation/wasp-94-a-b-context.webp differ diff --git a/public/navigation/wasp-94-a-context.webp b/public/navigation/wasp-94-a-context.webp new file mode 100644 index 00000000000..bf9aa53cd4e Binary files /dev/null and b/public/navigation/wasp-94-a-context.webp differ diff --git a/src/objects/cd-34-14724-b/NOTICE.md b/src/objects/cd-34-14724-b/NOTICE.md new file mode 100644 index 00000000000..da5abf071ad --- /dev/null +++ b/src/objects/cd-34-14724-b/NOTICE.md @@ -0,0 +1,9 @@ +# CD-34 14724B credits + +Radius, mass and temperature: Radius 1.431 +/- 0.068 solar radii from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the radius of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467); Mass 1.13 +/- 0.149 solar masses from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the mass of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467); temperature from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82). + +Color: Gaia DR3 XP spectrum, source 6780546169633474944, through the CIE 1931 2° color-matching functions (CIE 2019, CC BY-SA 4.0, doi:10.25039/CIE.DS.xvudnb9b). + +Limb darkening: Claret & Bloemen (2011), A&A 529, A75, via VizieR J/A+A/529/A75. + +Placement: Gaia DR3 source 6780546169633474944: position, parallax, proper motion and radial velocity. This work has made use of data from the European Space Agency (ESA) mission Gaia, processed by the Gaia Data Processing and Analysis Consortium (DPAC). Identifiers: SIMBAD, CDS, Strasbourg. diff --git a/src/objects/cd-34-14724-b/README.md b/src/objects/cd-34-14724-b/README.md new file mode 100644 index 00000000000..ec9d3cd0abf --- /dev/null +++ b/src/objects/cd-34-14724-b/README.md @@ -0,0 +1,24 @@ +# CD-34 14724B + +## Sources + +CD-34 14724B shares its motion through space with WASP-94 A, 3,171 AU away, so the two are a bound pair. Both are placed where Gaia measures them. The introduction is generated from El-Badry, Rix & Heintz (2021), MNRAS 506, 2269's published values; the sections below are the data's own. + +**Star.** Placement: Gaia DR3 source 6780546169633474944, parallax 4.721 ± 0.016 mas (211.83 pc). Radius 1.431 +/- 0.068 solar radii from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the radius of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467). Mass 1.13 +/- 0.149 solar masses from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the mass of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467). Temperature 6,049.7 K from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82). log g 4.18 from the mass and radius. + +**Color.** Gaia DR3 XP spectrum, source 6780546169633474944, through the CIE 1931 2° observer: #fff5f8. Routes tried in order: stis-ngsl: no HD number in SIMBAD; pulkovo: no HR number; kiehling: no HR number; kharitonov: no HR number; burnashev: no HR (BS) number; gaia-xp: used. + +**Limb.** The disc is dimmed toward the limb by the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,050 K and log g 4.18 (u1 0.405, u2 0.291): a model, because no fit of this star's limb is used. + +## Evidence + +Generated 2026-10-05 by [new-object-cli.mts](../../../packages/telescope-cli/src/new-object/new-object-cli.mts) from Gaia DR3, SIMBAD and the archives named above; each choice was read with the dataset's own reader. + + +## Known problems + +- **Assumptions of the frame.** The axis's position angle and the rotation phase are conventions. +- **Model limb.** The limb darkening is a model atmosphere at the catalogued temperature and gravity, not a measurement of this star. +- **Not shown.** CD-34 14724B's orbit around WASP-94 A is not measured; both stars are placed at their Gaia DR3 positions, which is where they are. + +[Investigation ledger](investigations.json) · [Inputs](source/manifest.json) · [Preparation](source/preparation) · [Delivered files](inventory.json) · [Credits](NOTICE.md) diff --git a/src/objects/cd-34-14724-b/inventory.json b/src/objects/cd-34-14724-b/inventory.json new file mode 100644 index 00000000000..481ce1e8700 --- /dev/null +++ b/src/objects/cd-34-14724-b/inventory.json @@ -0,0 +1,125 @@ +{ + "schema": "cssearth-inventory@1", + "assets": [ + { + "location": "public", + "filename": "cd-34-14724-b-arrival.webp", + "bytes": 26860, + "sha256": "b2b7a4785710c089fb62f156e4e81595533cc99a01fbd4ee9aeb26fbe34b5101" + }, + { + "location": "public", + "filename": "cd-34-14724-b-billboard.webp", + "bytes": 4710, + "sha256": "9a4a02dc80f2032a7708ffcf44ea9469bbf50e207aea726a34b657717a068051" + }, + { + "location": "public", + "filename": "cd-34-14724-b-dataset-color.webp", + "bytes": 44, + "sha256": "90f99c0ff48876bd531d1bd8afeddcd6cbf724c10fbd41e1160b856d93884785" + }, + { + "location": "public", + "filename": "cd-34-14724-b-limb-color@2x.webp", + "bytes": 97468, + "sha256": "8264d94cf79f9e6df733c4f347aebbf1496446dd05444d4618a1fd60dbebffa2" + }, + { + "location": "public", + "filename": "cd-34-14724-b-poles-color@2x.webp", + "bytes": 1340, + "sha256": "5a27d81ed81897fe8ebba5c7e5c7443279df4f9adb7013532bfa7535b4946bc3" + }, + { + "location": "public", + "filename": "cd-34-14724-b-surface-color@2x.webp", + "bytes": 38, + "sha256": "f0225ef7e394c9172dbccde2cd7b4ecc9b8b1bd8907b3eed1ad575a88307222f" + }, + { + "location": "prepared", + "filename": "arrival-billboard.json", + "bytes": 389, + "sha256": "1ab8a48b392e34d4c21b6c2d1b9d0fd78eac7799a28947690d48eb7602c74c1e" + }, + { + "location": "prepared", + "filename": "assets.json", + "bytes": 1723, + "sha256": "58411df7f636ea9f75488205a701a6797113c08fcd7894b18189458560bdc489" + }, + { + "location": "prepared", + "filename": "authored-preparation.json", + "bytes": 666, + "sha256": "513909e83f98c6e61be5204757493792aec1cac1ab5ef269d713ceff6dff41fa" + }, + { + "location": "prepared", + "filename": "content.json", + "bytes": 1250, + "sha256": "f9be7f33d4b5efcc75c371086e669e6a7d848bccd474c3ca55e43cd1eb220d07" + }, + { + "location": "prepared", + "filename": "controls.json", + "bytes": 376, + "sha256": "7fedbd35c6b901bf526d847c5d9166a8fc1c0ca969f800366288e753dc21f467" + }, + { + "location": "prepared", + "filename": "datasets.json", + "bytes": 1142, + "sha256": "509609f53f1e273bce550cad97e54b10e62a5a1241281e79006ae8fa2def8f77" + }, + { + "location": "prepared", + "filename": "minimaps.json", + "bytes": 101, + "sha256": "087c2291c1fc076eb71a6d0d7ff5d5b77074bd9ac9b4e63929c77733a8528617" + }, + { + "location": "prepared", + "filename": "minimaps/color.webp", + "bytes": 428, + "sha256": "08e467b527566a5f4440897fb80bea3f23093bdd3733addca32d3427fa65027c" + }, + { + "location": "prepared", + "filename": "runtime.json", + "bytes": 249614, + "sha256": "12411760493ea7cbb3ccf50e22c9b2f2c6d0667241c240fe2b6d381027410e48" + }, + { + "location": "prepared", + "filename": "scene.json", + "bytes": 395640, + "sha256": "10346f4b77e32307717dd3345e5825d7777974d7a86b04d9c2574f667b35e2e7" + }, + { + "location": "prepared", + "filename": "sky.json", + "bytes": 1207, + "sha256": "be9c5222d19f07b3f11def6596444794db83a8767e997893072164bfb9354599" + }, + { + "location": "prepared", + "filename": "sun.json", + "bytes": 5, + "sha256": "38e0b9de817f645c4bec37c0d4a3e58baecccb040f5718dc069a72c7385a0bed" + }, + { + "location": "prepared", + "filename": "text.json", + "bytes": 1125, + "sha256": "b00282ba875dbb9247cddba0876d6622cc3cd4e43230b7f36acd344d91a6feed" + }, + { + "location": "prepared", + "filename": "world-navigation.json", + "bytes": 1176, + "sha256": "0df7674798514a0821d07e8a1f369c0fc2d67b874aea42964708be45dca3eb0a" + } + ] +} diff --git a/src/objects/cd-34-14724-b/investigations.json b/src/objects/cd-34-14724-b/investigations.json new file mode 100644 index 00000000000..f8528bc2d98 --- /dev/null +++ b/src/objects/cd-34-14724-b/investigations.json @@ -0,0 +1,42 @@ +{ + "schema": "cssearth-investigation-ledger@1", + "objectId": "cd-34-14724-b", + "entries": [ + { + "id": "placement", + "subject": "Placement", + "status": "included", + "finding": "Gaia DR3 source 6780546169633474944: position at J2016.0. Distance: Gaia DR3 parallax 4.7208 +/- 0.0165 mas (same row, RUWE 0.81): 211.83 pc, no zero-point correction. Proper motion: Gaia DR3 (same row): 26.191, -44.702 mas/yr. Radial velocity from the same row.", + "evidence": [ + "https://gea.esac.esa.int/archive/" + ] + }, + { + "id": "radius-mass-and-temperature", + "subject": "Radius, mass and temperature", + "status": "included", + "finding": "Radius 1.431 +/- 0.068 solar radii from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the radius of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467). Mass 1.13 +/- 0.149 solar masses from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the mass of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467). Temperature 6049.7 +/- 126.5 K from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82).", + "evidence": [ + "https://doi.org/10.3847/1538-3881/ab3467" + ] + }, + { + "id": "color", + "subject": "Color", + "status": "included", + "finding": "Gaia DR3 XP spectrum, source 6780546169633474944, through the CIE 1931 2-degree observer: #fff5f8. Routes tried in order: stis-ngsl: no HD number in SIMBAD; pulkovo: no HR number; kiehling: no HR number; kharitonov: no HR number; burnashev: no HR (BS) number; gaia-xp: used.", + "evidence": [ + "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3" + ] + }, + { + "id": "limb-darkening", + "subject": "Limb darkening", + "status": "included", + "finding": "The disc is dimmed toward the limb by the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,050 K and log g 4.18 (u1 0.405, u2 0.291): a model, because no fit of this star's limb is used.", + "evidence": [ + "https://vizier.cds.unistra.fr/viz-bin/VizieR?-source=J/A+A/529/A75" + ] + } + ] +} diff --git a/src/objects/cd-34-14724-b/object.json b/src/objects/cd-34-14724-b/object.json new file mode 100644 index 00000000000..35ebfb758e5 --- /dev/null +++ b/src/objects/cd-34-14724-b/object.json @@ -0,0 +1,144 @@ +{ + "schema": "cssearth-object@2", + "id": "cd-34-14724-b", + "parent": "wasp-94-a-system", + "type": "layered-body", + "properties": { + "preparation": { + "schema": "cssearth-object-preparation@1", + "label": "CD-34 14724B", + "steps": [ + "verify-sources", + "assets", + "panel-content", + "datasets", + "starfield", + "scene", + "controls", + "presentation", + "runtime-assets" + ] + }, + "recipe": { + "schema": "cssearth-authored-object@2", + "surfaces": [ + { + "id": "body", + "source": "geometry", + "projection": "equirectangular", + "datasets": [ + { + "id": "color", + "source": "content", + "material": "emission" + } + ] + } + ], + "shape": { + "kind": "sphere", + "radiusKm": 995546.7 + }, + "materials": [ + { + "id": "emission", + "source": "raster", + "model": "emissive" + } + ], + "sources": [ + { + "id": "raster", + "path": "source/preparation/raster.json" + }, + { + "id": "geometry", + "path": "source/preparation/geometry.json" + }, + { + "id": "presentation", + "path": "source/preparation/presentation.json" + }, + { + "id": "content", + "path": "source/content/object.json" + }, + { + "id": "solar-system", + "path": "source/presentation/solar-system.json" + }, + { + "id": "rotation", + "path": "source/preparation/rotation.json" + }, + { + "id": "navigation", + "path": "source/preparation/navigation.json" + }, + { + "id": "acquisition", + "path": "source/preparation/acquisition.json" + } + ], + "emission": { + "source": "raster", + "material": "emission" + } + }, + "page": { + "stylesheets": [ + "src/renderers/css/styles/body-surfaces.css", + "src/renderers/css/styles/cd-34-14724-b-surfaces.css" + ], + "metadata": { + "url": "prepared/page.json" + } + }, + "catalog": { + "name": "CD-34 14724B", + "aliases": [ + "WASP-94 B" + ], + "classification": "star", + "color": "#fff5f8", + "distanceAu": 43692981.5, + "description": "CD-34 14724B is a star of 6,050 K, bound to WASP-94 A 3,171 AU away.", + "systemName": "WASP-94 system", + "order": 5780, + "context": { + "order": 5777 + } + }, + "worldFrame": { + "referenceFrame": "sun-icrf", + "epochJdTt": 2461286.5, + "originM": [ + 3743855420009231000, + -3905640046247194600, + -3667934930387447300 + ], + "presentationToReference": [ + 0.5727731573923773, + -0.3883170603718777, + -0.7219008039854995, + -0.5975246183339779, + 0.4050988783992409, + -0.6919965528848475, + -0.5611552732137526, + -0.8277105528769224, + -7.603455654671145e-7 + ], + "orbitUpReference": [ + 0.3883170603718777, + -0.4050988783992409, + 0.8277105528769224 + ], + "metersPerUnit": 3211440.9677419355, + "bodyRadiusM": 995546700 + } + }, + "prepared": { + "format": "cssearth-css-object@5", + "url": "prepared/object.json" + } +} diff --git a/src/objects/cd-34-14724-b/source/content/object.json b/src/objects/cd-34-14724-b/source/content/object.json new file mode 100644 index 00000000000..203326fb953 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/content/object.json @@ -0,0 +1,81 @@ +{ + "schema": "cssearth-object-content@2", + "version": 1, + "id": "cd-34-14724-b", + "displayName": "CD-34 14724B", + "panel": { + "facts": [ + { + "id": "radius", + "label": "Radius", + "value": "1.4 solar radii", + "source": { + "catalogueId": "vizier-iv-39-tic82-hd-189733-companion", + "url": "https://doi.org/10.3847/1538-3881/ab3467", + "label": "Stassun et al. (2019), The Astronomical Journal 158 138", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "radiusKm; radiusSource" + } + }, + { + "id": "distance", + "label": "Distance from the Sun", + "value": "212 parsecs", + "source": { + "catalogueId": "gaia-dr3-cd-34-14724-b", + "url": "https://gea.esac.esa.int/archive/", + "label": "Gaia DR3 parallax", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "distanceParsecs; distanceSource" + } + } + ], + "moreFacts": [] + }, + "datasets": { + "titleKey": "datasets", + "defaultDataset": "color", + "controls": [ + { + "id": "color", + "label": "Color", + "qualification": "Color from Gaia's spectrum of the star; darkening toward the edge from a model atmosphere. The disc itself is unresolved.", + "thumbnail": "cd-34-14724-b-dataset-color.webp", + "surface": "cd-34-14724-b-surface-color@2x.webp", + "poles": "cd-34-14724-b-poles-color@2x.webp", + "source": { + "id": "cd-34-14724-b-stellar-color", + "path": "../manifest.json", + "url": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3" + }, + "falseColor": false, + "notes": "The color of the star's Gaia DR3 BP/RP spectrum. The darkening toward the edge is the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,050 K and log g 4.18 (u1 0.405, u2 0.291): a model, because no fit of this star's limb is used. The spin axis's direction on the sky is a display convention." + } + ] + }, + "settings": { + "titleKey": "settings", + "controls": [] + }, + "charts": [], + "resources": [ + { + "label": "Research", + "role": "facts", + "description": "El-Badry, Rix & Heintz (2021), MNRAS 506, 2269", + "href": "https://arxiv.org/abs/2101.05282" + } + ], + "provenance": { + "editorial": { + "url": "https://arxiv.org/abs/2101.05282", + "credit": "El-Badry, Rix & Heintz (2021), MNRAS 506, 2269" + }, + "physical": { + "path": "../../../../../packages/astronomy/data/bodies/cd-34-14724-b.json", + "credit": "Stassun et al. radius; Gaia DR3 astrometry; no measured rotation axis (display convention)" + } + } +} diff --git a/src/objects/cd-34-14724-b/source/manifest.json b/src/objects/cd-34-14724-b/source/manifest.json new file mode 100644 index 00000000000..f472409a818 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/manifest.json @@ -0,0 +1,232 @@ +{ + "schema": "cssearth-authoritative-sources@3", + "inputs": [ + { + "id": "cd-34-14724-b-observational-measurements", + "path": "measurements.json", + "origin": "https://arxiv.org/abs/2101.05282", + "credit": "El-Badry, Rix & Heintz (2021), MNRAS 506, 2269", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Transcribed published measurements with their sources", + "redistribution": "Factual parameter transcription only; no paper figures", + "consumers": [ + "shape-model" + ], + "sourceBinding": { + "kind": "local", + "reason": "Measurements transcribed in this package with their sources; repinned when edited." + } + }, + { + "id": "cd-34-14724-b-preparation-raster", + "path": "preparation/raster.json", + "origin": "Repository-authored raster recipe: the shared neutral gray on the reference sphere, transparent plates", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "assets", + "datasets" + ] + }, + { + "id": "cd-34-14724-b-preparation-geometry", + "path": "preparation/geometry.json", + "origin": "Repository-authored CSS geometry profile: 248-unit sphere, 16 x 32 leaves, emissive material", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene", + "presentation" + ] + }, + { + "id": "cd-34-14724-b-preparation-presentation", + "path": "preparation/presentation.json", + "origin": "Repository-authored presentation profile: emissive mode", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "presentation" + ] + }, + { + "id": "cd-34-14724-b-physical-solar-system-recipe", + "path": "presentation/solar-system.json", + "origin": "Repository-authored scene recipe: published radius, camera plan", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene" + ] + }, + { + "id": "cd-34-14724-b-gaia-xp", + "path": "photometry/gaia-dr3-xp-sampled.csv", + "origin": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; De Angeli et al. (2023), A&A 674, A2; Montegriffo et al. (2023), A&A 674, A3", + "license": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "licenseEvidence": [ + "https://www.cosmos.esa.int/web/gaia-users/credits" + ], + "acquisition": "Download from ESA Gaia DataLink in source/preparation/acquisition.json: the externally calibrated BP/RP sampled mean spectrum of source_id 6780546169633474944, 343 samples from 336 to 1020 nm.", + "redistribution": "One archived spectrum or catalogue file, unchanged, with citation", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-cd-34-14724-b-gaia-xp", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + }, + { + "id": "cd-34-14724-b-stellar-color", + "path": "photometry/stellar-color.json", + "origin": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; De Angeli et al. (2023), A&A 674, A2; Montegriffo et al. (2023), A&A 674, A3; CIE 1931 2° observer", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Authored method record: names the archived spectrum, how it is read and the color computation applied", + "redistribution": "Method record only", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "local", + "reason": "Project-authored color recipe naming the Gaia DR3 spectrum bound above; repinned when edited." + } + }, + { + "id": "cd-34-14724-b-claret-2011-limb-darkening", + "path": "photometry/claret-2011-v-quadratic.tsv", + "origin": "https://vizier.cds.unistra.fr/viz-bin/asu-tsv", + "credit": "Claret & Bloemen (2011), A&A 529, A75 (VizieR J/A+A/529/A75)", + "license": "VizieR catalogue data, public with citation of the paper and CDS", + "licenseEvidence": [ + "https://cds.unistra.fr/vizier-org/licences_vizier.html" + ], + "acquisition": "VizieR request in source/preparation/acquisition.json: the quadratic Johnson V coefficients of the grid nodes at 6000..6250 K, log g 4..4.5, solar metallicity.", + "redistribution": "Catalogue rows retained unchanged with their citation.", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-cd-34-14724-b-claret-2011-limb-darkening", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + }, + { + "id": "cd-34-14724-b-gaia-dr3-source", + "path": "photometry/gaia-dr3-source.csv", + "origin": "https://gea.esac.esa.int/tap-server/tap/sync", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26 (FLAME)", + "license": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "licenseEvidence": [ + "https://www.cosmos.esa.int/web/gaia-users/credits" + ], + "acquisition": "Gaia Archive TAP query in source/preparation/acquisition.json: the gaia_source row of source_id 6780546169633474944 (position, parallax, proper motion, radial velocity) with its FLAME mass and radius.", + "redistribution": "One catalogue row, retained unchanged with its credit.", + "consumers": [ + "placement" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-cd-34-14724-b-gaia-dr3-source", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + } + ], + "generatedIntermediates": [ + { + "id": "limb-darkened-disc-context-marker", + "path": "presentation/context.png", + "origin": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "credit": "The color dataset as a disc, dimmed toward the limb by its model law; rendered by packages/telescope-cli/src/source-authoring/context-markers.mts", + "license": "Project-authored display derivative.", + "consumers": [ + "navigation" + ], + "recipe": { + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "inputs": [ + "cd-34-14724-b-stellar-color", + "cd-34-14724-b-gaia-xp", + "cd-34-14724-b-claret-2011-limb-darkening", + "cd-34-14724-b-preparation-raster" + ] + }, + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "sourceBinding": { + "kind": "local", + "reason": "The color dataset rendered as a disc with its limb darkening; `context-markers.mts --check` recomputes it." + } + } + ], + "documents": [ + { + "path": "content/object.json", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored factsheet, dataset recipe and legend." + } + }, + { + "path": "preparation/acquisition.json" + }, + { + "path": "preparation/navigation.json" + }, + { + "path": "preparation/rotation.json" + }, + { + "path": "preparation/new-object.json", + "sourceBinding": { + "kind": "local", + "reason": "The spec telescope new-object generated this package from, with the order it was given; `telescope new-object --refresh` regenerates from it." + } + } + ] +} diff --git a/src/objects/cd-34-14724-b/source/measurements.json b/src/objects/cd-34-14724-b/source/measurements.json new file mode 100644 index 00000000000..0637bccbe9b --- /dev/null +++ b/src/objects/cd-34-14724-b/source/measurements.json @@ -0,0 +1,18 @@ +{ + "schema": "cssearth-uniform-disc-star@1", + "id": "cd-34-14724-b", + "angularDiameterMas": 0.06, + "angularDiameterSource": "Computed here from the record's radius and distance: 2 x 995,547 km at 211.83 pc = 0.06 mas. No interferometric diameter of this star is used.", + "distanceParsecs": 211.82964258100284, + "distanceSource": "Gaia DR3 parallax 4.7208 +/- 0.0165 mas (same row, RUWE 0.81): 211.83 pc, no zero-point correction", + "radiusKm": 995546.7, + "radiusSource": "Radius 1.431 +/- 0.068 solar radii from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the radius of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467): 995,547 km at 695,700 km per solar radius. Mass 1.13 +/- 0.149 solar masses from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the mass of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467); GM is that mass times the JPL solar GM. Temperature 6049.7 +/- 126.5 K from Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467). presentationUp: the display axis is a sky-plane convention; the spin axis's position angle on the sky is not measured.", + "effectiveTemperatureK": 6049.7, + "effectiveTemperatureSource": "Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82) (https://doi.org/10.3847/1538-3881/ab3467)", + "surfaceGravityLogg": 4.18, + "surfaceGravitySource": "log g from the mass and radius in packages/astronomy/data/bodies/cd-34-14724-b.json (sources in its physicalNotes), log10(GM/R^2) in cgs: 4.180, rounded to two decimals", + "shape": { + "kind": "uniform-disc-sphere", + "qualification": "A sphere at the published radius, colored from the star's Gaia DR3 BP/RP spectrum (photometry/stellar-color.json); no image of the photosphere exists." + } +} diff --git a/src/objects/cd-34-14724-b/source/photometry/claret-2011-v-quadratic.tsv b/src/objects/cd-34-14724-b/source/photometry/claret-2011-v-quadratic.tsv new file mode 100644 index 00000000000..e8528021fbe --- /dev/null +++ b/src/objects/cd-34-14724-b/source/photometry/claret-2011-v-quadratic.tsv @@ -0,0 +1,11 @@ +logg Teff Z xi a b Filt Met Mod +[cm/s2] K [Sun] km/s +----- ------ ---- ---- ------- ------- -- - - + 4.00 6000 0.0 2.0 0.6801 0.1523 v L A + 4.50 6000 0.0 2.0 0.6731 0.1587 v L A + 4.00 6250 0.0 2.0 0.6169 0.2000 v L A + 4.50 6250 0.0 2.0 0.6059 0.2100 v L A + 4.00 6000 0.0 2.0 0.4110 0.2882 V L A + 4.50 6000 0.0 2.0 0.4152 0.2860 V L A + 4.00 6250 0.0 2.0 0.3758 0.3047 V L A + 4.50 6250 0.0 2.0 0.3771 0.3051 V L A diff --git a/src/objects/cd-34-14724-b/source/photometry/stellar-color.json b/src/objects/cd-34-14724-b/source/photometry/stellar-color.json new file mode 100644 index 00000000000..2bbd2dc2a4c --- /dev/null +++ b/src/objects/cd-34-14724-b/source/photometry/stellar-color.json @@ -0,0 +1,16 @@ +{ + "schema": "cssearth-stellar-photometric-color@1", + "objectId": "cd-34-14724-b", + "spectrum": "gaia-xp-sampled", + "sampledSpectrum": { + "source": "Gaia Collaboration (2023), Gaia Data Release 3, A&A 674, A1; BP/RP spectra: De Angeli et al. (2023), A&A 674, A2, and externally calibrated sampled spectra: Montegriffo et al. (2023), A&A 674, A3", + "path": "photometry/gaia-dr3-xp-sampled.csv", + "service": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "checked": "2026-10-05", + "sourceId": "6780546169633474944", + "sampling": "343 samples from 336 to 1020 nm in 2 nm steps, flux in W m^-2 nm^-1 (the DataLink default sampling)", + "note": "Gaia's own low-resolution spectrum of the star, calibrated to absolute flux. Its samples at the even wavelengths from 380 to 780 nm are weighted by the CIE 1931 2-degree observer. The range in the prepared report is the color of the spectrum moved one standard error down and up at every sample." + }, + "whitePoint": "sRGB D65", + "normalization": "brightest linear sRGB channel = 1" +} diff --git a/src/objects/cd-34-14724-b/source/preparation/acquisition.json b/src/objects/cd-34-14724-b/source/preparation/acquisition.json new file mode 100644 index 00000000000..8261fb8bb7b --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/acquisition.json @@ -0,0 +1,66 @@ +{ + "schema": "cssearth-acquisition-plan@1", + "operations": [ + { + "kind": "download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/gaia-dr3-xp-sampled.csv", + "url": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3" + }, + { + "kind": "request-download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/claret-2011-v-quadratic.tsv", + "url": "https://vizier.cds.unistra.fr/viz-bin/asu-tsv", + "form": { + "-source": "J/A+A/529/A75/table-af", + "-out.max": "500", + "Teff": "6000..6250", + "logg": "4..4.5", + "Z": "=0", + "xi": "=2", + "Filt": "V", + "Met": "L", + "Mod": "A", + "-out": "logg,Teff,Z,xi,a,b,Filt,Met,Mod" + }, + "requiredText": [ + "logg\tTeff" + ], + "replacements": [ + { + "pattern": "^#.*\\n", + "flags": "gm", + "replacement": "" + }, + { + "pattern": "^\\s*\\n", + "flags": "gm", + "replacement": "" + } + ] + }, + { + "kind": "request-download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/gaia-dr3-source.csv", + "url": "https://gea.esac.esa.int/tap-server/tap/sync", + "form": { + "REQUEST": "doQuery", + "LANG": "ADQL", + "FORMAT": "csv", + "QUERY": "SELECT s.source_id, s.ref_epoch, s.ra, s.dec, s.parallax, s.parallax_error, s.pmra, s.pmdec, s.radial_velocity, s.radial_velocity_error, s.ruwe, s.phot_g_mean_mag, s.bp_rp, s.has_xp_sampled, a.mass_flame, a.mass_flame_lower, a.mass_flame_upper, a.radius_flame, a.radius_flame_lower, a.radius_flame_upper FROM gaiadr3.gaia_source AS s LEFT JOIN gaiadr3.astrophysical_parameters AS a ON a.source_id = s.source_id WHERE s.source_id = 6780546169633474944" + }, + "requiredPrefix": "source_id," + } + ] +} diff --git a/src/objects/cd-34-14724-b/source/preparation/geometry.json b/src/objects/cd-34-14724-b/source/preparation/geometry.json new file mode 100644 index 00000000000..170010119c8 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/geometry.json @@ -0,0 +1,62 @@ +{ + "schema": "cssearth-css-geometry-profile@1", + "namespace": "cd-34-14724-b", + "surface": { + "radius": 248, + "polarRadius": 248, + "latitudeSegments": 16, + "longitudeSegments": 32, + "surface": { + "url": "/scenes/cd-34-14724-b/cd-34-14724-b-surface-color@2x.webp", + "width": 1024, + "height": 768 + }, + "surfaceLatitudeHeight": 512, + "packedBandGutter": 8, + "poles": { + "url": "/scenes/cd-34-14724-b/cd-34-14724-b-poles-color@2x.webp", + "width": 512, + "height": 256 + }, + "polarTileSize": 256, + "polarRadiusScale": 1.035, + "polarOffset": 0.1, + "uv": "cell", + "color": "#fff5f8" + }, + "projection": { + "tileSize": 50, + "layerElevation": 50, + "seamBleed": 0, + "interiorSeamBleed": 8, + "overlap": 0.015625, + "seamOutset": { + "targetPixels": 0.5, + "stepRatio": 1.4142135623730951, + "hysteresis": 0.1, + "firstDiameter": 16, + "lastDiameter": 32768 + }, + "fitToSource": false, + "rasterScale": 2, + "rasterGutter": 8, + "rasterOverscan": 0.5, + "projectivePoles": false, + "lightColor": "#ffffff", + "ambientIntensity": 1 + }, + "bodyRotationDegrees": 0, + "output": { + "schema": "csscd-34-14724-b-prepared-runtime-scene@1", + "materialSchema": "csscd-34-14724-b-prepared-emission@1", + "layout": "body-container", + "body": { + "axialTiltDegrees": 0, + "rotationDirection": "prograde", + "rotationPeriodEarthDays": 36525, + "sourceProjection": "the color dataset: a uniform photosphere color on the reference sphere", + "polarPreparation": "the same color on the polar tiles", + "axialTiltNote": "the star has no measured rotation axis; the object's rotation record places a display axis along celestial north in the plane of the sky, and this profile's tilt is not used for the frame" + } + } +} diff --git a/src/objects/cd-34-14724-b/source/preparation/navigation.json b/src/objects/cd-34-14724-b/source/preparation/navigation.json new file mode 100644 index 00000000000..860b33031f9 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/navigation.json @@ -0,0 +1,41 @@ +{ + "schema": "cssearth-navigation-marker@2", + "objectId": "cd-34-14724-b", + "owner": "object", + "presentation": { + "size": 5 + }, + "source": { + "path": "presentation/context.png" + }, + "operations": [ + { + "type": "resize", + "width": "tile", + "height": "tile", + "fit": "cover", + "position": "centre", + "kernel": "lanczos3" + }, + { + "type": "ensure-alpha" + }, + { + "type": "ellipse-mask", + "cx": 0.5, + "cy": 0.5, + "rx": 0.45, + "ry": 0.45, + "shading": { + "ambient": 0.35, + "diffuse": 0.65 + } + }, + { + "type": "png" + } + ], + "context": { + "pixels": 512 + } +} diff --git a/src/objects/cd-34-14724-b/source/preparation/new-object.json b/src/objects/cd-34-14724-b/source/preparation/new-object.json new file mode 100644 index 00000000000..21ec202cb52 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/new-object.json @@ -0,0 +1,44 @@ +{ + "id": "cd-34-14724-b", + "name": "CD-34 14724B", + "system": "WASP-94 system", + "description": "Star bound to WASP-94 A, 3,171 AU away.", + "boundTo": { + "host": "wasp-94-a", + "source": "El-Badry, Rix & Heintz (2021, MNRAS 506, 2269) list WASP-94 A and CD-34 14724B in their Gaia EDR3 wide-binary catalogue (VizieR J/MNRAS/506/2269, source_id 6780546169634170496 and 6780546169633474944), 3,171 AU apart on the sky, with a chance-alignment probability of 8.4e-6: the pair is bound. No orbit of the pair is published, and the Gaia astrometry alone does not constrain one, so the star is placed by its own astrometry and carries no orbit." + }, + "gaia": "6780546169633474944", + "paper": { + "url": "https://arxiv.org/abs/2101.05282", + "credit": "El-Badry, Rix & Heintz (2021), MNRAS 506, 2269" + }, + "radius": { + "value": 1.431, + "source": "Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the radius of TIC 92352621 (VizieR IV/39/tic82)", + "url": "https://doi.org/10.3847/1538-3881/ab3467", + "uncertainty": 0.068 + }, + "mass": { + "value": 1.13, + "source": "Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the mass of TIC 92352621 (VizieR IV/39/tic82)", + "url": "https://doi.org/10.3847/1538-3881/ab3467", + "uncertainty": 0.149 + }, + "temperature": { + "value": 6049.7, + "source": "Stassun et al. (2019), AJ 158, 138 (TIC v8.2), the effective temperature of TIC 92352621 (VizieR IV/39/tic82)", + "url": "https://doi.org/10.3847/1538-3881/ab3467", + "uncertainty": 126.5 + }, + "planets": [], + "companions": [], + "text": { + "card": "CD-34 14724B is a star of 6,050 K, bound to WASP-94 A 3,171 AU away.", + "introduction": "CD-34 14724B shares its motion through space with WASP-94 A, 3,171 AU away, so the two are a bound pair. Both are placed where Gaia measures them.", + "locator": "J/MNRAS/506/2269 catalog: source_id 6780546169634170496 and 6780546169633474944, sep_AU 3170.554071150918389, R_chance_align 8.353391498698639346e-06" + }, + "notes": [ + "CD-34 14724B's orbit around WASP-94 A is not measured; both stars are placed at their Gaia DR3 positions, which is where they are" + ], + "order": 5780 +} diff --git a/src/objects/cd-34-14724-b/source/preparation/presentation.json b/src/objects/cd-34-14724-b/source/preparation/presentation.json new file mode 100644 index 00000000000..25afffcff30 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/presentation.json @@ -0,0 +1,5 @@ +{ + "schema": "cssearth-css-presentation-profile@2", + "namespace": "cd-34-14724-b", + "mode": "emissive" +} diff --git a/src/objects/cd-34-14724-b/source/preparation/raster.json b/src/objects/cd-34-14724-b/source/preparation/raster.json new file mode 100644 index 00000000000..78a3557cff4 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/raster.json @@ -0,0 +1,79 @@ +{ + "schema": "cssearth-raster-recipe@2", + "publicBase": "/scenes/cd-34-14724-b/", + "sourceWidth": 1024, + "sourceHeight": 512, + "width": 1024, + "height": 512, + "latitudeBands": 16, + "polarTile": 256, + "resample": "density-before-pack", + "polarProjection": "orthographic-bilinear", + "polesOutput": "cd-34-14724-b-poles-{id}{suffix}.webp", + "surfaceMetadata": { + "schema": "csscd-34-14724-b-prepared-assets@1" + }, + "thumbnail": { + "size": 64, + "centerLongitudeDegrees": 0 + }, + "surfaces": [ + { + "id": "color", + "output": "cd-34-14724-b-surface-{id}{suffix}.webp", + "thumbnail": "cd-34-14724-b-dataset-{id}.webp", + "source": "photometry/stellar-color.json", + "falseColor": false, + "science": { + "kind": "stellar-photometric-color", + "limbDarkening": { + "law": "quadratic", + "path": "photometry/claret-2011-v-quadratic.tsv", + "grid": { + "teffK": 6049.7, + "logg": 4.18, + "models": { + "Filt": "V", + "Met": "L", + "Mod": "A" + } + }, + "columns": { + "teff": "Teff", + "logg": "logg", + "u1": "a", + "u2": "b" + } + }, + "qualification": "Photosphere color from the star's Gaia DR3 BP/RP spectrum, dimmed toward the limb by the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,050 K and log g 4.18 (u1 0.405, u2 0.291): a model, because no fit of this star's limb is used. The disc is unresolved: no map or absolute brightness is implied. Self-luminous, so no lighting." + } + } + ], + "emission": { + "offLimbSize": 600, + "limbSize": 512, + "bodyDiameter": 496, + "offLimbOutput": "cd-34-14724-b-context-{id}{suffix}.webp", + "limbOutput": "cd-34-14724-b-limb-{id}{suffix}.webp", + "metadata": { + "schema": "csscd-34-14724-b-prepared-emission@1", + "presentation": "prepared-emissive-surface-with-stationary-off-limb-context-and-limb-plate", + "offLimbContext": { + "logicalSize": 600, + "source": "none: no observation, a transparent plate", + "composition": "transparent", + "rotation": "none", + "runtimeAlphaProcessing": false + }, + "limbMaterial": { + "logicalSize": 496, + "composition": "black alpha darkens the photosphere according to the selected quadratic limb-darkening law; transparent outside the silhouette", + "surfaceReplacement": false, + "runtimeAlphaProcessing": false + }, + "lighting": false, + "shadows": false, + "runtimeLighting": false + } + } +} diff --git a/src/objects/cd-34-14724-b/source/preparation/rotation.json b/src/objects/cd-34-14724-b/source/preparation/rotation.json new file mode 100644 index 00000000000..16e2a2f33f4 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/preparation/rotation.json @@ -0,0 +1,10 @@ +{ + "schema": "cssearth-display-orientation@1", + "rightAscensionDegrees": 313.7882976030805, + "declinationDegrees": 55.864269710234446, + "displayMeridianDegrees": 90, + "phase": "arbitrary-display-phase", + "source": "No measured rotation axis or period is used. The display axis is celestial north at the catalogue position, placed in the plane of the sky; computed by skyPlaneOrientation in @cssearth/astronomy.", + "coordinateSystem": "ICRF/J2000. +Z is the display axis: the sky-north direction at the star, in the plane of the sky. +X is the display meridian, set so that grid longitude 0 faces the Sun and Earth at the scene epoch; east longitude. No spin is propagated.", + "qualification": "Display convention, not a measurement. The rotation axis, spin sense, period and prime meridian of CD-34 14724B are unmeasured; the axis shown is where celestial north lies on the sky." +} diff --git a/src/objects/cd-34-14724-b/source/presentation/context.png b/src/objects/cd-34-14724-b/source/presentation/context.png new file mode 100644 index 00000000000..c4b028067f8 Binary files /dev/null and b/src/objects/cd-34-14724-b/source/presentation/context.png differ diff --git a/src/objects/cd-34-14724-b/source/presentation/solar-system.json b/src/objects/cd-34-14724-b/source/presentation/solar-system.json new file mode 100644 index 00000000000..51debee05c0 --- /dev/null +++ b/src/objects/cd-34-14724-b/source/presentation/solar-system.json @@ -0,0 +1,9 @@ +{ + "schema": "cssearth-solar-system-preparation@1", + "bodyId": "cd-34-14724-b", + "displayName": "CD-34 14724B", + "bodyRadiusUnits": 248, + "bodyRadiusKilometers": 995546.7, + "defaultZoom": 1.25, + "geometryScale": 1.25 +} diff --git a/src/objects/cd-34-14724-b/text.json b/src/objects/cd-34-14724-b/text.json new file mode 100644 index 00000000000..2cf95d17301 --- /dev/null +++ b/src/objects/cd-34-14724-b/text.json @@ -0,0 +1,35 @@ +{ + "schema": "cssearth-object-text@1", + "objectId": "cd-34-14724-b", + "card": { + "text": "CD-34 14724B is a star of 6,050 K, bound to WASP-94 A 3,171 AU away.", + "sources": [ + { + "catalogueId": "vizier-j-mnras-506-2269", + "url": "https://arxiv.org/abs/2101.05282", + "label": "El-Badry et al. (2021)", + "checked": "2026-10-05", + "locator": "J/MNRAS/506/2269 catalog: source_id 6780546169634170496 and 6780546169633474944, sep_AU 3170.554071150918389, R_chance_align 8.353391498698639346e-06" + } + ] + }, + "introduction": { + "text": "CD-34 14724B shares its motion through space with WASP-94 A, 3,171 AU away, so the two are a bound pair. Both are placed where Gaia measures them.", + "sources": [ + { + "catalogueId": "vizier-j-mnras-506-2269", + "url": "https://arxiv.org/abs/2101.05282", + "label": "El-Badry et al. (2021)", + "checked": "2026-10-05", + "locator": "J/MNRAS/506/2269 catalog: source_id 6780546169634170496 and 6780546169633474944, sep_AU 3170.554071150918389, R_chance_align 8.353391498698639346e-06" + } + ] + }, + "datasets": { + "color": { + "title": "Photosphere color", + "detail": "From its Gaia spectrum", + "summary": "The color of the star's light, dimmed toward the edge by a model atmosphere." + } + } +} diff --git a/src/objects/milky-way/inventory.json b/src/objects/milky-way/inventory.json index 8b5483c4193..508b9cc75b2 100644 --- a/src/objects/milky-way/inventory.json +++ b/src/objects/milky-way/inventory.json @@ -28,14 +28,14 @@ { "location": "prepared", "filename": "members.json", - "bytes": 65292, - "sha256": "3b9d20b0c98cab194cde00a2e1ed9595fda769979dda15c69f659b5591478a20" + "bytes": 65529, + "sha256": "9d6a870b1a689e78b13bce268f069a201904cd6b17a55259b782d60f5724b517" }, { "location": "prepared", "filename": "places.json", - "bytes": 62903, - "sha256": "9a73abf541a46ef8fcaef15e3dc6989159a7129087fdba939f87b53d6d4dee9c" + "bytes": 62991, + "sha256": "1632997f96d2b3449ca537c0f76a6e3559a483d390b60fb12fa8e34b572c7549" }, { "location": "prepared", diff --git a/src/objects/observable-universe/inventory.json b/src/objects/observable-universe/inventory.json index 6c13a549cfa..598effeb2ee 100644 --- a/src/objects/observable-universe/inventory.json +++ b/src/objects/observable-universe/inventory.json @@ -64,14 +64,14 @@ { "location": "prepared", "filename": "world-context.json", - "bytes": 58717079, - "sha256": "52543db215dc9f6f20d3a4f9d8377a001781a702bda1f631c89daef3900fc8b6" + "bytes": 58766266, + "sha256": "00b9a6338e0a94b62a0f112793ea37812d24edb6889b4ef293d9798cfaa7b018" }, { "location": "prepared", "filename": "world-index.json", - "bytes": 1416735, - "sha256": "f4e20624375411cb1fcfb4022bbcc93348db55e5ee44bcffbdcc68774bc029bb" + "bytes": 1416796, + "sha256": "0922852a481b68166eb3a2dc9e9969f20c7bf7553dc38041ec4d66a0ef51d69b" }, { "location": "prepared", @@ -82,8 +82,8 @@ { "location": "prepared", "filename": "world.json", - "bytes": 2534, - "sha256": "f6f6344c50f4815ab5705cb6c41f5113e976e1a33c12b9c775bebe5b9aac8560" + "bytes": 2553, + "sha256": "76f48116dfd3ef73f9d8ff24eb9039f306536c110b29b839b42101d99b9bad53" } ] } diff --git a/src/objects/wasp-94-a-b/NOTICE.md b/src/objects/wasp-94-a-b/NOTICE.md new file mode 100644 index 00000000000..71a85303f0d --- /dev/null +++ b/src/objects/wasp-94-a-b/NOTICE.md @@ -0,0 +1,9 @@ +# WASP-94 A b credits + +Radius: Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive. Mass: Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive. + +Orbit: Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive. + +Color: a black body at the 1,535 K dayside brightness temperature of Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2), through the CIE 1931 2° color-matching functions. + +Climate model: Mukherjee et al. (2025), "Cloudy mornings and clear evenings on a gas giant exoplanet" (arXiv:2505.10910), Met Office Unified Model simulation, Zenodo 10.5281/zenodo.15085825, CC BY 4.0. diff --git a/src/objects/wasp-94-a-b/README.md b/src/objects/wasp-94-a-b/README.md new file mode 100644 index 00000000000..c20c70d1412 --- /dev/null +++ b/src/objects/wasp-94-a-b/README.md @@ -0,0 +1,38 @@ +# WASP-94 A b + +## Sources + +It is the only planet known around WASP-94 A. Its orbit and size follow Stassun et al. 2017's fit, the archive's default. The introduction is generated from Stassun et al. 2017's published values; the sections below are the data's own. + +**Size and mass.** Radius 1.58 Jupiter radii from Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract): 112,957.4 km at 71,492 km per Jupiter radius. GM from the mass 0.5 Jupiter masses (Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive, https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract) times JPL's Jupiter GM. A sphere: no oblateness is measured. + +**Orbit.** Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): P 3.9502001 d Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): a/R* 8.44; Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): inclination 88.7 degrees Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): e 0 Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): transit mid-time 2458300.64763 BJD, taken as BJD_TDB; with its period, the row that predicts 2026-01-01 best (1 sigma 1 min) Display convention: transit photometry does not measure the orbit's position angle on the sky, so the ascending node is set at position angle 0 (celestial north). + +**Color.** A black body at the 1,535 K dayside brightness temperature measured in secondary eclipse at 3.6 µm (Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2)): #ff6f00. Chosen from the archive's emission rows by rule: 2 measured of 2 rows; the smallest relative uncertainty, then the longest wavelength. Reflected starlight is not included. + +**Charts.** The orbits of WASP-94 A's planets from above, from their hosted-orbit records, and its transit in 3 TESS sectors (68, 95, 105), folded onto its orbit. Upper limits and rows without an error are left out. + +**Climate model dataset.** The page opens on the temperature of WASP-94 A b in the Met Office Unified Model run of Mukherjee et al. (2025, [arXiv:2505.10910](https://arxiv.org/abs/2505.10910)), released under CC BY 4.0 as [Zenodo record 15085825](https://zenodo.org/records/15085825). It is a model, labelled as one: a hydrogen and helium atmosphere at the Sun's share of heavier elements, without clouds, on a planet that turns once for each orbit. The file is `GCM_products/WASP-94Ab_GCM.nc` inside `GCM_products.zip`, the variable `air_temperature` in kelvin on the model's 144 × 90 grid and 87 levels, at the later of the two saved states (42,000 hours of model time; the earlier one, 240 hours before, differs by under 0.1 K). + +The model's levels are heights, and the paper notes that the pressure at one height differs by orders of magnitude between the day side and the night side, so it draws its own temperature map on a surface of one pressure (its Figure S18 D). The dataset does the same at 1 millibar, where the paper states the model's difference between the two edges: in each column the temperature is taken between the two levels whose `air_pressure` lies either side of 100 Pa, linearly in the logarithm of pressure ([netcdf-isobar.ts](../../../packages/bake/src/objects/raster/netcdf/netcdf-isobar.ts)). The reader refuses 0.01 millibar, the pressure of the paper's figure: near the model's top the released pressure passes it three times in some columns. The star is overhead at grid longitude 180°: the release's own `toa_incoming_shortwave_flux` peaks equally at 178.75° and 181.25°. + +The 632 MB file is NetCDF-4, which spreads its structure through the file, so it is kept whole, outside git, and [`new-object --simulation`](../../../packages/telescope-cli/src/new-object/simulation/simulation-dataset.mts) and the source restore take it out of the 546 MB archive. + +## Evidence + +Generated 2026-10-05 by [new-object-cli.mts](../../../packages/telescope-cli/src/new-object/new-object-cli.mts); the orbit is the one recorded in [its astronomy record](../../../packages/astronomy/data/bodies/wasp-94-a-b.json). + +**The model file is read as its own library reads it.** The NetCDF-4 reader ([hdf5-netcdf.ts](../../../packages/bake/src/objects/raster/netcdf/hdf5-netcdf.ts)) was compared with h5py 3.16 on the released file on 5 October 2026: both find 53 variables, and `air_temperature` and `air_pressure` at the later state and level 40 agree to the last digit (first value 1490.0345103484158 K, minimum 1265.2697968491468 K, maximum 1925.2773438740744 K). Its tests read two small files the reference libraries wrote ([fixtures](../../../packages/bake/src/objects/raster/netcdf/fixtures/README.md)). + +![WASP-94 A b as its page opens, on its day side, and turned to its night side, 5 October 2026](evidence/2026-10-05/day-and-night-side.jpg) + +**The map reproduces the paper's numbers.** At 1 millibar the map runs from 955.9 to 1,650.8 K with no column missing. The evening edge is 289 K hotter than the morning edge at the equator (1,472.7 against 1,183.4 K) and 263 K hotter averaged over all latitudes; the paper states about 300 K and about 265 K. + +## Known problems + +- **The model has no clouds; the measurement says there are some.** The paper measures the planet's two edges in one JWST transit and finds a cloudy morning edge and a clear evening edge. It maps clouds by a second model run on this one's output; that cloud map is not in the release, so the page shows the model's temperature, not its clouds. The measured edges themselves, two spectra, are not drawn. +- **One face to the star is assumed.** The model and the rotation record both assume the planet turns once for each orbit; no rotation period is measured. +- **Gray caps.** The model's grid has no samples within 1° of either pole, and nothing is extrapolated there. + + +[Investigation ledger](investigations.json) · [Inputs](source/manifest.json) · [Preparation](source/preparation) · [Delivered files](inventory.json) · [Credits](NOTICE.md) diff --git a/src/objects/wasp-94-a-b/evidence/2026-10-05/day-and-night-side.jpg b/src/objects/wasp-94-a-b/evidence/2026-10-05/day-and-night-side.jpg new file mode 100644 index 00000000000..baf5486fb01 Binary files /dev/null and b/src/objects/wasp-94-a-b/evidence/2026-10-05/day-and-night-side.jpg differ diff --git a/src/objects/wasp-94-a-b/inventory.json b/src/objects/wasp-94-a-b/inventory.json new file mode 100644 index 00000000000..53ecb8e0eec --- /dev/null +++ b/src/objects/wasp-94-a-b/inventory.json @@ -0,0 +1,365 @@ +{ + "schema": "cssearth-inventory@1", + "assets": [ + { + "location": "public", + "filename": "lighting-2x-billboard.webp", + "bytes": 115740, + "sha256": "de2473d298dfacd0e42f710629536fb7591083c2647f4d4a547602d01b2c1367" + }, + { + "location": "public", + "filename": "lighting-2x-row-00.webp", + "bytes": 37672, + "sha256": "ec6ee915f55a668fc95dacfef1499a42bd118a4aac16f3777b66e819ee31e2f2" + }, + { + "location": "public", + "filename": "lighting-2x-row-01.webp", + "bytes": 70898, + "sha256": "e4b9d197c7d62aecdae0144b33895e7f55dd005c84a96ca6409b0f5869700fbc" + }, + { + "location": "public", + "filename": "lighting-2x-row-02.webp", + "bytes": 92030, + "sha256": "5c752b1be40564aaba7d9ccadc0edba838ecaf4cc8fe5794efe58b404d4f6556" + }, + { + "location": "public", + "filename": "lighting-2x-row-03.webp", + "bytes": 106840, + "sha256": "11824e8fda78b665117d9d40d63495cb36f768377d768b62c383a369ed40758c" + }, + { + "location": "public", + "filename": "lighting-2x-row-04.webp", + "bytes": 120584, + "sha256": "55b22230f6aef465a46c3a66b00ab246dc223b080ca3d97412e2667f37deb66f" + }, + { + "location": "public", + "filename": "lighting-2x-row-05.webp", + "bytes": 132094, + "sha256": "a4cb1a4c54322a74ec9bcecd3b0ed4bb5ef9773db946a3a8d7aed8ef33f34d20" + }, + { + "location": "public", + "filename": "lighting-2x-row-06.webp", + "bytes": 143228, + "sha256": "2586ccfe25f6bbd21d438a5b97551031e9a7b9b6d87f04872aacfc6ffe6cb39c" + }, + { + "location": "public", + "filename": "lighting-2x-row-07.webp", + "bytes": 153142, + "sha256": "65482bb8de64f684ee6587ccc0f7ed0e7f0b3a6494b1dfa6510ed8c9dff79632" + }, + { + "location": "public", + "filename": "lighting-2x-row-08.webp", + "bytes": 163984, + "sha256": "0f13d5d4cf8a8591660f47e32deae93a3006e18b3aa10bc4b96942beadc14600" + }, + { + "location": "public", + "filename": "lighting-2x-row-09.webp", + "bytes": 172632, + "sha256": "b3bdd0776903940bc8c77ce34918ff6a6a0638dfe47ebb1896a3d2050f3ab1a3" + }, + { + "location": "public", + "filename": "lighting-2x-row-10.webp", + "bytes": 177128, + "sha256": "1ad6e95b79025596c3ae11e928658923be4408dbd611f58e22475436812594e2" + }, + { + "location": "public", + "filename": "lighting-2x-row-11.webp", + "bytes": 179144, + "sha256": "4920b80933238f45a83133c2c04c73b9f37f9bfefdf16afca4a1e1af9edcfbab" + }, + { + "location": "public", + "filename": "lighting-2x-row-12.webp", + "bytes": 173518, + "sha256": "c1e8fb56dbb734572cfcace301f7b373cb03743d4245cfad6f3059451d43e3af" + }, + { + "location": "public", + "filename": "lighting-2x-row-13.webp", + "bytes": 161150, + "sha256": "47d37733690f0de2136da2515f561e7f1cb520e5c9eaf8c38e06944ef0b25f91" + }, + { + "location": "public", + "filename": "lighting-2x-row-14.webp", + "bytes": 132038, + "sha256": "07b759240df293f70fba1d761088cbff06844fdbc0cfbb4f50bde670b60ad583" + }, + { + "location": "public", + "filename": "lighting-2x-row-15.webp", + "bytes": 69930, + "sha256": "2990c3e0953a66fbc2f1f7fff4006abbc014742c34d6ccc6f58e1f69750e8bac" + }, + { + "location": "public", + "filename": "lighting-2x-row-16.webp", + "bytes": 74522, + "sha256": "4cd554323a5b4c4d7d302fd75f077ee0829bd995f69028cdeed09cb71430a95d" + }, + { + "location": "public", + "filename": "lighting-2x-row-17.webp", + "bytes": 143520, + "sha256": "2ecca046c7a3ccfc32431f93ca3bfc1da1eed02c8faee885a51d32103ad550d4" + }, + { + "location": "public", + "filename": "lighting-2x-row-18.webp", + "bytes": 185628, + "sha256": "659839c1f5c68b28b73226c72861311cc2e223face47af04284f01c58462da9a" + }, + { + "location": "public", + "filename": "lighting-2x-row-19.webp", + "bytes": 218252, + "sha256": "0032b3cb244f0ef0d03e4176f2cca51a5efc25c6dac0a928fd70073c39a27e01" + }, + { + "location": "public", + "filename": "lighting-2x-row-20.webp", + "bytes": 245184, + "sha256": "dee7965558ba47853e80d02af052eb3d8d96c2cb0f9ea6ece30b8604af899ad6" + }, + { + "location": "public", + "filename": "lighting-2x-row-21.webp", + "bytes": 269836, + "sha256": "160e8c89beb6880cca9d1c4c49c1d7ae8d42be1f1f1bac790bcc1ae098809f41" + }, + { + "location": "public", + "filename": "lighting-2x-row-22.webp", + "bytes": 291326, + "sha256": "64714cc753f56a2a794c09926acd4f589c69ebedc97b7636576aa2e8a8391318" + }, + { + "location": "public", + "filename": "lighting-2x-row-23.webp", + "bytes": 312110, + "sha256": "f1fc46cc87683bce654494f74cb30e03e7490b8fcc50c00a1acda54ce7b014f2" + }, + { + "location": "public", + "filename": "lighting-2x-row-24.webp", + "bytes": 333302, + "sha256": "866d79248222db2db636fade29aa0ca9009fba80fab506c1858e0b515bc23520" + }, + { + "location": "public", + "filename": "lighting-2x-row-25.webp", + "bytes": 355976, + "sha256": "ae19b305ce1eeb00a4a9318a2be187734482bcd93f2d992d236fbf813072766c" + }, + { + "location": "public", + "filename": "lighting-2x-row-26.webp", + "bytes": 377782, + "sha256": "c981f9f2abc41afe63dcacee618425a422b37f88579dc7cb294919bfc03062d1" + }, + { + "location": "public", + "filename": "lighting-2x-row-27.webp", + "bytes": 403282, + "sha256": "cc34f5b92eeb1ae06c753ba286c86b36e5e9b91b06ff7b1d8f8aaa124bfa56e8" + }, + { + "location": "public", + "filename": "lighting-2x-row-28.webp", + "bytes": 429004, + "sha256": "e0dbc0c14c173d8ae855d6cbd9b240ce8e4b43383e01c49701f3628415b1704e" + }, + { + "location": "public", + "filename": "lighting-2x-row-29.webp", + "bytes": 456570, + "sha256": "2859562bb82812c38c6c6adcf748be8d5afc47ce68e6a3091255f24e489016cf" + }, + { + "location": "public", + "filename": "lighting-2x-row-30.webp", + "bytes": 481914, + "sha256": "e82ae2b58ba24928ad234c6aed182dc48c900ac0c04a61509519f93562d8b2b9" + }, + { + "location": "public", + "filename": "lighting-2x-row-31.webp", + "bytes": 526556, + "sha256": "76c1894c034234d95fe67d23bc98574e4a1055f9629dd702fa0ae2f3223d0637" + }, + { + "location": "public", + "filename": "lighting-2x-shadowless-billboard.webp", + "bytes": 968, + "sha256": "8cfe3d4e10e2b2c5b68dfb19e9e561a28d2b61e59f402756f9d0b5d536a6d2f1" + }, + { + "location": "public", + "filename": "lighting-2x-shadowless.webp", + "bytes": 56158, + "sha256": "88d20de295f141c506e6de64cf4cc10a9eefb6e58852d5c977d4ef8a6af88f9f" + }, + { + "location": "public", + "filename": "wasp-94-a-b-arrival.webp", + "bytes": 37998, + "sha256": "a610cf7f3a7ab0215136806f6610573fea08813304b6f9c1b3367f925f041603" + }, + { + "location": "public", + "filename": "wasp-94-a-b-billboard.webp", + "bytes": 6368, + "sha256": "054163a5a554f92ca65e0b987de3b68f6e369b0be4b84f4738f4f86ab8f2c771" + }, + { + "location": "public", + "filename": "wasp-94-a-b-dataset-climate-model.webp", + "bytes": 1176, + "sha256": "d3867d76e94e09865d5bc09783df8fbbddbd2ba21d6c256fee694b1768838b06" + }, + { + "location": "public", + "filename": "wasp-94-a-b-dataset-thermal.webp", + "bytes": 42, + "sha256": "8577dcf5042f53bd5e603d548ad55b629dc03da19441eb463d8a8c3be2d4cdfe" + }, + { + "location": "public", + "filename": "wasp-94-a-b-poles-climate-model@2x.webp", + "bytes": 21620, + "sha256": "9a258e76972e22c7777b8e84acfdd8bbf8885bd09b4194de3b0bfb1a2b54fc30" + }, + { + "location": "public", + "filename": "wasp-94-a-b-poles-thermal@2x.webp", + "bytes": 1340, + "sha256": "45c1ba53d2ca8be368684f13e561915dcdb6ca2aacc7cfd320c2b9ab4d7b9a9e" + }, + { + "location": "public", + "filename": "wasp-94-a-b-surface-climate-model@2x.webp", + "bytes": 92454, + "sha256": "f1b26c2f4d1ef3095e4b7adf36bc13e4996fb19ece06bcdafa56d8c3d6c60303" + }, + { + "location": "public", + "filename": "wasp-94-a-b-surface-thermal@2x.webp", + "bytes": 36, + "sha256": "3d2aa112426f65d67e40dfa4b21be30281ca7403880e7f0b7c9b6211fd39f47a" + }, + { + "location": "public", + "filename": "wasp-94-a-b-system-orbits.svg", + "bytes": 4613, + "sha256": "5fe1a25323e1cf7ff64d463ba6a2ce5c6be7b3a3f28c447d82d5982c2d78e2b8" + }, + { + "location": "public", + "filename": "wasp-94-a-b-transit.svg", + "bytes": 11859, + "sha256": "578e154091559cafcba4b72d8c46bf8a89102c33ddb84cc90800376f474aa977" + }, + { + "location": "prepared", + "filename": "arrival-billboard.json", + "bytes": 321, + "sha256": "23b3d65c6180a82d4e62424375ef9908fba44cf83e4850d1eee6242a6458cdde" + }, + { + "location": "prepared", + "filename": "assets.json", + "bytes": 85342, + "sha256": "4abad39b91b6637a5fcb78a0583bcf6f45daffff151bf5cb1be0dc01194e6541" + }, + { + "location": "prepared", + "filename": "authored-preparation.json", + "bytes": 664, + "sha256": "228c2d1485b532dff25944f98e570eb9a8bd9b322e7cddbede586c7f9554a55d" + }, + { + "location": "prepared", + "filename": "content.json", + "bytes": 3044, + "sha256": "9a3f022405a2a8420d16e769dba68152e5f2acfc87cf68e830d5fe202442c20f" + }, + { + "location": "prepared", + "filename": "controls.json", + "bytes": 1854, + "sha256": "8a2636861584b54a6908045015d2f0c8272dca20904c50edfc663d552368a83a" + }, + { + "location": "prepared", + "filename": "datasets.json", + "bytes": 2860, + "sha256": "e62ad6e42d057364b88266f883a13a3b7bd3e90ada57720011d44cda6c1005f2" + }, + { + "location": "prepared", + "filename": "minimaps.json", + "bytes": 209, + "sha256": "3889af6763aa2001f0720e885f86c44a8cc37e43394b1bc986af80a31a16c131" + }, + { + "location": "prepared", + "filename": "minimaps/climate-model.webp", + "bytes": 2568, + "sha256": "7a1d2be05d418de1af4bdf873c64280df994b18b304cc618a66d9486457f4fcc" + }, + { + "location": "prepared", + "filename": "minimaps/thermal.webp", + "bytes": 458, + "sha256": "9df4272933d87af009062040189529fe24aae642435cf83039fc9e9da2ea2362" + }, + { + "location": "prepared", + "filename": "runtime.json", + "bytes": 342850, + "sha256": "df0f2b3caaa2ae271764392bfe53463bbbc9f72f4bafbd876e8b7106c02af64c" + }, + { + "location": "prepared", + "filename": "scene.json", + "bytes": 394101, + "sha256": "7f28ee69f1cc0e0467f3155dad49d8f58ae289ee0c02a723135aaa18a03aa48a" + }, + { + "location": "prepared", + "filename": "sky.json", + "bytes": 1205, + "sha256": "05274091c3d104839da9fa6c1fbed5081d1979624e949b98485d5db5982e54ce" + }, + { + "location": "prepared", + "filename": "sun.json", + "bytes": 663, + "sha256": "9d5bb117fe79c7358ddcd24d7725a16452b5c3729fd7bd47d1dfe2911d58ad18" + }, + { + "location": "prepared", + "filename": "text.json", + "bytes": 1525, + "sha256": "5dece2d0536eeda87a3a1b9101ea3c969b6c67cacd9bc018b2208d1858969c04" + }, + { + "location": "prepared", + "filename": "world-navigation.json", + "bytes": 1156, + "sha256": "da537edd59778a941a8f268362198ddf0635b1b97db9d37da60a54eaad2fc596" + } + ] +} diff --git a/src/objects/wasp-94-a-b/investigations.json b/src/objects/wasp-94-a-b/investigations.json new file mode 100644 index 00000000000..7ae8a06645f --- /dev/null +++ b/src/objects/wasp-94-a-b/investigations.json @@ -0,0 +1,75 @@ +{ + "schema": "cssearth-investigation-ledger@1", + "objectId": "wasp-94-a-b", + "entries": [ + { + "id": "radius", + "subject": "Radius", + "status": "included", + "finding": "1.58 Jupiter radii from Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive.", + "evidence": [ + "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract" + ] + }, + { + "id": "mass", + "subject": "Mass", + "status": "included", + "finding": "0.5 Jupiter masses: Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract).", + "evidence": [ + "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract" + ] + }, + { + "id": "orbit", + "subject": "Orbit", + "status": "included", + "finding": "Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): P 3.9502001 d; Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): a/R* 8.44; Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): inclination 88.7 degrees; Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive ps table (pl_refname STASSUN_ET_AL__2017): e 0; Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): transit mid-time 2458300.64763 BJD, taken as BJD_TDB; with its period, the row that predicts 2026-01-01 best (1 sigma 1 min); Display convention: transit photometry does not measure the orbit's position angle on the sky, so the ascending node is set at position angle 0 (celestial north)..", + "evidence": [ + "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "https://ui.adsabs.harvard.edu/abs/2023ApJS..265....4K/abstract" + ] + }, + { + "id": "color", + "subject": "Color", + "status": "included", + "finding": "A black body at the 1,535 K dayside brightness temperature measured in secondary eclipse at 3.6 µm (Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2)): #ff6f00. Chosen from the archive's emission rows by rule: 2 measured of 2 rows; the smallest relative uncertainty, then the longest wavelength. Reflected starlight is not included.", + "evidence": [ + "https://ui.adsabs.harvard.edu/abs/2023AJ....165..104D/abstract" + ] + }, + { + "id": "climate-model", + "subject": "Climate model of Mukherjee et al. (2025)", + "status": "included", + "finding": "The Met Office Unified Model run the paper released (Zenodo 10.5281/zenodo.15085825, CC BY 4.0) is shown as its own dataset, labelled as a model: its temperature at 1 millibar, 955.9 to 1,650.8 K. The evening edge is 289 K hotter than the morning edge at the equator and 263 K hotter over all latitudes; the paper states about 300 K and about 265 K.", + "evidence": [ + "https://arxiv.org/abs/2505.10910", + "https://zenodo.org/records/15085825" + ] + }, + { + "id": "transit-limbs", + "subject": "Evening and morning edges from the JWST NIRISS transit of 11 to 12 October 2024", + "status": "unresolved", + "finding": "The released FIREFLy.csv gives each edge's transit depth in 59 wavelength bins from 0.87 to 2.80 micrometres: the morning edge is 0.1045 to 0.1094 of the star's radius and the evening edge 0.1022 to 0.1114. They are two spectra, not a map, and the page does not draw them. A drawing of them as two arcs on the day-night circle, as the paper's Figure 2 has them, was built on 5 October 2026 and left out.", + "evidence": [ + "https://arxiv.org/abs/2505.10910", + "https://zenodo.org/records/15085825" + ], + "revisitWhen": "When the page gains a chart for a planet's two edge spectra." + }, + { + "id": "cloud-map", + "subject": "The paper's night-side cloud map (its Figures 2 B and S19 B)", + "status": "unresolved", + "finding": "The paper maps cloud optical depth by running the VIRGA cloud model on the climate model's output. The release holds the climate model and no cloud field, so the map cannot be drawn from released numbers and is not shown.", + "evidence": [ + "https://arxiv.org/abs/2505.10910", + "https://zenodo.org/records/15085825" + ], + "revisitWhen": "When the authors release the cloud optical depth map, or a later paper releases one for this planet." + } + ] +} diff --git a/src/objects/wasp-94-a-b/object.json b/src/objects/wasp-94-a-b/object.json new file mode 100644 index 00000000000..f0ca2ce05e3 --- /dev/null +++ b/src/objects/wasp-94-a-b/object.json @@ -0,0 +1,148 @@ +{ + "schema": "cssearth-object@2", + "id": "wasp-94-a-b", + "parent": "wasp-94-a-system", + "type": "layered-body", + "properties": { + "preparation": { + "schema": "cssearth-object-preparation@1", + "label": "WASP-94 A b", + "steps": [ + "verify-sources", + "assets", + "panel-content", + "datasets", + "starfield", + "sky-sun", + "system-markers", + "scene", + "controls", + "presentation", + "runtime-assets" + ] + }, + "recipe": { + "schema": "cssearth-authored-object@2", + "surfaces": [ + { + "id": "body", + "source": "geometry", + "projection": "equirectangular", + "datasets": [ + { + "id": "thermal", + "source": "content", + "material": "lighting" + }, + { + "id": "climate-model", + "source": "content", + "material": "lighting" + } + ] + } + ], + "shape": { + "kind": "sphere", + "radiusKm": 112957.4 + }, + "materials": [ + { + "id": "lighting", + "source": "raster", + "model": "lit" + } + ], + "sources": [ + { + "id": "raster", + "path": "source/preparation/raster.json" + }, + { + "id": "geometry", + "path": "source/preparation/geometry.json" + }, + { + "id": "presentation", + "path": "source/preparation/presentation.json" + }, + { + "id": "content", + "path": "source/content/object.json" + }, + { + "id": "solar-system", + "path": "source/presentation/solar-system.json" + }, + { + "id": "rotation", + "path": "source/preparation/rotation.json" + }, + { + "id": "navigation", + "path": "source/preparation/navigation.json" + }, + { + "id": "acquisition", + "path": "source/preparation/acquisition.json" + } + ] + }, + "page": { + "stylesheets": [ + "src/renderers/css/styles/body-surfaces.css", + "src/renderers/css/styles/templates/composite-planet.css" + ], + "metadata": { + "url": "prepared/page.json" + } + }, + "catalog": { + "name": "WASP-94 A b", + "aliases": [ + "WASP-94A b", + "WASP-94Ab" + ], + "classification": "exoplanet", + "color": "#9a9a9a", + "distanceAu": 43425864.2, + "description": "WASP-94 A b was found in 2014 by SuperWASP as it crossed its star.", + "systemName": "WASP-94 system", + "order": 5779, + "context": { + "order": 5779 + } + }, + "worldFrame": { + "referenceFrame": "sun-icrf", + "epochJdTt": 2461286.5, + "originM": [ + 3720623766199783400, + -3882089727872293400, + -3645513621913107000 + ], + "presentationToReference": [ + 0.5889503772834871, + -0.7087833155901209, + 0.38828296980258264, + -0.5817230360394765, + -0.7053113709399579, + -0.4051347669156659, + -0.5610131570994144, + -0.012731125789656244, + 0.8277089802566332 + ], + "orbitUpReference": [ + 0.7087833155901209, + 0.7053113709399579, + 0.012731125789656244 + ], + "metersPerUnit": 364378.70967741933, + "bodyRadiusM": 112957400 + } + }, + "prepared": { + "format": "cssearth-css-object@5", + "url": "prepared/object.json" + } +} diff --git a/src/objects/wasp-94-a-b/source/content/charts.json b/src/objects/wasp-94-a-b/source/content/charts.json new file mode 100644 index 00000000000..bbd0ac8efc8 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/content/charts.json @@ -0,0 +1,47 @@ +{ + "schema": "cssearth-chart-assets@1", + "publicBase": "/scenes/wasp-94-a-b/", + "charts": [ + { + "kind": "system-orbits", + "id": "wasp-94-a-b-orbits", + "title": "WASP-94 A b: orbits around WASP-94 A", + "output": "wasp-94-a-b-system-orbits.svg", + "system": "wasp-94-a", + "highlight": "wasp-94-a-b", + "description": "The orbits of WASP-94 A's planets in this map from above, to scale, with WASP-94 A b highlighted and the direction to Earth down. Each orbit is the one the app draws, from the published period, a/R*, eccentricity and argument of periastron; the star is a marker, not to scale.", + "metadata": { + "orbitSource": "Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): P 3.9502001 d" + } + }, + { + "kind": "folded-transit", + "id": "wasp-94-a-b-transit", + "title": "WASP-94 A b: its transit", + "description": "WASP-94 A b crossing its star, as TESS recorded it: 3 sectors (68, 95, 105) of the SPOC pipeline's 2-minute light curves of TIC 92352620, each transit divided by a straight line fitted to the light either side and folded onto the orbit the app draws, then averaged in 18-minute bins. Error bars are each bin's standard error.", + "output": "wasp-94-a-b-transit.svg", + "metadata": { + "tic": "TIC 92352620", + "sectors": [ + 68, + 95, + 105 + ], + "pipeline": "TESS SPOC, PDCSAP flux, quality 0" + }, + "planet": "wasp-94-a-b", + "sources": [ + "photometry/tess/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits", + "photometry/tess/tess2025206162959-s0095-0000000092352620-0292-s_lc.fits", + "photometry/tess/tess2026164183000-s0105-0000000092352620-0307-s_lc.fits" + ], + "durationHours": 4.488, + "binMinutes": 18, + "notes": [ + "TESS 2-min SPOC · sectors 68, 95, 105", + "Folded on the app's orbit; 18-min bins.", + "Bars: standard error of each bin." + ] + } + ] +} diff --git a/src/objects/wasp-94-a-b/source/content/object.json b/src/objects/wasp-94-a-b/source/content/object.json new file mode 100644 index 00000000000..728ecf727c1 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/content/object.json @@ -0,0 +1,185 @@ +{ + "schema": "cssearth-object-content@2", + "version": 1, + "id": "wasp-94-a-b", + "displayName": "WASP-94 A b", + "panel": { + "facts": [ + { + "id": "radius", + "label": "Radius", + "value": "1.6 Jupiter radii", + "source": { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S", + "label": "Stassun et al. 2017", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "radiusKm; radiusSource" + } + }, + { + "id": "period", + "label": "Year", + "value": "3.95 days", + "source": { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S", + "label": "Stassun et al. 2017", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "orbitalPeriodDays; orbitalPeriodSource" + } + }, + { + "id": "mass", + "label": "Mass", + "value": "0.50 Jupiter masses", + "source": { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S", + "label": "Stassun et al. 2017", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "massSource" + } + } + ], + "moreFacts": [] + }, + "datasets": { + "titleKey": "datasets", + "defaultDataset": "climate-model", + "controls": [ + { + "id": "thermal", + "label": "Thermal glow", + "qualification": "Black-body color at its measured dayside temperature, 1,535 K. The disc itself is unresolved.", + "thumbnail": "wasp-94-a-b-dataset-thermal.webp", + "surface": "wasp-94-a-b-surface-thermal@2x.webp", + "poles": "wasp-94-a-b-poles-thermal@2x.webp", + "source": { + "id": "wasp-94-a-b-thermal-color", + "path": "../manifest.json", + "url": "https://ui.adsabs.harvard.edu/abs/2023AJ....165..104D/abstract" + }, + "falseColor": false, + "notes": "The color of WASP-94 A b's heat: a black body at the dayside brightness temperature measured in secondary eclipse at 3.6 µm (Spitzer IRAC). Its star lights the day side; the night side is not measured. Reflected starlight is not included." + }, + { + "id": "climate-model", + "label": "Climate model", + "qualification": "Simulation · Met Office UM · Mukherjee et al. (2025) · False color", + "thumbnail": "wasp-94-a-b-dataset-climate-model.webp", + "surface": "wasp-94-a-b-surface-climate-model@2x.webp", + "poles": "wasp-94-a-b-poles-climate-model@2x.webp", + "source": { + "id": "wasp-94-a-b-climate-model-simulation", + "path": "../manifest.json", + "url": "https://arxiv.org/abs/2505.10910" + }, + "falseColor": true, + "noData": true, + "legend": { + "kind": "scale", + "title": "Temperature at 1 millibar", + "labels": [ + "900", + "1300", + "1700" + ], + "recipe": { + "palette": [ + [ + 0, + 0, + 4 + ], + [ + 87, + 16, + 110 + ], + [ + 188, + 55, + 84 + ], + [ + 249, + 142, + 9 + ], + [ + 252, + 255, + 164 + ] + ], + "labels": [ + "900", + "1300", + "1700" + ] + }, + "meta": "K", + "sourceUrl": "https://arxiv.org/abs/2505.10910" + }, + "notes": "Temperature at 1 millibar of WASP-94 A b in the Met Office UM simulation of Mukherjee et al. (2025): a model, not a measurement. The run assumes a hydrogen and helium atmosphere with the Sun's share of heavier elements and no clouds, on a planet that turns once for each orbit. The map is one instant, the last state the release holds, read where the pressure is 1 millibar. With JWST, Mukherjee et al. (2025) measured the evening edge 449 ± 83 K hotter than the morning edge; in this model it is about 300 K hotter at this pressure. Gray caps: no released grid samples beyond 89° north or south. The false color runs from 900 to 1,700 K; it is not what an eye would see. With shadows on, the star's light darkens the night half." + } + ] + }, + "settings": { + "titleKey": "settings", + "controls": [ + { + "kind": "toggle", + "name": "shadows", + "label": "Shadows", + "checked": false + } + ] + }, + "charts": [ + { + "id": "wasp-94-a-b-orbits", + "titleKey": "systemOrbits", + "src": "/scenes/wasp-94-a-b/wasp-94-a-b-system-orbits.svg", + "alt": "The orbits of WASP-94 A's planets in this map from above, to scale, with WASP-94 A b highlighted and the direction to Earth down. Each orbit is the one the app draws, from the published period, a/R*, eccentricity and argument of periastron; the star is a marker, not to scale.", + "source": { + "id": "wasp-94-a-b-observational-measurements", + "path": "../manifest.json", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S" + } + }, + { + "id": "wasp-94-a-b-transit", + "titleKey": "transitLightCurve", + "src": "/scenes/wasp-94-a-b/wasp-94-a-b-transit.svg", + "alt": "WASP-94 A b crossing its star, as TESS recorded it: 3 sectors (68, 95, 105) of the SPOC pipeline's 2-minute light curves of TIC 92352620, each transit divided by a straight line fitted to the light either side and folded onto the orbit the app draws, then averaged in 18-minute bins. Error bars are each bin's standard error.", + "source": { + "id": "wasp-94-a-b-tess-sector-68", + "path": "../manifest.json", + "url": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits" + } + } + ], + "resources": [ + { + "label": "Research", + "role": "facts", + "description": "Stassun et al. 2017", + "href": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract" + } + ], + "provenance": { + "editorial": { + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "Stassun et al. 2017" + }, + "physical": { + "path": "../../../../../packages/astronomy/data/bodies/wasp-94-a-b.json", + "credit": "Radius from Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive; mass from Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive; orbit from Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive" + } + } +} diff --git a/src/objects/wasp-94-a-b/source/manifest.json b/src/objects/wasp-94-a-b/source/manifest.json new file mode 100644 index 00000000000..8fbe4bd7c50 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/manifest.json @@ -0,0 +1,280 @@ +{ + "schema": "cssearth-authoritative-sources@3", + "inputs": [ + { + "id": "wasp-94-a-b-observational-measurements", + "path": "measurements.json", + "origin": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "Stassun et al. 2017", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Transcribed published measurements with their sources", + "redistribution": "Factual parameter transcription only; no paper figures", + "consumers": [ + "shape-model" + ], + "sourceBinding": { + "kind": "local", + "reason": "Measurements transcribed in this package with their sources; repinned when edited." + } + }, + { + "id": "wasp-94-a-b-preparation-raster", + "path": "preparation/raster.json", + "origin": "Repository-authored raster recipe: the shared neutral gray on the reference sphere, lit by the host star", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "assets", + "datasets" + ] + }, + { + "id": "wasp-94-a-b-preparation-geometry", + "path": "preparation/geometry.json", + "origin": "Repository-authored CSS geometry profile: 248-unit sphere, 16 x 32 leaves, lit material", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene", + "presentation" + ] + }, + { + "id": "wasp-94-a-b-preparation-presentation", + "path": "preparation/presentation.json", + "origin": "Repository-authored presentation profile: composite mode", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "presentation" + ] + }, + { + "id": "wasp-94-a-b-physical-solar-system-recipe", + "path": "presentation/solar-system.json", + "origin": "Repository-authored scene recipe: published radius, camera plan", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene" + ] + }, + { + "id": "wasp-94-a-b-thermal-color", + "path": "photometry/thermal-color.json", + "origin": "https://ui.adsabs.harvard.edu/abs/2023AJ....165..104D/abstract", + "credit": "Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2); CIE 1931 2° observer", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Authored method record: names the archive row chosen and the color computation applied", + "redistribution": "Method record only", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "local", + "reason": "Project-authored color recipe naming the cited brightness temperature; repinned when edited." + } + }, + { + "id": "wasp-94-a-b-tess-sector-68", + "path": "photometry/tess/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits", + "origin": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits", + "productId": "tess2023209231226-s0068-0000000092352620-0262-s_lc.fits", + "title": "TESS SPOC 2-minute light curve of WASP-94 A b's star (TIC 92352620), sector 68", + "sourceUrl": "https://mast.stsci.edu/portal/Mashup/Clients/Mast/Portal.html", + "credit": "NASA TESS mission; SPOC pipeline (Jenkins et al. 2016); obtained from MAST", + "displayCredit": "TESS · SPOC", + "license": "Public NASA mission data (MAST)", + "licenseEvidence": [ + "https://archive.stsci.edu/publishing/data-use" + ], + "acquisition": "MAST download of the SPOC light-curve file, unchanged; restored through source/preparation/acquisition.json.", + "redistribution": "Not redistributed in git; restored from MAST.", + "consumers": [ + "charts" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "mast-tess-spoc-tic-92352620", + "role": "material", + "evidence": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits" + } + ] + } + }, + { + "id": "wasp-94-a-b-tess-sector-95", + "path": "photometry/tess/tess2025206162959-s0095-0000000092352620-0292-s_lc.fits", + "origin": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2025206162959-s0095-0000000092352620-0292-s_lc.fits", + "productId": "tess2025206162959-s0095-0000000092352620-0292-s_lc.fits", + "title": "TESS SPOC 2-minute light curve of WASP-94 A b's star (TIC 92352620), sector 95", + "sourceUrl": "https://mast.stsci.edu/portal/Mashup/Clients/Mast/Portal.html", + "credit": "NASA TESS mission; SPOC pipeline (Jenkins et al. 2016); obtained from MAST", + "displayCredit": "TESS · SPOC", + "license": "Public NASA mission data (MAST)", + "licenseEvidence": [ + "https://archive.stsci.edu/publishing/data-use" + ], + "acquisition": "MAST download of the SPOC light-curve file, unchanged; restored through source/preparation/acquisition.json.", + "redistribution": "Not redistributed in git; restored from MAST.", + "consumers": [ + "charts" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "mast-tess-spoc-tic-92352620", + "role": "material", + "evidence": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2025206162959-s0095-0000000092352620-0292-s_lc.fits" + } + ] + } + }, + { + "id": "wasp-94-a-b-tess-sector-105", + "path": "photometry/tess/tess2026164183000-s0105-0000000092352620-0307-s_lc.fits", + "origin": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2026164183000-s0105-0000000092352620-0307-s_lc.fits", + "productId": "tess2026164183000-s0105-0000000092352620-0307-s_lc.fits", + "title": "TESS SPOC 2-minute light curve of WASP-94 A b's star (TIC 92352620), sector 105", + "sourceUrl": "https://mast.stsci.edu/portal/Mashup/Clients/Mast/Portal.html", + "credit": "NASA TESS mission; SPOC pipeline (Jenkins et al. 2016); obtained from MAST", + "displayCredit": "TESS · SPOC", + "license": "Public NASA mission data (MAST)", + "licenseEvidence": [ + "https://archive.stsci.edu/publishing/data-use" + ], + "acquisition": "MAST download of the SPOC light-curve file, unchanged; restored through source/preparation/acquisition.json.", + "redistribution": "Not redistributed in git; restored from MAST.", + "consumers": [ + "charts" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "mast-tess-spoc-tic-92352620", + "role": "material", + "evidence": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2026164183000-s0105-0000000092352620-0307-s_lc.fits" + } + ] + } + }, + { + "id": "wasp-94-a-b-climate-model-simulation", + "path": "science/mukherjee-2025/WASP-94Ab_GCM.nc", + "origin": "https://zenodo.org/records/15085825/files/GCM_products.zip?download=1", + "productId": "GCM_products/WASP-94Ab_GCM.nc", + "version": "10.5281/zenodo.15085825", + "sourceUrl": "https://zenodo.org/records/15085825", + "credit": "Mukherjee et al. (2025); Met Office UM simulation", + "displayCredit": "Mukherjee et al. (2025) · Met Office UM", + "license": "CC BY 4.0", + "licenseEvidence": [ + "https://zenodo.org/records/15085825", + "https://creativecommons.org/licenses/by/4.0/" + ], + "title": "Temperature at 1 millibar (air_temperature) of WASP-94 A b in the Met Office UM simulation: Cloudy Mornings and Clear Evenings on a Giant Extrasolar World", + "acquisition": "Restored through source/preparation/acquisition.json from Zenodo record 10.5281/zenodo.15085825: the member GCM_products/WASP-94Ab_GCM.nc of GCM_products.zip (546.1 MB), unchanged and whole, because a NetCDF-4 file spreads its structure through itself. A model output, not an observation.", + "redistribution": "Not redistributed in git; restored from Zenodo. CC BY 4.0 with attribution.", + "consumers": [ + "climate-model-simulation" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-wasp-94-a-b-climate-model-simulation", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + } + ], + "generatedIntermediates": [ + { + "id": "dataset-color-context-marker", + "path": "presentation/context.png", + "origin": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "The default dataset's color as a disc; rendered by packages/telescope-cli/src/source-authoring/context-markers.mts", + "license": "Project-authored display derivative.", + "consumers": [ + "navigation" + ], + "recipe": { + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "inputs": [ + "wasp-94-a-b-observational-measurements", + "wasp-94-a-b-preparation-raster", + "wasp-94-a-b-thermal-color", + "wasp-94-a-b-climate-model-simulation" + ] + }, + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "sourceBinding": { + "kind": "local", + "reason": "The default dataset drawn as a disc; `context-markers.mts --check` recomputes it." + } + } + ], + "documents": [ + { + "path": "content/object.json", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored factsheet, dataset recipe and legend." + } + }, + { + "path": "preparation/acquisition.json" + }, + { + "path": "preparation/navigation.json" + }, + { + "path": "preparation/rotation.json" + }, + { + "path": "preparation/new-object.json", + "sourceBinding": { + "kind": "local", + "reason": "The spec telescope new-object generated this package from, with the order it was given; `telescope new-object --refresh` regenerates from it." + } + }, + { + "path": "content/charts.json", + "sourceBinding": { + "kind": "local", + "reason": "Prepared chart recipes: the orbits drawn from the hosted-orbit records, and archive spectra with their units, errors and source paths." + } + } + ] +} diff --git a/src/objects/wasp-94-a-b/source/measurements.json b/src/objects/wasp-94-a-b/source/measurements.json new file mode 100644 index 00000000000..971b01c246e --- /dev/null +++ b/src/objects/wasp-94-a-b/source/measurements.json @@ -0,0 +1,13 @@ +{ + "schema": "cssearth-hosted-planet@1", + "id": "wasp-94-a-b", + "radiusKm": 112957.4, + "radiusSource": "Radius 1.58 Jupiter radii from Stassun et al. 2017 (2017AJ....153..136S), via the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract): 112,957.4 km at 71,492 km per Jupiter radius. GM from the mass 0.5 Jupiter masses (Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive, https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract) times JPL's Jupiter GM. A sphere: no oblateness is measured.", + "orbitalPeriodDays": 3.9502001, + "orbitalPeriodSource": "Kokori et al. 2023 (2023ApJS..265....4K), via the NASA Exoplanet Archive ps table (pl_refname KOKORI_ET_AL__2023): P 3.9502001 d", + "shape": { + "kind": "sphere", + "qualification": "A sphere at the published radius in the shared neutral gray. No image, color, map or oblateness of WASP-94 A b is measured; only its size, mass and orbit are." + }, + "massSource": "0.5 Jupiter masses: Stassun et al. 2017, the mass the NASA Exoplanet Archive's composite table adopts (2017AJ....153..136S), via the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract)." +} diff --git a/src/objects/wasp-94-a-b/source/photometry/thermal-color.json b/src/objects/wasp-94-a-b/source/photometry/thermal-color.json new file mode 100644 index 00000000000..4c94b8364aa --- /dev/null +++ b/src/objects/wasp-94-a-b/source/photometry/thermal-color.json @@ -0,0 +1,20 @@ +{ + "schema": "cssearth-stellar-photometric-color@1", + "objectId": "wasp-94-a-b", + "spectrum": "planck", + "temperature": { + "published": { + "kelvin": 1535, + "lowerKelvin": 1497, + "upperKelvin": 1573, + "citation": "Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2): 1535 +/- 38 K, https://ui.adsabs.harvard.edu/abs/2023AJ....165..104D/abstract" + }, + "note": "Its day side's brightness temperature is measured in secondary eclipse at 3.6 µm (Spitzer IRAC) and nobody has imaged it, so the color is a Planck spectrum at its published temperature.", + "chosen": "2 measured of 2 rows; the smallest relative uncertainty, then the longest wavelength", + "wavelengthMicrometres": 3.6, + "facility": "Spitzer IRAC" + }, + "whitePoint": "sRGB D65", + "normalization": "brightest linear sRGB channel = 1", + "gamut": "desaturate" +} diff --git a/src/objects/wasp-94-a-b/source/preparation/acquisition.json b/src/objects/wasp-94-a-b/source/preparation/acquisition.json new file mode 100644 index 00000000000..c2e35c2de99 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/acquisition.json @@ -0,0 +1,42 @@ +{ + "schema": "cssearth-acquisition-plan@1", + "operations": [ + { + "kind": "download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/tess/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits", + "url": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits" + }, + { + "kind": "download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/tess/tess2025206162959-s0095-0000000092352620-0292-s_lc.fits", + "url": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2025206162959-s0095-0000000092352620-0292-s_lc.fits" + }, + { + "kind": "download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/tess/tess2026164183000-s0105-0000000092352620-0307-s_lc.fits", + "url": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2026164183000-s0105-0000000092352620-0307-s_lc.fits" + }, + { + "kind": "zip-member", + "groups": [ + "restore", + "refresh" + ], + "path": "science/mukherjee-2025/WASP-94Ab_GCM.nc", + "url": "https://zenodo.org/records/15085825/files/GCM_products.zip?download=1", + "member": "GCM_products/WASP-94Ab_GCM.nc" + } + ] +} diff --git a/src/objects/wasp-94-a-b/source/preparation/geometry.json b/src/objects/wasp-94-a-b/source/preparation/geometry.json new file mode 100644 index 00000000000..bef0eda3e5f --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/geometry.json @@ -0,0 +1,62 @@ +{ + "schema": "cssearth-css-geometry-profile@1", + "namespace": "wasp-94-a-b", + "surface": { + "radius": 248, + "polarRadius": 248, + "latitudeSegments": 16, + "longitudeSegments": 32, + "surface": { + "url": "/scenes/wasp-94-a-b/wasp-94-a-b-surface-thermal@2x.webp", + "width": 1024, + "height": 768 + }, + "surfaceLatitudeHeight": 512, + "packedBandGutter": 8, + "poles": { + "url": "/scenes/wasp-94-a-b/wasp-94-a-b-poles-thermal@2x.webp", + "width": 512, + "height": 256 + }, + "polarTileSize": 256, + "polarRadiusScale": 1.035, + "polarOffset": 0.1, + "uv": "cell", + "color": "#ff6f00" + }, + "projection": { + "tileSize": 50, + "layerElevation": 50, + "seamBleed": 0, + "interiorSeamBleed": 8, + "overlap": 0.015625, + "seamOutset": { + "targetPixels": 0.5, + "stepRatio": 1.4142135623730951, + "hysteresis": 0.1, + "firstDiameter": 16, + "lastDiameter": 32768 + }, + "fitToSource": false, + "rasterScale": 2, + "rasterGutter": 8, + "rasterOverscan": 0.5, + "projectivePoles": false, + "lightColor": "#ffffff", + "ambientIntensity": 0.05 + }, + "bodyRotationDegrees": 0, + "output": { + "schema": "csswasp-94-a-b-prepared-runtime-scene@1", + "materialSchema": "csswasp-94-a-b-prepared-lighting@1", + "layout": "body-container", + "body": { + "axialTiltDegrees": 0, + "rotationDirection": "prograde", + "rotationPeriodEarthDays": 3.9502001, + "sourceProjection": "no observation: the shared neutral gray of an unresolved surface on the reference sphere", + "polarPreparation": "the same gray on the polar tiles", + "axialTiltNote": "no obliquity of this planet is measured; the rotation record assumes the spin axis on the orbit normal, as tidal locking implies" + } + } +} diff --git a/src/objects/wasp-94-a-b/source/preparation/navigation.json b/src/objects/wasp-94-a-b/source/preparation/navigation.json new file mode 100644 index 00000000000..d8353d9631e --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/navigation.json @@ -0,0 +1,41 @@ +{ + "schema": "cssearth-navigation-marker@2", + "objectId": "wasp-94-a-b", + "owner": "object", + "presentation": { + "size": 5 + }, + "source": { + "path": "presentation/context.png" + }, + "operations": [ + { + "type": "resize", + "width": "tile", + "height": "tile", + "fit": "cover", + "position": "centre", + "kernel": "lanczos3" + }, + { + "type": "ensure-alpha" + }, + { + "type": "ellipse-mask", + "cx": 0.5, + "cy": 0.5, + "rx": 0.45, + "ry": 0.45, + "shading": { + "ambient": 0.35, + "diffuse": 0.65 + } + }, + { + "type": "png" + } + ], + "context": { + "pixels": 512 + } +} diff --git a/src/objects/wasp-94-a-b/source/preparation/new-object.json b/src/objects/wasp-94-a-b/source/preparation/new-object.json new file mode 100644 index 00000000000..c1ce0e4035b --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/new-object.json @@ -0,0 +1,33 @@ +{ + "host": "wasp-94-a", + "planets": [ + { + "id": "wasp-94-a-b", + "name": "WASP-94 A b", + "description": "Transiting planet of WASP-94 A with a 3.95-day year, found in 2014.", + "paper": { + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "Stassun et al. 2017" + }, + "orbit": { + "archive": "nasa-ps", + "reference": "STASSUN_ET_AL__2017" + }, + "text": { + "card": "WASP-94 A b was found in 2014 by SuperWASP as it crossed its star.", + "introduction": "It is the only planet known around WASP-94 A. Its orbit and size follow Stassun et al. 2017's fit, the archive's default.", + "locator": "NASA Exoplanet Archive ps table, default parameter set (pl_refname STASSUN_ET_AL__2017): disc_year 2014, disc_facility SuperWASP, sy_pnum 2, 1 of them with hostname WASP-94 A; pl_orbper 3.9502001, pl_radj 1.58, pl_bmassj 0.5" + }, + "thermal": { + "temperatureK": 1535, + "uncertaintyK": 38, + "wavelengthMicrometres": 3.6, + "facility": "Spitzer IRAC", + "source": "Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2)", + "url": "https://ui.adsabs.harvard.edu/abs/2023AJ....165..104D/abstract", + "chosen": "2 measured of 2 rows; the smallest relative uncertainty, then the longest wavelength" + }, + "order": 5779 + } + ] +} diff --git a/src/objects/wasp-94-a-b/source/preparation/presentation.json b/src/objects/wasp-94-a-b/source/preparation/presentation.json new file mode 100644 index 00000000000..e5db5c7e6d9 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/presentation.json @@ -0,0 +1,5 @@ +{ + "schema": "cssearth-css-presentation-profile@2", + "namespace": "wasp-94-a-b", + "mode": "composite" +} diff --git a/src/objects/wasp-94-a-b/source/preparation/raster.json b/src/objects/wasp-94-a-b/source/preparation/raster.json new file mode 100644 index 00000000000..31712463d72 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/raster.json @@ -0,0 +1,113 @@ +{ + "schema": "cssearth-raster-recipe@2", + "publicBase": "/scenes/wasp-94-a-b/", + "sourceWidth": 1024, + "sourceHeight": 512, + "width": 1024, + "height": 512, + "latitudeBands": 16, + "polarTile": 256, + "resample": "density-before-pack", + "polarProjection": "orthographic-bilinear", + "polesOutput": "wasp-94-a-b-poles-{id}{suffix}.webp", + "surfaceMetadata": { + "schema": "csswasp-94-a-b-prepared-assets@1" + }, + "thumbnail": { + "size": 64, + "centerLongitudeDegrees": 0 + }, + "surfaces": [ + { + "id": "thermal", + "output": "wasp-94-a-b-surface-{id}{suffix}.webp", + "thumbnail": "wasp-94-a-b-dataset-{id}.webp", + "source": "photometry/thermal-color.json", + "falseColor": false, + "science": { + "kind": "dayside-thermal-color", + "qualification": "Color of a black body at the dayside brightness temperature 1,535 K measured in secondary eclipse at 3.6 µm (Deming et al. 2023, dayside brightness temperature at 3.6 µm (uniform reanalysis of Spitzer's eclipses, CDS J/AJ/165/104 table 2)), uniform over the disc and lit by its star so the day side faces it. The planet is unresolved: no map is implied, and reflected starlight is not included. Its black-body color lies outside the sRGB gamut and is shown mixed with 2% white, the least that brings it inside, hue kept." + } + }, + { + "id": "climate-model", + "output": "wasp-94-a-b-surface-{id}{suffix}.webp", + "thumbnail": "wasp-94-a-b-dataset-{id}.webp", + "source": "science/mukherjee-2025/WASP-94Ab_GCM.nc", + "falseColor": true, + "science": { + "kind": "terrestrial-scientific", + "id": "climate-model", + "label": "Climate model", + "format": "netcdf-lonlat-field", + "path": "science/mukherjee-2025/WASP-94Ab_GCM.nc", + "variable": "air_temperature", + "coordinates": { + "longitude": "longitude_0", + "latitude": "latitude" + }, + "select": { + "time": 1 + }, + "isobar": { + "along": "level_height_0", + "pressure": "air_pressure", + "pressureUnits": "Pa", + "at": 100 + }, + "sourceUnits": "K", + "longitudeZeroAt": 180, + "coordinateToleranceDegrees": 0.005, + "consumer": "climate-model-simulation", + "sampling": "bilinear", + "displaySampling": "nearest", + "outputLongitudeOrigin": 0, + "units": "K", + "minimum": 900, + "maximum": 1700, + "colors": [ + "#000004", + "#57106e", + "#bc3754", + "#f98e09", + "#fcffa4" + ], + "labels": [ + "900", + "1300", + "1700" + ], + "description": "Simulated temperature at 1 millibar from the Met Office UM model, a published simulation and not a measurement. Gray caps: no released grid samples beyond 89° north or south.", + "title": "Climate model · Met Office UM simulation", + "sourceUrl": "https://arxiv.org/abs/2505.10910" + } + } + ], + "lighting": { + "bank": "sphere", + "presentationSize": 460, + "defaultFrame": 230, + "bankSchema": "csswasp-94-a-b-prepared-lighting-bank@1", + "billboardSchema": "csswasp-94-a-b-prepared-lighting-billboard@1", + "metadata": { + "schema": "csswasp-94-a-b-prepared-lighting@1", + "storageModel": "prepared-full-resolution-density-row-shards", + "model": "prepared-full-phase-lambert-cubic-sky-sun-no-atmosphere", + "sourceRadius": "measurements.json#radiusKm", + "limbMeaning": "The adopted source-radius opaque-sphere silhouette and prepared phase lighting define the limb; no atmospheric rim is inferred.", + "sourceRenderer": "OpenSpace@56e29b54/modules/globebrowsing/shaders/texturetilemapping.glsl", + "ambientIntensity": 0.05, + "shadowlessFloodLimbFloor": 0.35, + "orenNayarRoughness": 0, + "terminatorSmoothstep": [ + 0, + 0.1 + ], + "minimumLightViewZ": -1, + "maximumLightViewZ": 1, + "baseLightAzimuthDegrees": 0, + "cameraContract": "unbounded-accumulated-matrix3d-phase-and-roll", + "runtimeRasterization": false + } + } +} diff --git a/src/objects/wasp-94-a-b/source/preparation/rotation.json b/src/objects/wasp-94-a-b/source/preparation/rotation.json new file mode 100644 index 00000000000..cea67f4597e --- /dev/null +++ b/src/objects/wasp-94-a-b/source/preparation/rotation.json @@ -0,0 +1,7 @@ +{ + "schema": "cssearth-synchronous-rotation@1", + "host": "wasp-94-a", + "source": "Assumed synchronous rotation: a planet 8 stellar radii from its star is expected to be tidally locked, and no rotation period of WASP-94 A b is measured in the sources this package cites. Pole, prime meridian and rate are computed from the hosted orbit in packages/astronomy/data/bodies/wasp-94-a-b.json.", + "coordinateSystem": "ICRF/J2000. +Z is the orbit normal (prograde spin); +X points at the host star at each instant, so longitude 0 is the substellar point; east longitude, the direction of rotation.", + "qualification": "Tidal locking and a spin axis on the orbit normal are assumptions, not measurements: neither the rotation period nor the obliquity of WASP-94 A b is measured." +} diff --git a/src/objects/wasp-94-a-b/source/presentation/context.png b/src/objects/wasp-94-a-b/source/presentation/context.png new file mode 100644 index 00000000000..f4fea4b6cd5 Binary files /dev/null and b/src/objects/wasp-94-a-b/source/presentation/context.png differ diff --git a/src/objects/wasp-94-a-b/source/presentation/solar-system.json b/src/objects/wasp-94-a-b/source/presentation/solar-system.json new file mode 100644 index 00000000000..1a3438e7a10 --- /dev/null +++ b/src/objects/wasp-94-a-b/source/presentation/solar-system.json @@ -0,0 +1,9 @@ +{ + "schema": "cssearth-solar-system-preparation@1", + "bodyId": "wasp-94-a-b", + "displayName": "WASP-94 A b", + "bodyRadiusUnits": 248, + "bodyRadiusKilometers": 112957.4, + "defaultZoom": 1.25, + "geometryScale": 1.25 +} diff --git a/src/objects/wasp-94-a-b/text.json b/src/objects/wasp-94-a-b/text.json new file mode 100644 index 00000000000..4b2abe8e093 --- /dev/null +++ b/src/objects/wasp-94-a-b/text.json @@ -0,0 +1,40 @@ +{ + "schema": "cssearth-object-text@1", + "objectId": "wasp-94-a-b", + "card": { + "text": "WASP-94 A b was found in 2014 by SuperWASP as it crossed its star.", + "sources": [ + { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "label": "Stassun et al. 2017", + "checked": "2026-10-05", + "locator": "NASA Exoplanet Archive ps table, default parameter set (pl_refname STASSUN_ET_AL__2017): disc_year 2014, disc_facility SuperWASP, sy_pnum 2, 1 of them with hostname WASP-94 A; pl_orbper 3.9502001, pl_radj 1.58, pl_bmassj 0.5" + } + ] + }, + "introduction": { + "text": "It is the only planet known around WASP-94 A. Its orbit and size follow Stassun et al. 2017's fit, the archive's default.", + "sources": [ + { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "label": "Stassun et al. 2017", + "checked": "2026-10-05", + "locator": "NASA Exoplanet Archive ps table, default parameter set (pl_refname STASSUN_ET_AL__2017): disc_year 2014, disc_facility SuperWASP, sy_pnum 2, 1 of them with hostname WASP-94 A; pl_orbper 3.9502001, pl_radj 1.58, pl_bmassj 0.5" + } + ] + }, + "datasets": { + "thermal": { + "title": "Dayside heat", + "detail": "From its dayside temperature", + "summary": "The color of a black body at the 1,535 K day side measured in eclipse." + }, + "climate-model": { + "title": "Met Office UM simulation", + "detail": "Published simulation", + "summary": "Temperature at 1 millibar in a published model, in false color. One scenario, not a measurement; gray caps lack samples." + } + } +} diff --git a/src/objects/wasp-94-a-system/inventory.json b/src/objects/wasp-94-a-system/inventory.json new file mode 100644 index 00000000000..d38d3cb5ad7 --- /dev/null +++ b/src/objects/wasp-94-a-system/inventory.json @@ -0,0 +1,29 @@ +{ + "schema": "cssearth-inventory@1", + "assets": [ + { + "location": "prepared", + "filename": "members.json", + "bytes": 1148, + "sha256": "5975f8367bcc4542f20922dbf8a7bcdc6f8273a6f733b1549b783b684e4a5986" + }, + { + "location": "prepared", + "filename": "orbits/wasp-94-a-b.bin", + "bytes": 1948, + "sha256": "ca9f3f9b471073c8e62c67c73ba2d6cbf885c7745b4b9c07f839a1aa9ec835cc" + }, + { + "location": "prepared", + "filename": "plain-stars.bin", + "bytes": 944, + "sha256": "60db77fc50298f92845114b6053edae7ea27b81a4a42c9aab5453318bdd0a3cc" + }, + { + "location": "prepared", + "filename": "views/wasp-94-a.json", + "bytes": 30143, + "sha256": "893762f69aa12fe01f2d9fa5b682bc3e79e82cfd422e3c53dc10ac6103bd956c" + } + ] +} diff --git a/src/objects/wasp-94-a-system/object.json b/src/objects/wasp-94-a-system/object.json new file mode 100644 index 00000000000..30512c745a6 --- /dev/null +++ b/src/objects/wasp-94-a-system/object.json @@ -0,0 +1,47 @@ +{ + "schema": "cssearth-object@2", + "id": "wasp-94-a-system", + "parent": "milky-way", + "type": "system", + "generator": "site/build/prepare/system-packages.mts", + "properties": { + "system": { + "host": "wasp-94-a" + }, + "catalog": { + "name": "WASP-94 system", + "systemName": "WASP-94 system", + "classification": "planetary-system", + "color": "#fff7fd", + "distanceAu": 43425864.2, + "description": "WASP-94 A b crosses WASP-94 A as seen from Earth, which is how it was found." + }, + "worldFrame": { + "referenceFrame": "sun-icrf", + "epochJdTt": 2461286.5, + "originM": [ + 3720623772194467000, + -3882089733927324700, + -3645513620204698600 + ], + "presentationToReference": [ + 0.572720276913402, + -0.38828159869801654, + -0.7219618303793828, + -0.5975749346238792, + 0.40513341263663627, + -0.6919328836488836, + -0.5611556663899089, + -0.8277102863187732, + -7.693145033235067e-7 + ], + "orbitUpReference": [ + 0.38828159869801654, + -0.40513341263663627, + 0.8277102863187732 + ], + "metersPerUnit": 3321406.4516129033, + "bodyRadiusM": 1029636000 + } + } +} diff --git a/src/objects/wasp-94-a/NOTICE.md b/src/objects/wasp-94-a/NOTICE.md new file mode 100644 index 00000000000..05a633e9e6e --- /dev/null +++ b/src/objects/wasp-94-a/NOTICE.md @@ -0,0 +1,9 @@ +# WASP-94 A credits + +Radius, mass and temperature: Radius 1.48 +/- 0.12 solar radii from Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract); Mass 1.67 +/- 0.64 solar masses from Stassun et al. 2017, the stellar mass of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract); temperature from Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive. + +Color: Gaia DR3 XP spectrum, source 6780546169634170496, through the CIE 1931 2° color-matching functions (CIE 2019, CC BY-SA 4.0, doi:10.25039/CIE.DS.xvudnb9b). + +Limb darkening: Claret & Bloemen (2011), A&A 529, A75, via VizieR J/A+A/529/A75. + +Placement: Gaia DR3 source 6780546169634170496: position, parallax, proper motion and radial velocity. This work has made use of data from the European Space Agency (ESA) mission Gaia, processed by the Gaia Data Processing and Analysis Consortium (DPAC). Identifiers: SIMBAD, CDS, Strasbourg. diff --git a/src/objects/wasp-94-a/README.md b/src/objects/wasp-94-a/README.md new file mode 100644 index 00000000000..4d2edf5f41f --- /dev/null +++ b/src/objects/wasp-94-a/README.md @@ -0,0 +1,23 @@ +# WASP-94 A + +## Sources + +Its radius and temperature follow Stassun et al. 2017. The introduction is generated from Stassun et al. 2017's published values; the sections below are the data's own. + +**Star.** Placement: Gaia DR3 source 6780546169634170496, parallax 4.750 ± 0.024 mas (210.53 pc). Radius 1.48 +/- 0.12 solar radii from Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract). Mass 1.67 +/- 0.64 solar masses from Stassun et al. 2017, the stellar mass of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract). Temperature 6,170 K from Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive. log g 4.32 from the mass and radius. + +**Color.** Gaia DR3 XP spectrum, source 6780546169634170496, through the CIE 1931 2° observer: #fff7fd. Routes tried in order: stis-ngsl: no HD number in SIMBAD; pulkovo: no HR number; kiehling: no HR number; kharitonov: no HR number; burnashev: no HR (BS) number; gaia-xp: used. + +**Limb.** The disc is dimmed toward the limb by the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,170 K and log g 4.32 (u1 0.388, u2 0.299): a model, because no fit of this star's limb is used. + +## Evidence + +Generated 2026-10-05 by [new-object-cli.mts](../../../packages/telescope-cli/src/new-object/new-object-cli.mts) from Gaia DR3, SIMBAD and the archives named above; each choice was read with the dataset's own reader. + + +## Known problems + +- **Assumptions of the frame.** The axis's position angle and the rotation phase are conventions. +- **Model limb.** The limb darkening is a model atmosphere at the catalogued temperature and gravity, not a measurement of this star. + +[Investigation ledger](investigations.json) · [Inputs](source/manifest.json) · [Preparation](source/preparation) · [Delivered files](inventory.json) · [Credits](NOTICE.md) diff --git a/src/objects/wasp-94-a/inventory.json b/src/objects/wasp-94-a/inventory.json new file mode 100644 index 00000000000..9e0c779c31c --- /dev/null +++ b/src/objects/wasp-94-a/inventory.json @@ -0,0 +1,125 @@ +{ + "schema": "cssearth-inventory@1", + "assets": [ + { + "location": "public", + "filename": "wasp-94-a-arrival.webp", + "bytes": 26796, + "sha256": "72310da89cbd424092fbc4c4003ccbad21fd92f6534690fcc5e93acbbac7c779" + }, + { + "location": "public", + "filename": "wasp-94-a-billboard.webp", + "bytes": 4756, + "sha256": "d27eb22323d10fb0cb824d1835305db89eeb76e4393f27c8cfa2511c85dec99a" + }, + { + "location": "public", + "filename": "wasp-94-a-dataset-color.webp", + "bytes": 44, + "sha256": "606e10bda584f4ea854abe0517f6e2a8383577236dbcfc116391d9fc0be1dd55" + }, + { + "location": "public", + "filename": "wasp-94-a-limb-color@2x.webp", + "bytes": 96952, + "sha256": "fd802f878e8a3fe0a85b0e810af3e769ff25e10cbcb53bf5d99ef6d43e8bf481" + }, + { + "location": "public", + "filename": "wasp-94-a-poles-color@2x.webp", + "bytes": 1340, + "sha256": "2701d6b6df0a4967067a429ce25b47959ac4846c5670231017db772c16e302f8" + }, + { + "location": "public", + "filename": "wasp-94-a-surface-color@2x.webp", + "bytes": 38, + "sha256": "4afdce5842c3caf522dabf7d34ae3721186f14ca9b9684aafad6d6e681782b9c" + }, + { + "location": "prepared", + "filename": "arrival-billboard.json", + "bytes": 385, + "sha256": "64d35f687fa98d2d6b4d72d4096beb005fb594eb109e6eec27c2605bb260424d" + }, + { + "location": "prepared", + "filename": "assets.json", + "bytes": 1694, + "sha256": "20da2ba4e6a7dc217a01e44984bddbe5834ef84b22035886bdfae6585e0cae47" + }, + { + "location": "prepared", + "filename": "authored-preparation.json", + "bytes": 662, + "sha256": "b404b0462513509a818232db047ea0b3631a8852eca6c77645c95d52c6ec6f5e" + }, + { + "location": "prepared", + "filename": "content.json", + "bytes": 1343, + "sha256": "7ed62c727e8aba0bb9cde92dd544cc0a39699620052da3769909f25eb7045fb3" + }, + { + "location": "prepared", + "filename": "controls.json", + "bytes": 368, + "sha256": "a9675b6cdd2db8f02ed68aade07d42b287c73b7e9e309886be13df394c3ca650" + }, + { + "location": "prepared", + "filename": "datasets.json", + "bytes": 1078, + "sha256": "12250cd5092d0084a9269d2c808a6851a7dc40fcd7a6c3dd747fdefb78b8c651" + }, + { + "location": "prepared", + "filename": "minimaps.json", + "bytes": 101, + "sha256": "087c2291c1fc076eb71a6d0d7ff5d5b77074bd9ac9b4e63929c77733a8528617" + }, + { + "location": "prepared", + "filename": "minimaps/color.webp", + "bytes": 432, + "sha256": "04caaccdefcf9e1804030daf55ecab57418e9f3b76d1e4659118b68994c77b5d" + }, + { + "location": "prepared", + "filename": "runtime.json", + "bytes": 249514, + "sha256": "645eb8b82319736defb7f2b08b803df461c98da6e54348bdc33788243c6ce7e1" + }, + { + "location": "prepared", + "filename": "scene.json", + "bytes": 393839, + "sha256": "45bf8e6feef9ac6c68eff441d0e7d925b1740fb551085fdf793560e9e51819b5" + }, + { + "location": "prepared", + "filename": "sky.json", + "bytes": 1204, + "sha256": "b03b188b8d44be74a3e95cf2bcfc8dae04dc51f6c5ae49391f2380ac6236a021" + }, + { + "location": "prepared", + "filename": "sun.json", + "bytes": 5, + "sha256": "38e0b9de817f645c4bec37c0d4a3e58baecccb040f5718dc069a72c7385a0bed" + }, + { + "location": "prepared", + "filename": "text.json", + "bytes": 1085, + "sha256": "519ee6a5ecec685e79050cacc20e3ef67c6f90de609bb31b86aa041310f57989" + }, + { + "location": "prepared", + "filename": "world-navigation.json", + "bytes": 1177, + "sha256": "274f5055a6bd7d70e9efb23f284addb26e9e42ae16cdadff9e9ce4fae3dae5a1" + } + ] +} diff --git a/src/objects/wasp-94-a/investigations.json b/src/objects/wasp-94-a/investigations.json new file mode 100644 index 00000000000..e332516843c --- /dev/null +++ b/src/objects/wasp-94-a/investigations.json @@ -0,0 +1,42 @@ +{ + "schema": "cssearth-investigation-ledger@1", + "objectId": "wasp-94-a", + "entries": [ + { + "id": "placement", + "subject": "Placement", + "status": "included", + "finding": "Gaia DR3 source 6780546169634170496: position at J2016.0. Distance: Gaia DR3 parallax 4.7498 +/- 0.0242 mas (same row, RUWE 0.98): 210.53 pc, no zero-point correction. Proper motion: Gaia DR3 (same row): 26.500, -44.971 mas/yr. Radial velocity from the same row.", + "evidence": [ + "https://gea.esac.esa.int/archive/" + ] + }, + { + "id": "radius-mass-and-temperature", + "subject": "Radius, mass and temperature", + "status": "included", + "finding": "Radius 1.48 +/- 0.12 solar radii from Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract). Mass 1.67 +/- 0.64 solar masses from Stassun et al. 2017, the stellar mass of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract). Temperature 6170 +/- 80 K from Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive.", + "evidence": [ + "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract" + ] + }, + { + "id": "color", + "subject": "Color", + "status": "included", + "finding": "Gaia DR3 XP spectrum, source 6780546169634170496, through the CIE 1931 2-degree observer: #fff7fd. Routes tried in order: stis-ngsl: no HD number in SIMBAD; pulkovo: no HR number; kiehling: no HR number; kharitonov: no HR number; burnashev: no HR (BS) number; gaia-xp: used.", + "evidence": [ + "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3" + ] + }, + { + "id": "limb-darkening", + "subject": "Limb darkening", + "status": "included", + "finding": "The disc is dimmed toward the limb by the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,170 K and log g 4.32 (u1 0.388, u2 0.299): a model, because no fit of this star's limb is used.", + "evidence": [ + "https://vizier.cds.unistra.fr/viz-bin/VizieR?-source=J/A+A/529/A75" + ] + } + ] +} diff --git a/src/objects/wasp-94-a/object.json b/src/objects/wasp-94-a/object.json new file mode 100644 index 00000000000..6e2fc69cce7 --- /dev/null +++ b/src/objects/wasp-94-a/object.json @@ -0,0 +1,141 @@ +{ + "schema": "cssearth-object@2", + "id": "wasp-94-a", + "parent": "wasp-94-a-system", + "type": "layered-body", + "properties": { + "preparation": { + "schema": "cssearth-object-preparation@1", + "label": "WASP-94 A", + "steps": [ + "verify-sources", + "assets", + "panel-content", + "datasets", + "starfield", + "scene", + "controls", + "presentation", + "runtime-assets" + ] + }, + "recipe": { + "schema": "cssearth-authored-object@2", + "surfaces": [ + { + "id": "body", + "source": "geometry", + "projection": "equirectangular", + "datasets": [ + { + "id": "color", + "source": "content", + "material": "emission" + } + ] + } + ], + "shape": { + "kind": "sphere", + "radiusKm": 1029636 + }, + "materials": [ + { + "id": "emission", + "source": "raster", + "model": "emissive" + } + ], + "sources": [ + { + "id": "raster", + "path": "source/preparation/raster.json" + }, + { + "id": "geometry", + "path": "source/preparation/geometry.json" + }, + { + "id": "presentation", + "path": "source/preparation/presentation.json" + }, + { + "id": "content", + "path": "source/content/object.json" + }, + { + "id": "solar-system", + "path": "source/presentation/solar-system.json" + }, + { + "id": "rotation", + "path": "source/preparation/rotation.json" + }, + { + "id": "navigation", + "path": "source/preparation/navigation.json" + }, + { + "id": "acquisition", + "path": "source/preparation/acquisition.json" + } + ], + "emission": { + "source": "raster", + "material": "emission" + } + }, + "page": { + "stylesheets": [ + "src/renderers/css/styles/body-surfaces.css", + "src/renderers/css/styles/wasp-94-a-surfaces.css" + ], + "metadata": { + "url": "prepared/page.json" + } + }, + "catalog": { + "name": "WASP-94 A", + "classification": "star", + "color": "#fff7fd", + "distanceAu": 43425864.2, + "description": "WASP-94 A b crosses WASP-94 A as seen from Earth, which is how it was found.", + "systemName": "WASP-94 system", + "order": 5778, + "context": { + "order": 5775 + } + }, + "worldFrame": { + "referenceFrame": "sun-icrf", + "epochJdTt": 2461286.5, + "originM": [ + 3720623772194467000, + -3882089733927324700, + -3645513620204698600 + ], + "presentationToReference": [ + 0.572720276913402, + -0.38828159869801654, + -0.7219618303793828, + -0.5975749346238792, + 0.40513341263663627, + -0.6919328836488836, + -0.5611556663899089, + -0.8277102863187732, + -7.693145033235067e-7 + ], + "orbitUpReference": [ + 0.38828159869801654, + -0.40513341263663627, + 0.8277102863187732 + ], + "metersPerUnit": 3321406.4516129033, + "bodyRadiusM": 1029636000 + } + }, + "prepared": { + "format": "cssearth-css-object@5", + "url": "prepared/object.json" + } +} diff --git a/src/objects/wasp-94-a/source/content/object.json b/src/objects/wasp-94-a/source/content/object.json new file mode 100644 index 00000000000..261f6da2dd4 --- /dev/null +++ b/src/objects/wasp-94-a/source/content/object.json @@ -0,0 +1,81 @@ +{ + "schema": "cssearth-object-content@2", + "version": 1, + "id": "wasp-94-a", + "displayName": "WASP-94 A", + "panel": { + "facts": [ + { + "id": "radius", + "label": "Radius", + "value": "1.5 solar radii", + "source": { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S", + "label": "Stassun et al. (2017), The Astronomical Journal 153 136", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "radiusKm; radiusSource" + } + }, + { + "id": "distance", + "label": "Distance from the Sun", + "value": "211 parsecs", + "source": { + "catalogueId": "gaia-dr3-wasp-94-a", + "url": "https://gea.esac.esa.int/archive/", + "label": "Gaia DR3 parallax", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "distanceParsecs; distanceSource" + } + } + ], + "moreFacts": [] + }, + "datasets": { + "titleKey": "datasets", + "defaultDataset": "color", + "controls": [ + { + "id": "color", + "label": "Color", + "qualification": "Color from Gaia's spectrum of the star; darkening toward the edge from a model atmosphere. The disc itself is unresolved.", + "thumbnail": "wasp-94-a-dataset-color.webp", + "surface": "wasp-94-a-surface-color@2x.webp", + "poles": "wasp-94-a-poles-color@2x.webp", + "source": { + "id": "wasp-94-a-stellar-color", + "path": "../manifest.json", + "url": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3" + }, + "falseColor": false, + "notes": "The color of the star's Gaia DR3 BP/RP spectrum. The darkening toward the edge is the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,170 K and log g 4.32 (u1 0.388, u2 0.299): a model, because no fit of this star's limb is used. The spin axis's direction on the sky is a display convention." + } + ] + }, + "settings": { + "titleKey": "settings", + "controls": [] + }, + "charts": [], + "resources": [ + { + "label": "Research", + "role": "facts", + "description": "Stassun et al. 2017", + "href": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract" + } + ], + "provenance": { + "editorial": { + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "Stassun et al. 2017" + }, + "physical": { + "path": "../../../../../packages/astronomy/data/bodies/wasp-94-a.json", + "credit": "Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive radius; Gaia DR3 astrometry; no measured rotation axis (display convention)" + } + } +} diff --git a/src/objects/wasp-94-a/source/manifest.json b/src/objects/wasp-94-a/source/manifest.json new file mode 100644 index 00000000000..73e900ccd8a --- /dev/null +++ b/src/objects/wasp-94-a/source/manifest.json @@ -0,0 +1,232 @@ +{ + "schema": "cssearth-authoritative-sources@3", + "inputs": [ + { + "id": "wasp-94-a-observational-measurements", + "path": "measurements.json", + "origin": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "Stassun et al. 2017", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Transcribed published measurements with their sources", + "redistribution": "Factual parameter transcription only; no paper figures", + "consumers": [ + "shape-model" + ], + "sourceBinding": { + "kind": "local", + "reason": "Measurements transcribed in this package with their sources; repinned when edited." + } + }, + { + "id": "wasp-94-a-preparation-raster", + "path": "preparation/raster.json", + "origin": "Repository-authored raster recipe: the shared neutral gray on the reference sphere, transparent plates", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "assets", + "datasets" + ] + }, + { + "id": "wasp-94-a-preparation-geometry", + "path": "preparation/geometry.json", + "origin": "Repository-authored CSS geometry profile: 248-unit sphere, 16 x 32 leaves, emissive material", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene", + "presentation" + ] + }, + { + "id": "wasp-94-a-preparation-presentation", + "path": "preparation/presentation.json", + "origin": "Repository-authored presentation profile: emissive mode", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "presentation" + ] + }, + { + "id": "wasp-94-a-physical-solar-system-recipe", + "path": "presentation/solar-system.json", + "origin": "Repository-authored scene recipe: published radius, camera plan", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene" + ] + }, + { + "id": "wasp-94-a-gaia-xp", + "path": "photometry/gaia-dr3-xp-sampled.csv", + "origin": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; De Angeli et al. (2023), A&A 674, A2; Montegriffo et al. (2023), A&A 674, A3", + "license": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "licenseEvidence": [ + "https://www.cosmos.esa.int/web/gaia-users/credits" + ], + "acquisition": "Download from ESA Gaia DataLink in source/preparation/acquisition.json: the externally calibrated BP/RP sampled mean spectrum of source_id 6780546169634170496, 343 samples from 336 to 1020 nm.", + "redistribution": "One archived spectrum or catalogue file, unchanged, with citation", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-wasp-94-a-gaia-xp", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + }, + { + "id": "wasp-94-a-stellar-color", + "path": "photometry/stellar-color.json", + "origin": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; De Angeli et al. (2023), A&A 674, A2; Montegriffo et al. (2023), A&A 674, A3; CIE 1931 2° observer", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Authored method record: names the archived spectrum, how it is read and the color computation applied", + "redistribution": "Method record only", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "local", + "reason": "Project-authored color recipe naming the Gaia DR3 spectrum bound above; repinned when edited." + } + }, + { + "id": "wasp-94-a-claret-2011-limb-darkening", + "path": "photometry/claret-2011-v-quadratic.tsv", + "origin": "https://vizier.cds.unistra.fr/viz-bin/asu-tsv", + "credit": "Claret & Bloemen (2011), A&A 529, A75 (VizieR J/A+A/529/A75)", + "license": "VizieR catalogue data, public with citation of the paper and CDS", + "licenseEvidence": [ + "https://cds.unistra.fr/vizier-org/licences_vizier.html" + ], + "acquisition": "VizieR request in source/preparation/acquisition.json: the quadratic Johnson V coefficients of the grid nodes at 6000..6250 K, log g 4..4.5, solar metallicity.", + "redistribution": "Catalogue rows retained unchanged with their citation.", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-wasp-94-a-claret-2011-limb-darkening", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + }, + { + "id": "wasp-94-a-gaia-dr3-source", + "path": "photometry/gaia-dr3-source.csv", + "origin": "https://gea.esac.esa.int/tap-server/tap/sync", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26 (FLAME)", + "license": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "licenseEvidence": [ + "https://www.cosmos.esa.int/web/gaia-users/credits" + ], + "acquisition": "Gaia Archive TAP query in source/preparation/acquisition.json: the gaia_source row of source_id 6780546169634170496 (position, parallax, proper motion, radial velocity) with its FLAME mass and radius.", + "redistribution": "One catalogue row, retained unchanged with its credit.", + "consumers": [ + "placement" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-wasp-94-a-gaia-dr3-source", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + } + ], + "generatedIntermediates": [ + { + "id": "limb-darkened-disc-context-marker", + "path": "presentation/context.png", + "origin": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "credit": "The color dataset as a disc, dimmed toward the limb by its model law; rendered by packages/telescope-cli/src/source-authoring/context-markers.mts", + "license": "Project-authored display derivative.", + "consumers": [ + "navigation" + ], + "recipe": { + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "inputs": [ + "wasp-94-a-stellar-color", + "wasp-94-a-gaia-xp", + "wasp-94-a-claret-2011-limb-darkening", + "wasp-94-a-preparation-raster" + ] + }, + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "sourceBinding": { + "kind": "local", + "reason": "The color dataset rendered as a disc with its limb darkening; `context-markers.mts --check` recomputes it." + } + } + ], + "documents": [ + { + "path": "content/object.json", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored factsheet, dataset recipe and legend." + } + }, + { + "path": "preparation/acquisition.json" + }, + { + "path": "preparation/navigation.json" + }, + { + "path": "preparation/rotation.json" + }, + { + "path": "preparation/new-object.json", + "sourceBinding": { + "kind": "local", + "reason": "The spec telescope new-object generated this package from, with the order it was given; `telescope new-object --refresh` regenerates from it." + } + } + ] +} diff --git a/src/objects/wasp-94-a/source/measurements.json b/src/objects/wasp-94-a/source/measurements.json new file mode 100644 index 00000000000..e78fe0fbc54 --- /dev/null +++ b/src/objects/wasp-94-a/source/measurements.json @@ -0,0 +1,18 @@ +{ + "schema": "cssearth-uniform-disc-star@1", + "id": "wasp-94-a", + "angularDiameterMas": 0.07, + "angularDiameterSource": "Computed here from the record's radius and distance: 2 x 1,029,636 km at 210.53 pc = 0.07 mas. No interferometric diameter of this star is used.", + "distanceParsecs": 210.53462213743273, + "distanceSource": "Gaia DR3 parallax 4.7498 +/- 0.0242 mas (same row, RUWE 0.98): 210.53 pc, no zero-point correction", + "radiusKm": 1029636, + "radiusSource": "Radius 1.48 +/- 0.12 solar radii from Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract): 1,029,636 km at 695,700 km per solar radius. Mass 1.67 +/- 0.64 solar masses from Stassun et al. 2017, the stellar mass of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract); GM is that mass times the JPL solar GM. Temperature 6170 +/- 80 K from Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract). presentationUp: the display axis is a sky-plane convention; the spin axis's position angle on the sky is not measured.", + "effectiveTemperatureK": 6170, + "effectiveTemperatureSource": "Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive (https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract)", + "surfaceGravityLogg": 4.32, + "surfaceGravitySource": "log g from the mass and radius in packages/astronomy/data/bodies/wasp-94-a.json (sources in its physicalNotes), log10(GM/R^2) in cgs: 4.320, rounded to two decimals", + "shape": { + "kind": "uniform-disc-sphere", + "qualification": "A sphere at the published radius, colored from the star's Gaia DR3 BP/RP spectrum (photometry/stellar-color.json); no image of the photosphere exists." + } +} diff --git a/src/objects/wasp-94-a/source/photometry/claret-2011-v-quadratic.tsv b/src/objects/wasp-94-a/source/photometry/claret-2011-v-quadratic.tsv new file mode 100644 index 00000000000..e8528021fbe --- /dev/null +++ b/src/objects/wasp-94-a/source/photometry/claret-2011-v-quadratic.tsv @@ -0,0 +1,11 @@ +logg Teff Z xi a b Filt Met Mod +[cm/s2] K [Sun] km/s +----- ------ ---- ---- ------- ------- -- - - + 4.00 6000 0.0 2.0 0.6801 0.1523 v L A + 4.50 6000 0.0 2.0 0.6731 0.1587 v L A + 4.00 6250 0.0 2.0 0.6169 0.2000 v L A + 4.50 6250 0.0 2.0 0.6059 0.2100 v L A + 4.00 6000 0.0 2.0 0.4110 0.2882 V L A + 4.50 6000 0.0 2.0 0.4152 0.2860 V L A + 4.00 6250 0.0 2.0 0.3758 0.3047 V L A + 4.50 6250 0.0 2.0 0.3771 0.3051 V L A diff --git a/src/objects/wasp-94-a/source/photometry/stellar-color.json b/src/objects/wasp-94-a/source/photometry/stellar-color.json new file mode 100644 index 00000000000..5097d958109 --- /dev/null +++ b/src/objects/wasp-94-a/source/photometry/stellar-color.json @@ -0,0 +1,16 @@ +{ + "schema": "cssearth-stellar-photometric-color@1", + "objectId": "wasp-94-a", + "spectrum": "gaia-xp-sampled", + "sampledSpectrum": { + "source": "Gaia Collaboration (2023), Gaia Data Release 3, A&A 674, A1; BP/RP spectra: De Angeli et al. (2023), A&A 674, A2, and externally calibrated sampled spectra: Montegriffo et al. (2023), A&A 674, A3", + "path": "photometry/gaia-dr3-xp-sampled.csv", + "service": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "checked": "2026-10-05", + "sourceId": "6780546169634170496", + "sampling": "343 samples from 336 to 1020 nm in 2 nm steps, flux in W m^-2 nm^-1 (the DataLink default sampling)", + "note": "Gaia's own low-resolution spectrum of the star, calibrated to absolute flux. Its samples at the even wavelengths from 380 to 780 nm are weighted by the CIE 1931 2-degree observer. The range in the prepared report is the color of the spectrum moved one standard error down and up at every sample." + }, + "whitePoint": "sRGB D65", + "normalization": "brightest linear sRGB channel = 1" +} diff --git a/src/objects/wasp-94-a/source/preparation/acquisition.json b/src/objects/wasp-94-a/source/preparation/acquisition.json new file mode 100644 index 00000000000..cd029e6bbf9 --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/acquisition.json @@ -0,0 +1,66 @@ +{ + "schema": "cssearth-acquisition-plan@1", + "operations": [ + { + "kind": "download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/gaia-dr3-xp-sampled.csv", + "url": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3" + }, + { + "kind": "request-download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/claret-2011-v-quadratic.tsv", + "url": "https://vizier.cds.unistra.fr/viz-bin/asu-tsv", + "form": { + "-source": "J/A+A/529/A75/table-af", + "-out.max": "500", + "Teff": "6000..6250", + "logg": "4..4.5", + "Z": "=0", + "xi": "=2", + "Filt": "V", + "Met": "L", + "Mod": "A", + "-out": "logg,Teff,Z,xi,a,b,Filt,Met,Mod" + }, + "requiredText": [ + "logg\tTeff" + ], + "replacements": [ + { + "pattern": "^#.*\\n", + "flags": "gm", + "replacement": "" + }, + { + "pattern": "^\\s*\\n", + "flags": "gm", + "replacement": "" + } + ] + }, + { + "kind": "request-download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/gaia-dr3-source.csv", + "url": "https://gea.esac.esa.int/tap-server/tap/sync", + "form": { + "REQUEST": "doQuery", + "LANG": "ADQL", + "FORMAT": "csv", + "QUERY": "SELECT s.source_id, s.ref_epoch, s.ra, s.dec, s.parallax, s.parallax_error, s.pmra, s.pmdec, s.radial_velocity, s.radial_velocity_error, s.ruwe, s.phot_g_mean_mag, s.bp_rp, s.has_xp_sampled, a.mass_flame, a.mass_flame_lower, a.mass_flame_upper, a.radius_flame, a.radius_flame_lower, a.radius_flame_upper FROM gaiadr3.gaia_source AS s LEFT JOIN gaiadr3.astrophysical_parameters AS a ON a.source_id = s.source_id WHERE s.source_id = 6780546169634170496" + }, + "requiredPrefix": "source_id," + } + ] +} diff --git a/src/objects/wasp-94-a/source/preparation/geometry.json b/src/objects/wasp-94-a/source/preparation/geometry.json new file mode 100644 index 00000000000..ce819e403a9 --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/geometry.json @@ -0,0 +1,62 @@ +{ + "schema": "cssearth-css-geometry-profile@1", + "namespace": "wasp-94-a", + "surface": { + "radius": 248, + "polarRadius": 248, + "latitudeSegments": 16, + "longitudeSegments": 32, + "surface": { + "url": "/scenes/wasp-94-a/wasp-94-a-surface-color@2x.webp", + "width": 1024, + "height": 768 + }, + "surfaceLatitudeHeight": 512, + "packedBandGutter": 8, + "poles": { + "url": "/scenes/wasp-94-a/wasp-94-a-poles-color@2x.webp", + "width": 512, + "height": 256 + }, + "polarTileSize": 256, + "polarRadiusScale": 1.035, + "polarOffset": 0.1, + "uv": "cell", + "color": "#fff7fd" + }, + "projection": { + "tileSize": 50, + "layerElevation": 50, + "seamBleed": 0, + "interiorSeamBleed": 8, + "overlap": 0.015625, + "seamOutset": { + "targetPixels": 0.5, + "stepRatio": 1.4142135623730951, + "hysteresis": 0.1, + "firstDiameter": 16, + "lastDiameter": 32768 + }, + "fitToSource": false, + "rasterScale": 2, + "rasterGutter": 8, + "rasterOverscan": 0.5, + "projectivePoles": false, + "lightColor": "#ffffff", + "ambientIntensity": 1 + }, + "bodyRotationDegrees": 0, + "output": { + "schema": "csswasp-94-a-prepared-runtime-scene@1", + "materialSchema": "csswasp-94-a-prepared-emission@1", + "layout": "body-container", + "body": { + "axialTiltDegrees": 0, + "rotationDirection": "prograde", + "rotationPeriodEarthDays": 36525, + "sourceProjection": "the color dataset: a uniform photosphere color on the reference sphere", + "polarPreparation": "the same color on the polar tiles", + "axialTiltNote": "the star has no measured rotation axis; the object's rotation record places a display axis along celestial north in the plane of the sky, and this profile's tilt is not used for the frame" + } + } +} diff --git a/src/objects/wasp-94-a/source/preparation/navigation.json b/src/objects/wasp-94-a/source/preparation/navigation.json new file mode 100644 index 00000000000..7d1b627cae5 --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/navigation.json @@ -0,0 +1,41 @@ +{ + "schema": "cssearth-navigation-marker@2", + "objectId": "wasp-94-a", + "owner": "object", + "presentation": { + "size": 5 + }, + "source": { + "path": "presentation/context.png" + }, + "operations": [ + { + "type": "resize", + "width": "tile", + "height": "tile", + "fit": "cover", + "position": "centre", + "kernel": "lanczos3" + }, + { + "type": "ensure-alpha" + }, + { + "type": "ellipse-mask", + "cx": 0.5, + "cy": 0.5, + "rx": 0.45, + "ry": 0.45, + "shading": { + "ambient": 0.35, + "diffuse": 0.65 + } + }, + { + "type": "png" + } + ], + "context": { + "pixels": 512 + } +} diff --git a/src/objects/wasp-94-a/source/preparation/new-object.json b/src/objects/wasp-94-a/source/preparation/new-object.json new file mode 100644 index 00000000000..c6356f0de10 --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/new-object.json @@ -0,0 +1,40 @@ +{ + "id": "wasp-94-a", + "name": "WASP-94 A", + "system": "WASP-94 system", + "description": "Star of 6,170 K, 211 parsecs away, with 1 transiting planet.", + "parent": "milky-way", + "target": "WASP-94 A", + "gaia": "6780546169634170496", + "paper": { + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "credit": "Stassun et al. 2017" + }, + "radius": { + "value": 1.48, + "source": "Stassun et al. 2017, the stellar radius of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "uncertainty": 0.12 + }, + "mass": { + "value": 1.67, + "source": "Stassun et al. 2017, the stellar mass of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "uncertainty": 0.64 + }, + "temperature": { + "value": 6170, + "source": "Stassun et al. 2017, the stellar temperature of the default parameter set of WASP-94 A b in the NASA Exoplanet Archive", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "uncertainty": 80 + }, + "planets": [], + "companions": [], + "text": { + "card": "WASP-94 A b crosses WASP-94 A as seen from Earth, which is how it was found.", + "introduction": "Its radius and temperature follow Stassun et al. 2017.", + "locator": "NASA Exoplanet Archive ps table (st_refname STASSUN_ET_AL__2017): st_teff 6170, st_rad 1.48, st_mass 1.67" + }, + "notes": [], + "order": 5778 +} diff --git a/src/objects/wasp-94-a/source/preparation/presentation.json b/src/objects/wasp-94-a/source/preparation/presentation.json new file mode 100644 index 00000000000..63cf5c1dea3 --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/presentation.json @@ -0,0 +1,5 @@ +{ + "schema": "cssearth-css-presentation-profile@2", + "namespace": "wasp-94-a", + "mode": "emissive" +} diff --git a/src/objects/wasp-94-a/source/preparation/raster.json b/src/objects/wasp-94-a/source/preparation/raster.json new file mode 100644 index 00000000000..6a296e1f76b --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/raster.json @@ -0,0 +1,79 @@ +{ + "schema": "cssearth-raster-recipe@2", + "publicBase": "/scenes/wasp-94-a/", + "sourceWidth": 1024, + "sourceHeight": 512, + "width": 1024, + "height": 512, + "latitudeBands": 16, + "polarTile": 256, + "resample": "density-before-pack", + "polarProjection": "orthographic-bilinear", + "polesOutput": "wasp-94-a-poles-{id}{suffix}.webp", + "surfaceMetadata": { + "schema": "csswasp-94-a-prepared-assets@1" + }, + "thumbnail": { + "size": 64, + "centerLongitudeDegrees": 0 + }, + "surfaces": [ + { + "id": "color", + "output": "wasp-94-a-surface-{id}{suffix}.webp", + "thumbnail": "wasp-94-a-dataset-{id}.webp", + "source": "photometry/stellar-color.json", + "falseColor": false, + "science": { + "kind": "stellar-photometric-color", + "limbDarkening": { + "law": "quadratic", + "path": "photometry/claret-2011-v-quadratic.tsv", + "grid": { + "teffK": 6170, + "logg": 4.32, + "models": { + "Filt": "V", + "Met": "L", + "Mod": "A" + } + }, + "columns": { + "teff": "Teff", + "logg": "logg", + "u1": "a", + "u2": "b" + } + }, + "qualification": "Photosphere color from the star's Gaia DR3 BP/RP spectrum, dimmed toward the limb by the quadratic law Claret & Bloemen (2011), A&A 529, A75 compute from ATLAS model atmospheres for the Johnson V band at 6,170 K and log g 4.32 (u1 0.388, u2 0.299): a model, because no fit of this star's limb is used. The disc is unresolved: no map or absolute brightness is implied. Self-luminous, so no lighting." + } + } + ], + "emission": { + "offLimbSize": 600, + "limbSize": 512, + "bodyDiameter": 496, + "offLimbOutput": "wasp-94-a-context-{id}{suffix}.webp", + "limbOutput": "wasp-94-a-limb-{id}{suffix}.webp", + "metadata": { + "schema": "csswasp-94-a-prepared-emission@1", + "presentation": "prepared-emissive-surface-with-stationary-off-limb-context-and-limb-plate", + "offLimbContext": { + "logicalSize": 600, + "source": "none: no observation, a transparent plate", + "composition": "transparent", + "rotation": "none", + "runtimeAlphaProcessing": false + }, + "limbMaterial": { + "logicalSize": 496, + "composition": "black alpha darkens the photosphere according to the selected quadratic limb-darkening law; transparent outside the silhouette", + "surfaceReplacement": false, + "runtimeAlphaProcessing": false + }, + "lighting": false, + "shadows": false, + "runtimeLighting": false + } + } +} diff --git a/src/objects/wasp-94-a/source/preparation/rotation.json b/src/objects/wasp-94-a/source/preparation/rotation.json new file mode 100644 index 00000000000..caf10bf9ec2 --- /dev/null +++ b/src/objects/wasp-94-a/source/preparation/rotation.json @@ -0,0 +1,10 @@ +{ + "schema": "cssearth-display-orientation@1", + "rightAscensionDegrees": 313.7832437626676, + "declinationDegrees": 55.864242493789504, + "displayMeridianDegrees": 90.00000000000001, + "phase": "arbitrary-display-phase", + "source": "No measured rotation axis or period is used. The display axis is celestial north at the catalogue position, placed in the plane of the sky; computed by skyPlaneOrientation in @cssearth/astronomy.", + "coordinateSystem": "ICRF/J2000. +Z is the display axis: the sky-north direction at the star, in the plane of the sky. +X is the display meridian, set so that grid longitude 0 faces the Sun and Earth at the scene epoch; east longitude. No spin is propagated.", + "qualification": "Display convention, not a measurement. The rotation axis, spin sense, period and prime meridian of WASP-94 A are unmeasured; the axis shown is where celestial north lies on the sky." +} diff --git a/src/objects/wasp-94-a/source/presentation/context.png b/src/objects/wasp-94-a/source/presentation/context.png new file mode 100644 index 00000000000..6a2627f284e Binary files /dev/null and b/src/objects/wasp-94-a/source/presentation/context.png differ diff --git a/src/objects/wasp-94-a/source/presentation/solar-system.json b/src/objects/wasp-94-a/source/presentation/solar-system.json new file mode 100644 index 00000000000..cefe38603dc --- /dev/null +++ b/src/objects/wasp-94-a/source/presentation/solar-system.json @@ -0,0 +1,9 @@ +{ + "schema": "cssearth-solar-system-preparation@1", + "bodyId": "wasp-94-a", + "displayName": "WASP-94 A", + "bodyRadiusUnits": 248, + "bodyRadiusKilometers": 1029636, + "defaultZoom": 1.25, + "geometryScale": 1.25 +} diff --git a/src/objects/wasp-94-a/text.json b/src/objects/wasp-94-a/text.json new file mode 100644 index 00000000000..9f6afa5d7da --- /dev/null +++ b/src/objects/wasp-94-a/text.json @@ -0,0 +1,35 @@ +{ + "schema": "cssearth-object-text@1", + "objectId": "wasp-94-a", + "card": { + "text": "WASP-94 A b crosses WASP-94 A as seen from Earth, which is how it was found.", + "sources": [ + { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "label": "Stassun et al. (2017), The Astronomical Journal 153 136", + "checked": "2026-10-05", + "locator": "NASA Exoplanet Archive ps table (st_refname STASSUN_ET_AL__2017): st_teff 6170, st_rad 1.48, st_mass 1.67" + } + ] + }, + "introduction": { + "text": "Its radius and temperature follow Stassun et al. 2017.", + "sources": [ + { + "catalogueId": "doi-10-3847-1538-3881-aa5df3", + "url": "https://ui.adsabs.harvard.edu/abs/2017AJ....153..136S/abstract", + "label": "Stassun et al. (2017), The Astronomical Journal 153 136", + "checked": "2026-10-05", + "locator": "NASA Exoplanet Archive ps table (st_refname STASSUN_ET_AL__2017): st_teff 6170, st_rad 1.48, st_mass 1.67" + } + ] + }, + "datasets": { + "color": { + "title": "Photosphere color", + "detail": "From its Gaia spectrum", + "summary": "The color of the star's light, dimmed toward the edge by a model atmosphere." + } + } +} diff --git a/src/renderers/css/styles/cd-34-14724-b-surfaces.css b/src/renderers/css/styles/cd-34-14724-b-surfaces.css new file mode 100644 index 00000000000..51fe3e95af1 --- /dev/null +++ b/src/renderers/css/styles/cd-34-14724-b-surfaces.css @@ -0,0 +1,76 @@ +/* CD-34 14724B: the Sun's emissive presentation (body-surfaces.css, SUN block) scoped to this object, loaded after the shared + body-surfaces.css base rules. No spin: the rotation axis and period are unmeasured. Both plates are transparent: no observation is cast. */ +/* The shared stage rule (body-surfaces.css) sizes the camera and the Sun's plates; this object's plates need the same box. */ +.object-stage[data-object-id="cd-34-14724-b"] > :is(.cd-34-14724-b-corona-layer, .cd-34-14724-b-limb-layer) { + position: absolute; + inset: 0; + transform-origin: 50% 50%; + pointer-events: none; +} + +/* Paint order: off-limb context behind the sphere, limb plate above it. */ +.object-stage[data-object-id="cd-34-14724-b"] .cd-34-14724-b-corona-layer { + z-index: 0; +} + +.object-stage[data-object-id="cd-34-14724-b"] .polycss-camera { + z-index: 1; +} + +.object-stage[data-object-id="cd-34-14724-b"] .cd-34-14724-b-limb-layer { + z-index: 2; +} + +.object-stage[data-object-id="cd-34-14724-b"] .cd-34-14724-b-body { + transform-style: preserve-3d; +} + +.object-stage[data-object-id="cd-34-14724-b"] .polycss-scene s { + position: absolute; + display: block; + transform-origin: 0 0; + margin: 0; + padding: 0; + line-height: 0; + text-decoration: none; + background-repeat: no-repeat; + pointer-events: none; +} + +/* The silhouette-fit binding already includes physical framing. Keep the prepared plate's native dimensions. */ +.object-stage[data-object-id="cd-34-14724-b"] .cd-34-14724-b-corona-layer { + background-position: center; + background-repeat: no-repeat; + background-size: + 600px + 600px; +} + +/* Limb plate: 496 px = raster.json emission.bodyDiameter = camera.logicalBodyDiameter. */ +.object-stage[data-object-id="cd-34-14724-b"] .cd-34-14724-b-limb-layer { + background-position: center; + background-repeat: no-repeat; + background-size: + 496px + 496px; +} + +.object-stage[data-object-id="cd-34-14724-b"] .polycss-camera { + display: block; + width: 100%; + height: 100%; +} + +.object-stage[data-object-id="cd-34-14724-b"] .polycss-scene { + will-change: transform; +} + +.object-stage[data-object-id="cd-34-14724-b"] .polycss-scene s { + width: 64px; + height: 64px; + transform-style: preserve-3d; + font: inherit; + font-weight: normal; + font-style: normal; + line-height: 0; +} diff --git a/src/renderers/css/styles/wasp-94-a-surfaces.css b/src/renderers/css/styles/wasp-94-a-surfaces.css new file mode 100644 index 00000000000..00f7bfc753f --- /dev/null +++ b/src/renderers/css/styles/wasp-94-a-surfaces.css @@ -0,0 +1,76 @@ +/* WASP-94 A: the Sun's emissive presentation (body-surfaces.css, SUN block) scoped to this object, loaded after the shared + body-surfaces.css base rules. No spin: the rotation axis and period are unmeasured. Both plates are transparent: no observation is cast. */ +/* The shared stage rule (body-surfaces.css) sizes the camera and the Sun's plates; this object's plates need the same box. */ +.object-stage[data-object-id="wasp-94-a"] > :is(.wasp-94-a-corona-layer, .wasp-94-a-limb-layer) { + position: absolute; + inset: 0; + transform-origin: 50% 50%; + pointer-events: none; +} + +/* Paint order: off-limb context behind the sphere, limb plate above it. */ +.object-stage[data-object-id="wasp-94-a"] .wasp-94-a-corona-layer { + z-index: 0; +} + +.object-stage[data-object-id="wasp-94-a"] .polycss-camera { + z-index: 1; +} + +.object-stage[data-object-id="wasp-94-a"] .wasp-94-a-limb-layer { + z-index: 2; +} + +.object-stage[data-object-id="wasp-94-a"] .wasp-94-a-body { + transform-style: preserve-3d; +} + +.object-stage[data-object-id="wasp-94-a"] .polycss-scene s { + position: absolute; + display: block; + transform-origin: 0 0; + margin: 0; + padding: 0; + line-height: 0; + text-decoration: none; + background-repeat: no-repeat; + pointer-events: none; +} + +/* The silhouette-fit binding already includes physical framing. Keep the prepared plate's native dimensions. */ +.object-stage[data-object-id="wasp-94-a"] .wasp-94-a-corona-layer { + background-position: center; + background-repeat: no-repeat; + background-size: + 600px + 600px; +} + +/* Limb plate: 496 px = raster.json emission.bodyDiameter = camera.logicalBodyDiameter. */ +.object-stage[data-object-id="wasp-94-a"] .wasp-94-a-limb-layer { + background-position: center; + background-repeat: no-repeat; + background-size: + 496px + 496px; +} + +.object-stage[data-object-id="wasp-94-a"] .polycss-camera { + display: block; + width: 100%; + height: 100%; +} + +.object-stage[data-object-id="wasp-94-a"] .polycss-scene { + will-change: transform; +} + +.object-stage[data-object-id="wasp-94-a"] .polycss-scene s { + width: 64px; + height: 64px; + transform-style: preserve-3d; + font: inherit; + font-weight: normal; + font-style: normal; + line-height: 0; +} diff --git a/src/sources/gaia-dr3-cd-34-14724-b.json b/src/sources/gaia-dr3-cd-34-14724-b.json new file mode 100644 index 00000000000..03a97275244 --- /dev/null +++ b/src/sources/gaia-dr3-cd-34-14724-b.json @@ -0,0 +1,41 @@ +{ + "id": "gaia-dr3-cd-34-14724-b", + "kind": "data-product", + "identityLevel": "work", + "title": "Gaia DR3 gaia_source row for CD-34 14724B (source_id 6780546169633474944)", + "identifiers": [ + { + "type": "Gaia DR3 source_id", + "value": "6780546169633474944" + } + ], + "links": [ + { + "role": "archive", + "url": "https://gea.esac.esa.int/archive/", + "label": "Gaia Archive" + }, + { + "role": "landing", + "url": "https://doi.org/10.1051/0004-6361/202243940", + "label": "Gaia Collaboration (2023), Gaia DR3 summary" + } + ], + "evidence": [ + { + "url": "https://gea.esac.esa.int/tap-server/tap/sync?REQUEST=doQuery&LANG=ADQL&FORMAT=csv&QUERY=SELECT+s.source_id%2C+s.ref_epoch%2C+s.ra%2C+s.dec%2C+s.parallax%2C+s.parallax_error%2C+s.pmra%2C+s.pmdec%2C+s.radial_velocity%2C+s.radial_velocity_error%2C+s.ruwe%2C+s.phot_g_mean_mag%2C+s.bp_rp%2C+s.has_xp_sampled%2C+a.mass_flame%2C+a.mass_flame_lower%2C+a.mass_flame_upper%2C+a.radius_flame%2C+a.radius_flame_lower%2C+a.radius_flame_upper+FROM+gaiadr3.gaia_source+AS+s+LEFT+JOIN+gaiadr3.astrophysical_parameters+AS+a+ON+a.source_id+%3D+s.source_id+WHERE+s.source_id+%3D+6780546169633474944", + "checkedOn": "2026-10-05", + "locator": "gaiadr3.gaia_source at epoch 2016.0: parallax 4.72077 +/- 0.01647 mas, pmra 26.191, pmdec -44.702 mas/yr, radial_velocity -8.30 km/s, RUWE 0.81, has_xp_sampled true; mass_flame 1.0658, radius_flame 1.3995" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26", + "scope": "citation", + "evidence": "https://www.cosmos.esa.int/web/gaia-users/credits" + } + ], + "publisher": "European Space Agency, Gaia Data Processing and Analysis Consortium" +} diff --git a/src/sources/gaia-dr3-wasp-94-a.json b/src/sources/gaia-dr3-wasp-94-a.json new file mode 100644 index 00000000000..db1eeaa2f2d --- /dev/null +++ b/src/sources/gaia-dr3-wasp-94-a.json @@ -0,0 +1,41 @@ +{ + "id": "gaia-dr3-wasp-94-a", + "kind": "data-product", + "identityLevel": "work", + "title": "Gaia DR3 gaia_source row for WASP-94 A (source_id 6780546169634170496)", + "identifiers": [ + { + "type": "Gaia DR3 source_id", + "value": "6780546169634170496" + } + ], + "links": [ + { + "role": "archive", + "url": "https://gea.esac.esa.int/archive/", + "label": "Gaia Archive" + }, + { + "role": "landing", + "url": "https://doi.org/10.1051/0004-6361/202243940", + "label": "Gaia Collaboration (2023), Gaia DR3 summary" + } + ], + "evidence": [ + { + "url": "https://gea.esac.esa.int/tap-server/tap/sync?REQUEST=doQuery&LANG=ADQL&FORMAT=csv&QUERY=SELECT+s.source_id%2C+s.ref_epoch%2C+s.ra%2C+s.dec%2C+s.parallax%2C+s.parallax_error%2C+s.pmra%2C+s.pmdec%2C+s.radial_velocity%2C+s.radial_velocity_error%2C+s.ruwe%2C+s.phot_g_mean_mag%2C+s.bp_rp%2C+s.has_xp_sampled%2C+a.mass_flame%2C+a.mass_flame_lower%2C+a.mass_flame_upper%2C+a.radius_flame%2C+a.radius_flame_lower%2C+a.radius_flame_upper+FROM+gaiadr3.gaia_source+AS+s+LEFT+JOIN+gaiadr3.astrophysical_parameters+AS+a+ON+a.source_id+%3D+s.source_id+WHERE+s.source_id+%3D+6780546169634170496", + "checkedOn": "2026-10-05", + "locator": "gaiadr3.gaia_source at epoch 2016.0: parallax 4.74981 +/- 0.02421 mas, pmra 26.500, pmdec -44.971 mas/yr, radial_velocity -8.36 km/s, RUWE 0.98, has_xp_sampled true; mass_flame 1.2318, radius_flame 1.5988" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26", + "scope": "citation", + "evidence": "https://www.cosmos.esa.int/web/gaia-users/credits" + } + ], + "publisher": "European Space Agency, Gaia Data Processing and Analysis Consortium" +} diff --git a/src/sources/mast-tess-spoc-tic-92352620.json b/src/sources/mast-tess-spoc-tic-92352620.json new file mode 100644 index 00000000000..6c29371ba88 --- /dev/null +++ b/src/sources/mast-tess-spoc-tic-92352620.json @@ -0,0 +1,41 @@ +{ + "id": "mast-tess-spoc-tic-92352620", + "kind": "data-product", + "identityLevel": "work", + "title": "TESS SPOC 2-minute light curves of TIC 92352620, sectors 68, 95, 105", + "identifiers": [ + { + "type": "TIC", + "value": "92352620" + } + ], + "links": [ + { + "role": "archive", + "url": "https://mast.stsci.edu/portal/Mashup/Clients/Mast/Portal.html", + "label": "MAST" + }, + { + "role": "landing", + "url": "https://doi.org/10.1117/12.2233418", + "label": "Jenkins et al. (2016), the TESS Science Processing Operations Center" + } + ], + "evidence": [ + { + "url": "https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:TESS/product/tess2023209231226-s0068-0000000092352620-0262-s_lc.fits", + "checkedOn": "2026-10-05", + "locator": "SPOC light-curve files for sectors 68, 95, 105: LIGHTCURVE table, PDCSAP_FLUX, QUALITY, TIME in BJD_TDB - 2457000" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "This includes data collected by the TESS mission, funded by the NASA Explorer Program, obtained from MAST; SPOC pipeline, Jenkins et al. (2016)", + "scope": "citation", + "evidence": "https://archive.stsci.edu/missions-and-data/tess" + } + ], + "publisher": "Mikulski Archive for Space Telescopes (STScI)" +} diff --git a/src/sources/source-cd-34-14724-b-claret-2011-limb-darkening.json b/src/sources/source-cd-34-14724-b-claret-2011-limb-darkening.json new file mode 100644 index 00000000000..bb181c5b784 --- /dev/null +++ b/src/sources/source-cd-34-14724-b-claret-2011-limb-darkening.json @@ -0,0 +1,35 @@ +{ + "id": "source-cd-34-14724-b-claret-2011-limb-darkening", + "kind": "data-product", + "identityLevel": "work", + "title": "CD-34 14724B · cd 34 14724 b claret 2011 limb darkening", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://vizier.cds.unistra.fr/viz-bin/asu-tsv", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/cd-34-14724-b/source/manifest.json", + "locator": "/inputs/7" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "Claret & Bloemen (2011), A&A 529, A75 (VizieR J/A+A/529/A75)", + "scope": "The cd 34 14724 b claret 2011 limb darkening input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "VizieR catalogue data, public with citation of the paper and CDS", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +} diff --git a/src/sources/source-cd-34-14724-b-gaia-dr3-source.json b/src/sources/source-cd-34-14724-b-gaia-dr3-source.json new file mode 100644 index 00000000000..16f2c14cdf9 --- /dev/null +++ b/src/sources/source-cd-34-14724-b-gaia-dr3-source.json @@ -0,0 +1,35 @@ +{ + "id": "source-cd-34-14724-b-gaia-dr3-source", + "kind": "data-product", + "identityLevel": "work", + "title": "CD-34 14724B · cd 34 14724 b gaia dr3 source", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://gea.esac.esa.int/tap-server/tap/sync", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/cd-34-14724-b/source/manifest.json", + "locator": "/inputs/8" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26 (FLAME)", + "scope": "The cd 34 14724 b gaia dr3 source input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +} diff --git a/src/sources/source-cd-34-14724-b-gaia-xp.json b/src/sources/source-cd-34-14724-b-gaia-xp.json new file mode 100644 index 00000000000..5cf8fa4f5d7 --- /dev/null +++ b/src/sources/source-cd-34-14724-b-gaia-xp.json @@ -0,0 +1,35 @@ +{ + "id": "source-cd-34-14724-b-gaia-xp", + "kind": "data-product", + "identityLevel": "work", + "title": "CD-34 14724B · cd 34 14724 b gaia xp", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169633474944&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/cd-34-14724-b/source/manifest.json", + "locator": "/inputs/5" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; De Angeli et al. (2023), A&A 674, A2; Montegriffo et al. (2023), A&A 674, A3", + "scope": "The cd 34 14724 b gaia xp input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +} diff --git a/src/sources/source-wasp-94-a-b-climate-model-simulation.json b/src/sources/source-wasp-94-a-b-climate-model-simulation.json new file mode 100644 index 00000000000..671f8dcb5ac --- /dev/null +++ b/src/sources/source-wasp-94-a-b-climate-model-simulation.json @@ -0,0 +1,40 @@ +{ + "id": "source-wasp-94-a-b-climate-model-simulation", + "kind": "data-product", + "identityLevel": "work", + "title": "WASP-94 A b · wasp 94 a b climate model simulation", + "identifiers": [ + { + "type": "pds4-lidvid", + "value": "GCM_products/WASP-94Ab_GCM.nc" + } + ], + "links": [ + { + "role": "landing", + "url": "https://zenodo.org/records/15085825/files/GCM_products.zip?download=1", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/wasp-94-a-b/source/manifest.json", + "locator": "/inputs/10" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "Mukherjee et al. (2025); Met Office UM simulation", + "scope": "The wasp 94 a b climate model simulation input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "CC BY 4.0", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +} diff --git a/src/sources/source-wasp-94-a-claret-2011-limb-darkening.json b/src/sources/source-wasp-94-a-claret-2011-limb-darkening.json new file mode 100644 index 00000000000..7ecbbaeff62 --- /dev/null +++ b/src/sources/source-wasp-94-a-claret-2011-limb-darkening.json @@ -0,0 +1,35 @@ +{ + "id": "source-wasp-94-a-claret-2011-limb-darkening", + "kind": "data-product", + "identityLevel": "work", + "title": "WASP-94 A · wasp 94 a claret 2011 limb darkening", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://vizier.cds.unistra.fr/viz-bin/asu-tsv", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/wasp-94-a/source/manifest.json", + "locator": "/inputs/7" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "Claret & Bloemen (2011), A&A 529, A75 (VizieR J/A+A/529/A75)", + "scope": "The wasp 94 a claret 2011 limb darkening input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "VizieR catalogue data, public with citation of the paper and CDS", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +} diff --git a/src/sources/source-wasp-94-a-gaia-dr3-source.json b/src/sources/source-wasp-94-a-gaia-dr3-source.json new file mode 100644 index 00000000000..b0cca3e3165 --- /dev/null +++ b/src/sources/source-wasp-94-a-gaia-dr3-source.json @@ -0,0 +1,35 @@ +{ + "id": "source-wasp-94-a-gaia-dr3-source", + "kind": "data-product", + "identityLevel": "work", + "title": "WASP-94 A · wasp 94 a gaia dr3 source", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://gea.esac.esa.int/tap-server/tap/sync", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/wasp-94-a/source/manifest.json", + "locator": "/inputs/8" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26 (FLAME)", + "scope": "The wasp 94 a gaia dr3 source input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +} diff --git a/src/sources/source-wasp-94-a-gaia-xp.json b/src/sources/source-wasp-94-a-gaia-xp.json new file mode 100644 index 00000000000..3d21bd301ee --- /dev/null +++ b/src/sources/source-wasp-94-a-gaia-xp.json @@ -0,0 +1,35 @@ +{ + "id": "source-wasp-94-a-gaia-xp", + "kind": "data-product", + "identityLevel": "work", + "title": "WASP-94 A · wasp 94 a gaia xp", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://gea.esac.esa.int/data-server/data?RETRIEVAL_TYPE=XP_SAMPLED&ID=6780546169634170496&FORMAT=csv&DATA_STRUCTURE=RAW&RELEASE=Gaia+DR3", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/wasp-94-a/source/manifest.json", + "locator": "/inputs/5" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; De Angeli et al. (2023), A&A 674, A2; Montegriffo et al. (2023), A&A 674, A3", + "scope": "The wasp 94 a gaia xp input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +}