diff --git a/docs/surface-preparation.md b/docs/surface-preparation.md index d6dbf0fd354..6e499b17118 100644 --- a/docs/surface-preparation.md +++ b/docs/surface-preparation.md @@ -785,7 +785,8 @@ same published law, so the limb in the app is the limb the instrument saw. | Venus | Minnaert, k 1.32 to 1.36 | [Pérez-Hoyos et al. 2018](https://doi.org/10.1002/2017JE005406), MESSENGER MASCS | | Mars | Hapke, surface only | [Vincendon 2013](https://doi.org/10.1016/j.pss.2012.12.005), OMEGA and CRISM | | Jupiter | Minnaert per channel | [Simon et al. 2015](https://doi.org/10.1088/0004-637X/812/1/55), OPAL | - | Saturn, Uranus, Neptune | Minnaert per channel | the OPAL README of each map | + | Saturn | Minnaert per display channel: the README's exponents weighted by the light each channel shows | the [OPAL README](https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt) of its map and the spectrum of [Karkoschka 1998](https://doi.org/10.1006/icar.1998.5913), weighted by [display-limb.mts](../packages/bake/authoring/saturn/display-limb.mts) | + | Uranus, Neptune | Minnaert per channel | the OPAL README of each map | | Earth | Minnaert per channel | fitted here to six [DSCOVR EPIC](https://epic.gsfc.nasa.gov/about) Level 1B frames ([fit-epic-limb.mts](../packages/bake/cli/fit-epic-limb.mts)) | | Moon | Hapke at 643 nm | [Sato et al. 2014](https://doi.org/10.1002/2013JE004580), the correction of the LROC WAC mosaic; w, b and h_S are medians of its PDS parameter map | | Ceres (dwarf planet) | Hapke at 749 nm | [Li et al. 2019](https://doi.org/10.1016/j.icarus.2018.12.038), Dawn Framing Camera | diff --git a/packages/bake/authoring/saturn/display-limb.mts b/packages/bake/authoring/saturn/display-limb.mts new file mode 100644 index 00000000000..b2279e8f874 --- /dev/null +++ b/packages/bake/authoring/saturn/display-limb.mts @@ -0,0 +1,122 @@ +/** + * Saturn's limb law for each display channel. + * + * Hubble OPAL publishes one Minnaert exponent per filter, in the README of its Saturn maps. A display channel shows a + * range of wavelengths, so no single filter is its law. Each channel takes the README's exponents weighted by the light + * that channel shows: Saturn's full-disc albedo (Karkoschka 1998, PDS 1995LOW.TAB) times a 5,772 K Planck spectrum, + * through the CIE 1931 observer into linear sRGB, the computation behind the body's whole-disc color. Between two + * filters the exponent is interpolated in wavelength, and outside them the nearest one holds; that join is ours. The + * methane-band filters are left out: a narrow absorption band says nothing of the continuum beside it. + * + * A weighted sum of Minnaert laws is not a Minnaert law. Each channel records the exponent that fits its weighted + * profile best over the flood-lit disc, weighted by projected area, out to the emission angle the map's data reached, + * and the command prints how far that power law departs from the profile. + * + * node packages/bake/authoring/saturn/display-limb.mts --albedo=<1995low.tab> [--write] + * + * Reads the per-filter records in src/objects/saturn/source/photometry/ and checks the spectrum against the recorded + * whole-disc color; --write stores the three display records beside them. Preparation only. + */ +import { projectRoot as checkoutProjectRoot } from '@cssearth/core/node'; +import { readFile, readdir, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { parseCieTable } from '@cssearth/bake/objects/color'; +import { readCie1931ColorMatching } from '@cssearth/bake/objects/sources'; +import { spectrumLinearSrgb } from '@cssearth/bake/objects/stellar'; +import { PHOTOMETRIC_MODEL_SCHEMA, loadPhotometricModelRecord } from '@cssearth/bake/photometry'; + +/** The Sun's effective temperature in NASA's Sun fact sheet: the illuminant of the whole-disc color record. */ +const SOLAR_KELVIN = 5772; +const PLANCK_H = 6.62607015e-34, LIGHT_C = 299792458, BOLTZMANN_K = 1.380649e-23; +const VISIBLE_NM = Array.from({ length: 401 }, (_, i) => 380 + i); +const CHANNELS = ['red', 'green', 'blue'] as const; + +export interface FilterExponent { readonly wavelengthNm: number; readonly coefficient: number } + +/** The exponent at a wavelength: linear between two filters, the nearest filter's outside them. */ +export function exponentAt(filters: readonly FilterExponent[], wavelengthNm: number): number { + const sorted = [...filters].sort((a, b) => a.wavelengthNm - b.wavelengthNm); + if (!sorted.length) throw new TypeError('A display limb law needs at least one filter.'); + if (wavelengthNm <= sorted[0]!.wavelengthNm) return sorted[0]!.coefficient; + for (let i = 1; i < sorted.length; i++) { + const low = sorted[i - 1]!, high = sorted[i]!; + if (wavelengthNm <= high.wavelengthNm) return low.coefficient + (high.coefficient - low.coefficient) * (wavelengthNm - low.wavelengthNm) / (high.wavelengthNm - low.wavelengthNm); + } + return sorted.at(-1)!.coefficient; +} + +/** + * Each display channel's flood-lit limb profile, mu^(2k - 1) weighted by the light the channel shows, and the Minnaert + * exponent that fits it best over the disc out to `maximumEmissionDegrees`. + */ +export function displayLimbExponents({ filters, light, colorMatching, maximumEmissionDegrees, steps = 1024 }: { + filters: readonly FilterExponent[]; light: (wavelengthNm: number) => number; colorMatching: Map; maximumEmissionDegrees: number; steps?: number; +}) { + const whole = spectrumLinearSrgb(VISIBLE_NM, light, colorMatching), limit = Math.sin(maximumEmissionDegrees * Math.PI / 180); + const radii = Array.from({ length: steps }, (_, step) => (step + 0.5) / steps * limit); + const profiles = radii.map(radius => { + const mu = Math.sqrt(1 - radius * radius), lit = spectrumLinearSrgb(VISIBLE_NM, wavelength => light(wavelength) * mu ** (2 * exponentAt(filters, wavelength) - 1), colorMatching); + return { radius, mu, factors: [lit[0] / whole[0], lit[1] / whole[1], lit[2] / whole[2]] as const }; + }); + return CHANNELS.map((channel, index) => { + let best = { coefficient: Number.NaN, error: Infinity }; + for (let thousandth = 300; thousandth <= 1300; thousandth++) { + const coefficient = thousandth / 1000; + let error = 0; + for (const { radius, mu, factors } of profiles) error += (mu ** (2 * coefficient - 1) - factors[index]!) ** 2 * radius; + if (error < best.error) best = { coefficient, error }; + } + const departure = Math.max(...profiles.map(({ mu, factors }) => Math.abs(mu ** (2 * best.coefficient - 1) - factors[index]!))); + return { channel, coefficient: best.coefficient, departure, discMean: 2 / (2 * best.coefficient + 1) }; + }); +} + +/** Saturn's albedo from the PDS table's eight columns: air wavelength is the second, Saturn's albedo the fifth. */ +export function readSaturnAlbedo(table: string): (wavelengthNm: number) => number { + const rows = table.split(/\r?\n/u).filter(line => line.trim()).map(line => { const cells = line.trim().split(/\s+/u).map(Number); return { cells: cells.length, wavelength: cells[1]!, albedo: cells[4]! }; }); + if (rows.length < 2 || rows.some(row => row.cells !== 8 || !Number.isFinite(row.wavelength) || !Number.isFinite(row.albedo))) throw new TypeError('The albedo table is not the 1995LOW.TAB layout.'); + if (rows[0]!.wavelength > VISIBLE_NM[0]! || rows.at(-1)!.wavelength < VISIBLE_NM.at(-1)!) throw new RangeError('The albedo table does not cover 380-780 nm.'); + return wavelength => { + const next = Math.max(1, rows.findIndex(row => row.wavelength >= wavelength)), low = rows[next - 1]!, high = rows[next]!; + return low.albedo + (high.albedo - low.albedo) * (wavelength - low.wavelength) / (high.wavelength - low.wavelength); + }; +} + +const planck = (wavelengthNm: number) => { const metres = wavelengthNm * 1e-9; return 1 / (metres ** 5 * Math.expm1(PLANCK_H * LIGHT_C / (metres * BOLTZMANN_K * SOLAR_KELVIN))); }; + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + const albedoPath = process.argv.find(argument => argument.startsWith('--albedo='))?.slice('--albedo='.length); + if (!albedoPath) throw new TypeError('Usage: display-limb.mts --albedo=<1995low.tab> [--write]'); + const source = resolve(checkoutProjectRoot(import.meta.url), 'src/objects/saturn/source'), photometry = resolve(source, 'photometry'); + // The README's continuum filters: every per-filter record beside the display records, by the wavelength its filter states. + const paths = (await readdir(photometry)).filter(name => /^opal-2025-minnaert-f\d+[nmw]\.json$/u.test(name)).sort(); + const records = await Promise.all(paths.map(name => loadPhotometricModelRecord(source, `photometry/${name}`))); + const filters = records.map(record => { + const wavelengthNm = Number(/(\d+) nm/u.exec(record.filter)?.[1]), model = record.model; + if (!Number.isFinite(wavelengthNm) || model.family !== 'separable' || model.disk.family !== 'minnaert') throw new TypeError(`${record.id} is not a Minnaert record with a stated wavelength.`); + return { id: record.id, wavelengthNm, coefficient: model.disk.coefficient }; + }); + const ranges = records.map(record => JSON.stringify(record.fit)); + if (new Set(ranges).size !== 1) throw new Error('The per-filter records state different fitted ranges.'); + const fit = records[0]!.fit; + if (!fit.emissionDegrees) throw new TypeError('The per-filter records state no emission range.'); + const albedo = readSaturnAlbedo(await readFile(resolve(albedoPath), 'latin1')), light = (wavelength: number) => planck(wavelength) * albedo(wavelength); + const colorMatching = parseCieTable((await readCie1931ColorMatching()).toString('utf8'), 3); + // The same spectrum gives the recorded whole-disc color; a different table or observer would not. + const recorded = JSON.parse(await readFile(resolve(photometry, 'karkoschka-1998-whole-disc-color.json'), 'utf8')).linearSrgb as number[]; + const whole = spectrumLinearSrgb(VISIBLE_NM, light, colorMatching); + for (const channel of [1, 2]) if (Math.abs(whole[channel]! / whole[0] - recorded[channel]! / recorded[0]!) > 5e-4) + throw new Error(`The spectrum's ${CHANNELS[channel]}/red ratio ${(whole[channel]! / whole[0]).toFixed(4)} is not the recorded whole-disc color's ${(recorded[channel]! / recorded[0]!).toFixed(4)}.`); + const result = displayLimbExponents({ filters, light, colorMatching, maximumEmissionDegrees: fit.emissionDegrees[1] }); + console.log('filters', filters.map(filter => `${filter.wavelengthNm} nm ${filter.coefficient}`).join(', ')); + for (const { channel, coefficient, departure, discMean } of result) console.log(`${channel}: k ${coefficient.toFixed(3)}, flood-disc mean ${discMean.toFixed(4)}, largest departure from the weighted profile ${departure.toFixed(4)}`); + if (process.argv.includes('--write')) { + for (const { channel, coefficient } of result) await writeFile(resolve(photometry, `opal-2025-minnaert-display-${channel}.json`), JSON.stringify({ + schema: PHOTOMETRIC_MODEL_SCHEMA, id: `opal-2025-minnaert-display-${channel}`, instrument: 'Hubble WFC3/UVIS (OPAL)', + filter: `sRGB ${channel}: the README's continuum filters weighted by Saturn's sunlit spectrum through the CIE 1931 observer`, quantity: 'radiance-factor', + model: { family: 'separable', disk: { family: 'minnaert', coefficient, coefficientPerDegree: 0 } }, fit, + }, null, 2) + '\n'); + console.log('wrote the three display records'); + } +} diff --git a/packages/bake/authoring/saturn/display-limb.test.mts b/packages/bake/authoring/saturn/display-limb.test.mts new file mode 100644 index 00000000000..c4c311cb891 --- /dev/null +++ b/packages/bake/authoring/saturn/display-limb.test.mts @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { parseCieTable } from '@cssearth/bake/objects/color'; +import { readCie1931ColorMatching } from '@cssearth/bake/objects/sources'; +import { displayLimbExponents, exponentAt, readSaturnAlbedo } from './display-limb.mts'; + +const colorMatching = parseCieTable((await readCie1931ColorMatching()).toString('utf8'), 3); + +test('an exponent is interpolated between two filters and held outside them', () => { + const filters = [{ wavelengthNm: 600, coefficient: 0.8 }, { wavelengthNm: 400, coefficient: 0.4 }]; + assert.equal(exponentAt(filters, 380), 0.4); + assert.ok(Math.abs(exponentAt(filters, 450) - 0.5) < 1e-12); + assert.equal(exponentAt(filters, 700), 0.8); + assert.throws(() => exponentAt([], 500), /at least one filter/); +}); + +test('one filter gives every display channel its exponent', () => { + const result = displayLimbExponents({ filters: [{ wavelengthNm: 550, coefficient: 0.7 }], light: () => 1, colorMatching, maximumEmissionDegrees: 86.3 }); + assert.deepEqual(result.map(channel => channel.coefficient), [0.7, 0.7, 0.7]); + for (const channel of result) { assert.ok(channel.departure < 1e-9); assert.ok(Math.abs(channel.discMean - 2 / 2.4) < 1e-12); } +}); + +test('a channel takes the exponents of the wavelengths it shows', () => { + const filters = [{ wavelengthNm: 450, coefficient: 0.6 }, { wavelengthNm: 650, coefficient: 0.9 }]; + const [red, green, blue] = displayLimbExponents({ filters, light: () => 1, colorMatching, maximumEmissionDegrees: 86.3 }); + // An sRGB primary weighs some wavelengths negatively, so a channel's exponent may lie outside the filters' range. + assert.ok(blue!.coefficient < green!.coefficient && green!.coefficient < red!.coefficient); + // The light's spectrum is part of the weight: another spectrum gives other exponents. + const reddened = displayLimbExponents({ filters, light: wavelength => wavelength ** 4, colorMatching, maximumEmissionDegrees: 86.3 }); + assert.notDeepEqual(reddened.map(channel => channel.coefficient), [red!.coefficient, green!.coefficient, blue!.coefficient]); +}); + +test('the albedo table is read by column, past 1,000 nm too', () => { + const rows = [' 300.4 300.31 .0000 .2128 .2532 .5306 .6933 .0431', ' 600.0 599.83 .0010 .5000 .5000 .5000 .5000 .5000', '1049.2 1048.89 .2688 .3974 .5348 .0704 .0444 .1821']; + const albedo = readSaturnAlbedo(rows.join('\r\n') + '\r\n'); + assert.ok(Math.abs(albedo(599.83) - 0.5) < 1e-12); + assert.ok(albedo(450) > 0.2532 && albedo(450) < 0.5); + assert.throws(() => readSaturnAlbedo(rows.slice(0, 2).join('\n')), /380-780 nm/); + assert.throws(() => readSaturnAlbedo('1 2 3\n4 5 6'), /1995LOW/); +}); diff --git a/packages/bake/src/objects/geometry/ellipsoid.ts b/packages/bake/src/objects/geometry/ellipsoid.ts index 9ea76ac693a..6a5b2d0cca4 100644 --- a/packages/bake/src/objects/geometry/ellipsoid.ts +++ b/packages/bake/src/objects/geometry/ellipsoid.ts @@ -235,3 +235,21 @@ export function planetographicRowsToMeshLatitude(data: Uint8Array, width: number } return output; } + +/** Resample an equirectangular map whose rows are samples of planetocentric latitude from +90 to -90 degrees inclusive (the first + * and last rows are the poles, as the Cassini ISS global maps of the PDS Atmospheres Node are gridded) onto rows evenly spaced in + * the mesh's own latitude: tan(planetocentric) = (b / a) tan(beta). The result has one row fewer than the samples. Rows are + * linearly interpolated; columns are unchanged. */ +export function planetocentricSampleRowsToMeshLatitude(data: Uint8Array, width: number, height: number, channels: number, axisRatio: number): Buffer { + if (!(axisRatio >= 1) || !Number.isFinite(axisRatio)) throw new RangeError('An axis ratio a / b must be finite and at least 1.'); + if (!(height >= 2) || data.length !== width * height * channels) throw new RangeError('Map bytes do not match its dimensions.'); + const rows = height - 1, rowBytes = width * channels, output = Buffer.alloc(rows * rowBytes); + for (let y = 0; y < rows; y += 1) { + const beta = Math.PI / 2 - (y + 0.5) / rows * Math.PI; + const planetocentric = Math.atan2(Math.sin(beta), axisRatio * Math.cos(beta)); + const source = Math.max(0, Math.min(rows, (Math.PI / 2 - planetocentric) / Math.PI * rows)); + const y0 = Math.min(rows - 1, Math.floor(source)), y1 = y0 + 1, fraction = source - y0; + for (let i = 0; i < rowBytes; i += 1) output[y * rowBytes + i] = Math.round(data[y0 * rowBytes + i]! * (1 - fraction) + data[y1 * rowBytes + i]! * fraction); + } + return output; +} diff --git a/packages/bake/src/objects/layers/material-composition/layered-oblate.test.mts b/packages/bake/src/objects/layers/material-composition/layered-oblate.test.mts index 8476e2e4448..72ff172e282 100644 --- a/packages/bake/src/objects/layers/material-composition/layered-oblate.test.mts +++ b/packages/bake/src/objects/layers/material-composition/layered-oblate.test.mts @@ -50,8 +50,8 @@ test("Saturn's tinted OPAL map is tied to Karkoschka's whole-disc color and keep const { tie: report } = await prepareSurfaceColor({ sourcePath: join(source, recipe.sources.surface), unobservedRows: recipe.surfaceUnobservedRows, width: parameters.planetSourceTextureWidth, height: parameters.planetSourceTextureHeight, equatorialToPolar: parameters.objectEquatorialRadiusKm / parameters.objectPolarRadiusKm, channelFactors: [tint.r / peak, tint.g / peak, tint.b / peak], tie }); - assert.deepEqual(report, { reference: 'red', source: 'karkoschka-1998-whole-disc-color', measured: { green: 0.9317, blue: 0.6975 }, published: { green: 0.6759, blue: 0.5057 }, gains: [1, 0.7254, 0.725], - luminance: { factor: 1.2687, knee: 0.8, shoulderedTexels: 283798, shoulderedShare: 0.0684 } }); + assert.deepEqual(report, { reference: 'red', source: 'karkoschka-1998-whole-disc-color', measured: { green: 0.9317, blue: 0.6975 }, published: { green: 0.7052, blue: 0.4573 }, gains: [1, 0.7569, 0.6556], + luminance: { factor: 1.2391, knee: 0.8, shoulderedTexels: 216448, shoulderedShare: 0.0522 } }); }); test("Saturn's caps sit on the oblate polar carrier, which faces out of the body at the lane's own tile size", async () => { diff --git a/packages/bake/src/objects/layers/material-composition/layered-oblate.ts b/packages/bake/src/objects/layers/material-composition/layered-oblate.ts index 655bdad45d5..47a8bdaa3ab 100644 --- a/packages/bake/src/objects/layers/material-composition/layered-oblate.ts +++ b/packages/bake/src/objects/layers/material-composition/layered-oblate.ts @@ -43,7 +43,7 @@ import { createProjectiveSurfaceRasterPresentation, fitTextureGeometry, fitProje import { optimizePreparedQ75Webp, PREPARED_Q75_WEBP_ENCODING } from '../../../delivery/index.ts'; import { verifyObservationSources } from '../observed-surfaces/index.ts'; import { ellipsoidPoint, planetographicRowsToMeshLatitude, intersectViewRayWithEllipsoid, prepareProjectedEllipsoidSilhouetteCoverage, prepareObjectViewDirection as prepareViewDirection, prepareObjectSpaceDirection, dotVector, subtractVector, rotateX, rotateY, rotateZ } from '../../geometry/index.ts'; -import { CHANNEL_NAMES, floodDiscMean, loadLimbLaw, limbFactors, limbOverlay, meanObservedColor, outsideSilhouette, scatteringAngles, type Channels } from '../../../photometry/index.ts'; +import { CHANNEL_NAMES, floodDiscMean, loadLimbLaw, limbFactors, limbOverlay, meanObservedColor, scatteringAngles, type Channels } from '../../../photometry/index.ts'; import { displayBandRatios, keepLuminance, latitudeWeightedLuminance, loadWholeDiscColor, tieBandRatios } from '../../raster/index.ts'; import type { BandRatioPolicy } from '../../raster/index.ts'; @@ -1073,7 +1073,22 @@ function prepareFixedMaterialPlane({ down[axis] * sampledScreenY * materialSampleScale); let hit = intersectViewRayWithEllipsoidSurface(materialOrigin, view); // A sample that misses the ellipsoid at content scale lies past the silhouette, where the overlay may only darken. - const outsideGlobe = !hit; + // A texel the silhouette crosses keeps its color by the share of it that lies inside, so the dark outline stays one + // unbroken line when the frame is drawn larger than it was prepared. + const silhouetteRadius = Math.hypot(sampledScreenX / radiusX, sampledScreenY / radiusY) * materialSampleScale; + let colorKept = hit ? 1 : 0; + if (Math.abs(silhouetteRadius - 1) < 6 * materialSampleScale / outputSize) { + const samples = PLANET_ORBIT_MATERIAL_SILHOUETTE_SUPERSAMPLING; + let inside = 0; + for (let sampleRow = 0; sampleRow < samples; sampleRow += 1) { + for (let sampleColumn = 0; sampleColumn < samples; sampleColumn += 1) { + const x = ((column + (sampleColumn + 0.5) / samples) / outputSize * 2 - 1) * radiusX * materialSampleScale; + const y = ((row + (sampleRow + 0.5) / samples) / outputSize * 2 - 1) * radiusY * materialSampleScale; + if (intersectViewRayWithEllipsoidSurface(mapVector3((axis) => right[axis] * x + down[axis] * y), view)) inside += 1; + } + } + colorKept = inside / (samples * samples); + } if (!hit) { let lowerScale = 1; let upperScale = materialSampleScale; @@ -1143,7 +1158,7 @@ function prepareFixedMaterialPlane({ } if (!cutawayMaterialRemoved) { const overlay = globeLimbOverlay(hit.normal, objectLight, view, 1 / outputSize, directTransmission); - writeLimbOverlay(output, outputOffset, outsideGlobe ? outsideSilhouette(overlay) : overlay); + writeLimbOverlay(output, outputOffset, [overlay[0] * colorKept, overlay[1] * colorKept, overlay[2] * colorKept, overlay[3]]); } const surfaceDistance = dotVector( subtractVector(coverageHit.position, coverageOrigin), @@ -2866,7 +2881,10 @@ function preparePolarMaterialSample({ function globeLimbOverlay(normal:ReadonlyVector3, objectLight:ReadonlyVector3, objectView:ReadonlyVector3, emissionFloor:number, directTransmission:number) { if (!limbReference) throw new Error("Saturn's limb overlay needs the prepared surface's reference color first."); const { incidence, emission, phase } = scatteringAngles(normal, objectLight, objectView, emissionFloor); - return limbOverlay(limbFactors(LIMB_LAW, incidence, emission, phase).map((factor) => factor * directTransmission), limbReference); + // Flood light is the view, so the incidence is the emission. The emission floor leaves a grazing texel's incidence past + // its emission; the law, held at the emission its data reached, would then fall to black on that texel alone. + const lit = phase < 1e-6 ? emission : incidence; + return limbOverlay(limbFactors(LIMB_LAW, lit, emission, phase).map((factor) => factor * directTransmission), limbReference); } function writeLimbOverlay(output:Uint8Array, offset:number, [r, g, b, alpha]:readonly number[]) { diff --git a/packages/bake/src/objects/layers/material-composition/leaf-layouts.ts b/packages/bake/src/objects/layers/material-composition/leaf-layouts.ts index 7070b4b1512..11a7df7fbb3 100644 --- a/packages/bake/src/objects/layers/material-composition/leaf-layouts.ts +++ b/packages/bake/src/objects/layers/material-composition/leaf-layouts.ts @@ -12,8 +12,8 @@ function declarations(stylesheet: string, selector: string): Record}) { const pixelLength=/^\d+(?:\.\d+)?px$/; const common=declarations(stylesheet,config.stylesheet.scope+'.polycss-scene s'); - const width=common.width?.match(/^var\(--polycss-atlas-width,\s*([^)]*)\)$/)?.[1]; - const height=common.height?.match(/^var\(--polycss-atlas-height,\s*([^)]*)\)$/)?.[1]; + // The stylesheet states the leaf box as plain lengths: no custom property is set where a body is drawn. + const {width,height}=common; if(!width||!height||!pixelLength.test(width)||!pixelLength.test(height))throw new Error('Prepared leaf defaults must be explicit positive pixel lengths.'); const classes: Record={}; for(const shell of scene.interior.shells) { diff --git a/packages/bake/src/objects/layers/material-composition/material-composition.test.mts b/packages/bake/src/objects/layers/material-composition/material-composition.test.mts index 6746aaf41d8..9457c2419d2 100644 --- a/packages/bake/src/objects/layers/material-composition/material-composition.test.mts +++ b/packages/bake/src/objects/layers/material-composition/material-composition.test.mts @@ -4,7 +4,7 @@ import assert from 'node:assert/strict'; import { sourceTest } from '@cssearth/objects/node/source-test'; const test = sourceTest(); import {readFile} from 'node:fs/promises'; -import {intersectViewRayWithEllipsoid,rotateSequence,convexHull2d,prepareProjectedEllipsoidSilhouetteCoverage,planetographicRowsToMeshLatitude} from '@cssearth/bake/objects/geometry'; +import {intersectViewRayWithEllipsoid,rotateSequence,convexHull2d,prepareProjectedEllipsoidSilhouetteCoverage,planetocentricSampleRowsToMeshLatitude,planetographicRowsToMeshLatitude} from '@cssearth/bake/objects/geometry'; import { polarQuad, validateMaterialRecipe, prepareLayeredLeafLayouts, prepareLayeredOblateObject, isLayeredOblateRecipe } from '@cssearth/bake/objects/layers/material-composition'; import { writeMaterialAtlasTile, sampleRgbaBilinear, sampleAlphaBilinear, validateRelativePath } from '@cssearth/bake/objects/layers/giant'; import {fitTextureGeometry} from '@cssearth/bake/scene'; @@ -65,7 +65,7 @@ test('material paths and parameters fail before source work',()=>{ }); test('projective leaf layout is derived from the scoped CSS it reads',()=>{ - const stylesheet='.scope .polycss-scene s{width:var(--polycss-atlas-width, 64px);height:var(--polycss-atlas-height, 64px)}.scope .shell > s:not(.demo-interior-pole){background-size:1024px 512px}'; + const stylesheet='.scope .polycss-scene s{width:64px;height:64px}.scope .shell > s:not(.demo-interior-pole){background-size:1024px 512px}'; const config={namespace:'demo',stylesheet:{path:'scoped.css',scope:'.scope '}}; const result=prepareLayeredLeafLayouts({scene:{interior:{shells:[{className:'shell'}]}},stylesheet,config}); assert.deepEqual(result.classes.shell,{width:'64px',height:'64px',backgroundSize:'1024px 512px'}); @@ -88,3 +88,18 @@ test('planetographic map rows move to the parametric latitude of the mesh',()=>{ assert.deepEqual([...planetographicRowsToMeshLatitude(rows,1,height,1,1)],[...rows]); assert.throws(()=>planetographicRowsToMeshLatitude(rows,1,height,1,0.9),/axis ratio/); }); + +test('planetocentric sample rows, poles included, move to the parametric latitude of the mesh',()=>{ + const samples=181,axisRatio=60268/54364,rows=Uint8Array.from({length:samples},(_,y)=>y); + const mesh=planetocentricSampleRowsToMeshLatitude(rows,1,samples,1,axisRatio); + assert.equal(mesh.length,180); + // Mesh row 45 sits at parametric latitude 44.5 deg; its planetocentric latitude is atan(b/a tan 44.5) = 41.6 deg, sample 48.4. + const beta=44.5*Math.PI/180,expected=90-Math.atan(Math.tan(beta)/axisRatio)*180/Math.PI; + assert.equal(mesh[45],Math.round(expected)); + // The first and last mesh rows lie half a row inside the poles: samples 0.55 and 179.45. + assert.ok(mesh[0]===1&&mesh[179]===179); + // On a sphere every mesh row lies midway between two samples. + assert.ok([...planetocentricSampleRowsToMeshLatitude(rows,1,samples,1,1)].every((value,y)=>value===y||value===y+1)); + assert.throws(()=>planetocentricSampleRowsToMeshLatitude(rows,1,samples,1,0.9),/axis ratio/); + assert.throws(()=>planetocentricSampleRowsToMeshLatitude(rows,1,samples+1,1,axisRatio),/dimensions/); +}); diff --git a/packages/bake/src/objects/layers/observed-surfaces/contract.ts b/packages/bake/src/objects/layers/observed-surfaces/contract.ts index 175e4443ba7..39ee3634143 100644 --- a/packages/bake/src/objects/layers/observed-surfaces/contract.ts +++ b/packages/bake/src/objects/layers/observed-surfaces/contract.ts @@ -11,12 +11,12 @@ const continuation = obj({boundaryFraction: n, exponent: n, minimumBoundarySum: export type BoundaryContinuation = s.Infer; const color = obj({channels: s.literal(3, 4), palette: s.tuple(vector3, vector3, vector3), percentiles: vector2, transfer: s.literal('sqrt', 'power'), exponent: n, minimumCoverage: n, positiveValidity: opt(s.boolean)}); export type FalseColor = s.Infer; -const decode = s.union(obj({kind: s.literal('raster'), channels: s.literal(3, 4), crop: opt(region), continuation: opt(continuation)}), obj({kind: s.literal('fits'), bitpix: n, width: n, height: n, color, continuation: opt(continuation)})); +const decode = s.union(obj({kind: s.literal('raster'), channels: s.literal(3, 4), crop: opt(region), continuation: opt(continuation)}), obj({kind: s.literal('fits'), bitpix: n, width: n, height: n, color, continuation: opt(continuation)}), obj({kind: s.literal('fits-planes'), bitpix: n, width: n, height: n, firstRow: s.literal('north', 'south'), crop: opt(region)})); const provenance = obj({authority: str, model: str, sourcePath: opt(str)}); export type BaselineProvenance = s.Infer; const pixelPresence = {alphaValidity: opt(s.boolean), minimumBrightness: n}; export interface PixelPresence {alphaValidity?: boolean; minimumBrightness: number} -const coverage = s.union(obj({kind: s.literal('component-fits'), sources: s.tuple(str,str,str), unobservedRows: arr(vector2), reverseLongitude: s.boolean, longitudeOffsetDegrees: n, longitudePeriod: n}), obj({kind: s.literal('boundary-mean'), boundaryFraction: n, exponent: n, minimumBoundarySum: opt(n)}), obj({kind: s.literal('uniform-baseline'), ...pixelPresence, minimumCoverage: n, minimumRowCoverage: n, equatorialInsetRows: n, allowedBoundaryRange: vector2, transitionRows: n, baseline: opt(str), baselineProvenance: opt(provenance)})); +const coverage = s.union(obj({kind: s.literal('component-fits'), sources: s.tuple(str,str,str), unobservedRows: arr(vector2), reverseLongitude: s.boolean, longitudeOffsetDegrees: n, longitudePeriod: n}), obj({kind: s.literal('declared-rows'), unobservedRows: arr(vector2), reverseLongitude: s.boolean, longitudeOffsetDegrees: n, longitudePeriod: n}), obj({kind: s.literal('boundary-mean'), boundaryFraction: n, exponent: n, minimumBoundarySum: opt(n)}), obj({kind: s.literal('uniform-baseline'), ...pixelPresence, minimumCoverage: n, minimumRowCoverage: n, equatorialInsetRows: n, allowedBoundaryRange: vector2, transitionRows: n, baseline: opt(str), baselineProvenance: opt(provenance)})); export type UniformCoverage = Extract, {kind: 'uniform-baseline'}>; const baseline = obj({id: str, source: str, minimumBrightness: n, minimumCoverage: n, radiusFraction: n, minimumSampleShare: n, provenance}); export type DiscBaseline = s.Infer; @@ -28,7 +28,7 @@ const polarProjection = obj({tileSize: n, poles: arr(s.literal('north', 'south') export type PolarProjection = s.Infer; const productCommon = {filename: str, encoding: webpEncoding, transforms: opt(arr(transform)), removeAlpha: opt(s.boolean)}; const product = s.union(obj({...productCommon, kind: s.literal('surface'), packing: obj({bandCount: n, gutter: n, overscan: opt(n)})}), obj({...productCommon, kind: s.literal('poles'), projection: polarProjection}), obj({...productCommon, kind: s.literal('thumbnail')})); -const dataset = obj({id: str, source: str, decode, products: arr(product), transforms: opt(arr(transform)), coverage: opt(coverage), calibration: opt(calibration), atmosphereColor: opt(brightTail), planetographicAxisRatio: opt(n)}); +const dataset = obj({id: str, source: str, decode, products: arr(product), transforms: opt(arr(transform)), coverage: opt(coverage), calibration: opt(calibration), atmosphereColor: opt(brightTail), planetographicAxisRatio: opt(n), planetocentricSampleRows: opt(n)}); export const observedRecipe = obj({schema: s.literal('cssearth-observed-surfaces@2'), sources: arr(sourcePin), datasets: arr(dataset), baselines: opt(arr(baseline))}); export type ObservedSurfaceRecipe = s.Infer; export interface RasterMap {data: Buffer; width: number; height: number; channels: 1 | 2 | 3 | 4; missing?: Uint8Array; atmosphereColor?: number[]; calibration?: unknown; coverage?: {baselineColor: readonly number[]}; stretch?: number[]} diff --git a/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.test.mts b/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.test.mts index 73dd009cd37..cd34a10de29 100644 --- a/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.test.mts +++ b/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.test.mts @@ -38,6 +38,36 @@ test('dated component coverage normalizes longitude and preserves isolated dark }finally{await rm(directory,{recursive:true,force:true});} }); +test('a color cube with declared rows reads its first row as stated, reverses longitude and moves sample rows to mesh latitude',async()=>{ + const directory=await mkdtemp(join(tmpdir(),'fits-planes-')); + try{ + // 5 samples of longitude with the end meridian repeated, 5 sample rows from pole to pole, stored north first + const width=5,height=5,value=(plane: number,y: number,x: number)=>plane===0?(x===0?250:10*(x%4)+y):plane===1?100+y:200; + const header=Buffer.from([card('SIMPLE','T'),card('BITPIX','-32'),card('NAXIS','3'),card('NAXIS1',String(width)),card('NAXIS2',String(height)),card('NAXIS3','3'),'END'.padEnd(80)].join('').padEnd(2880)); + const data=Buffer.alloc(2880);for(let plane=0;plane<3;plane++)for(let y=0;y({id:'cube',source:'cube.fits',decode:{kind:'fits-planes',bitpix:-32,width,height,firstRow:'north'},coverage:{kind:'declared-rows',unobservedRows:[[2,2]],reverseLongitude:true,longitudeOffsetDegrees:0,longitudePeriod:4},products:[{kind:'thumbnail',filename:'cube.webp',encoding:{lossless:true}}],...extra}); + const prepare=(extra: object={})=>prepareObservedSurfaces({sourceDirectory:directory,config:{schema:'cssearth-observed-surfaces@2',sources:[{path:'cube.fits'}],datasets:[dataset(extra)]}}); + const map=(await prepare()).maps.get('cube');assert.ok(map); + assert.deepEqual([map.width,map.height],[5,5]); + assert.deepEqual([0,1,2,3,4].map(x=>map.data[x*4]),[250,30,20,10,250]); // column x shows stored column 4 - x of the 4-sample period + assert.deepEqual([...map.data.slice((4*5+1)*4,(4*5+1)*4+4)],[34,104,200,255]); // the last stored row is the south pole + // A file whose first column holds no map: cropped away, and the offset of one sample brings the end meridian to column 0. + const cropped=(await prepare({decode:{kind:'fits-planes',bitpix:-32,width,height,firstRow:'north',crop:{left:1,top:0,width:4,height:5}},coverage:{kind:'declared-rows',unobservedRows:[[2,2]],reverseLongitude:true,longitudeOffsetDegrees:-90,longitudePeriod:4}})).maps.get('cube'); + assert.deepEqual([cropped?.width,[0,1,2,3].map(x=>cropped?.data[x*4])],[4,[0,30,20,10]]); + assert.ok(map.missing?.slice(2*5,3*5).every(missing=>missing===1));assert.equal(map.missing?.[5],0); + const south=(await prepare({decode:{kind:'fits-planes',bitpix:-32,width,height,firstRow:'south'}})).maps.get('cube'); + assert.equal(south?.data[1],104); // read south first, the same bytes put stored row 4 at the top + const mesh=(await prepare({planetocentricSampleRows:1})).maps.get('cube');assert.ok(mesh); + assert.deepEqual([mesh.width,mesh.height],[5,4]); + assert.equal(mesh.data[1],101); // on a sphere a mesh row lies midway between two sample rows: (100 + 101) / 2, rounded + assert.deepEqual([...mesh.missing!.slice(0,20)].filter((_,i)=>i%5===0),[0,1,1,0]); // rows that touch the declared row are missing + await writeFile(join(directory,'cube.fits'),Buffer.concat([header,Buffer.from(data).fill(0x43,0,4)])); // 0x43434343 is 195.26, not a display value + await assert.rejects(prepare(),/8-bit display values/); + assert.throws(()=>parseObservedSurfaceRecipe({schema:'cssearth-observed-surfaces@2',sources:[{path:'cube.fits'}],datasets:[dataset({decode:{kind:'fits-planes',bitpix:-32,width,height,firstRow:'north',crop:{left:2,top:0,width:4,height:5}}})]}),/FITS plane geometry/); + }finally{await rm(directory,{recursive:true,force:true});} +}); + test('boundary continuation retains observed rows and varies only source-derived longitude',()=>{ const input=new Float32Array([0,0,0,0,2,6,10,12,14,16,18,20]); const result=continueBoundaryMean(input,{width:2,height:6,channels:1,boundaryFraction:1/3,exponent:3,minimumBoundarySum:0}); diff --git a/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.ts b/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.ts index fd86694cfc8..d9b973b8b5b 100644 --- a/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.ts +++ b/packages/bake/src/objects/layers/observed-surfaces/observed-surfaces.ts @@ -5,8 +5,8 @@ import type {SourcePin} from '../../geometry/index.ts'; import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises'; import { relative, resolve, sep } from 'node:path'; import sharp from 'sharp'; -import { readFitsPrimary } from '@cssearth/fits'; -import { planetographicRowsToMeshLatitude } from '../../geometry/index.ts'; +import { readFitsImage, readFitsPrimary } from '@cssearth/fits'; +import { planetocentricSampleRowsToMeshLatitude, planetographicRowsToMeshLatitude } from '../../geometry/index.ts'; import { packProjectiveSurfaceRaster } from '../../../scene/index.ts'; import { missingCoverageColor } from '../../../raster/index.ts'; import { resizeObservedRgb, sampleObservedRgb } from './coverage.ts'; @@ -31,14 +31,20 @@ export function parseObservedSurfaceRecipe(input: unknown) { const operations=(items: readonly ObservationTransform[] | undefined)=>{for(const op of items??[]){if(!['flip','crop','resize','sharpen'].includes(op.kind))throw new TypeError('Unsupported observation transform.');if(op.kind==='resize'&&!dimensions(op.width,op.height))throw new TypeError('Invalid resize dimensions.');if(op.kind==='crop'&&(!op.region||!dimensions(op.region.width,op.region.height)||![op.region.left,op.region.top].every(v=>Number.isSafeInteger(v)&&v>=0)))throw new TypeError('Invalid crop dimensions.');if(op.kind==='sharpen'&&!positive(op.options?.sigma))throw new TypeError('Invalid sharpening radius.');}}; for(const dataset of config.datasets){ if(!OBSERVATION_RECIPE_POLICY.identifier.test(dataset.id)||ids.has(dataset.id)||!sourcePaths.has(dataset.source)||!dataset.decode)throw new TypeError('Invalid observation identity.');ids.add(dataset.id); - if(!['raster','fits'].includes(dataset.decode.kind))throw new TypeError('Unsupported observation decoder.'); + if(!['raster','fits','fits-planes'].includes(dataset.decode.kind))throw new TypeError('Unsupported observation decoder.'); + if(dataset.decode.kind==='fits-planes'){ + const {bitpix,width,height,crop}=dataset.decode; + if(bitpix!==-32||!dimensions(width,height)||crop&&(!dimensions(crop.width,crop.height)||![crop.left,crop.top].every(v=>Number.isSafeInteger(v)&&v>=0)||crop.left+crop.width>width||crop.top+crop.height>height))throw new TypeError('Invalid FITS plane geometry.'); + } + if(dataset.coverage?.kind==='declared-rows'&&dataset.calibration)throw new TypeError('Declared-row coverage does not take a calibration.'); + if(dataset.planetographicAxisRatio!==undefined&&dataset.planetocentricSampleRows!==undefined)throw new TypeError('A map states one latitude convention.'); if(dataset.decode.kind==='raster'&&![3,4].includes(dataset.decode.channels))throw new TypeError('Invalid raster channels.'); if(dataset.decode.kind==='fits'){ const color=dataset.decode.color; if(dataset.decode.bitpix!==-32||!dimensions(dataset.decode.width,dataset.decode.height)||!color||![3,4].includes(color.channels)||!isArray(color.palette)||color.palette.length!==3||color.palette.some(rgb=>!isArray(rgb)||rgb.length!==3||rgb.some(c=>!Number.isSafeInteger(c)||c<0||c>255))||!isArray(color.percentiles)||color.percentiles.length!==2||!color.percentiles.every(fraction)||color.percentiles[0]>=color.percentiles[1]||color.percentiles[1]===1||!['sqrt','power'].includes(color.transfer)||!positive(color.exponent)||!fraction(color.minimumCoverage))throw new TypeError('Invalid scientific color mapping.'); } if(dataset.coverage?.kind==='component-fits'&&(dataset.decode.kind!=='raster'||dataset.decode.crop||dataset.calibration||dataset.coverage.sources.some(path=>!sourcePaths.has(path))))throw new TypeError('Component coverage requires an uncropped RGB map and declared FITS sources.'); - for(const continuation of[dataset.decode.continuation,dataset.coverage?.kind==='boundary-mean'?dataset.coverage:null])if(continuation&&(!positive(continuation.exponent)||!positive(continuation.boundaryFraction)||continuation.boundaryFraction>=1||!Number.isFinite(continuation.minimumBoundarySum ?? 0)))throw new TypeError('Invalid boundary continuation.'); + for(const continuation of[dataset.decode.kind==='fits-planes'?undefined:dataset.decode.continuation,dataset.coverage?.kind==='boundary-mean'?dataset.coverage:null])if(continuation&&(!positive(continuation.exponent)||!positive(continuation.boundaryFraction)||continuation.boundaryFraction>=1||!Number.isFinite(continuation.minimumBoundarySum ?? 0)))throw new TypeError('Invalid boundary continuation.'); if(dataset.calibration&&(!sourcePaths.has(dataset.calibration.source)||(dataset.decode.kind !== 'raster' || dataset.decode.channels!==3)))throw new TypeError('Invalid true-color calibration input.'); operations(dataset.transforms); if(!isArray(dataset.products)||!dataset.products.length)throw new TypeError('Observation has no products.'); @@ -212,6 +218,25 @@ async function decodeRaster(bytes: Buffer | undefined,config: {channels: 3 | 4; const {data,info}=await pipeline.raw().toBuffer({resolveWithObject:true});return {data,width:info.width,height:info.height,channels:info.channels}; } +/** A publisher's color cube: three FITS planes that hold display values, 0 to 255, read as red, green and blue. FITS + * stores its first row first; the recipe says which pole that row is, because a header can describe the other order. + * A crop, counted from the north-up map's top left, leaves out rows or columns the file holds no map in. */ +function decodeFitsPlanes(bytes: Buffer | undefined,config: {bitpix: number; width: number; height: number; firstRow: 'north' | 'south'; crop?: Region}): RasterMap { + if (!bytes) throw new TypeError('Observation has no verified source.'); + const planes=[1,2,3].map(plane=>readFitsImage(bytes,{plane})),{left,top,width,height}=config.crop??{left:0,top:0,width:config.width,height:config.height}; + if(planes.some(plane=>plane.planes!==3||plane.bitpix!==config.bitpix||plane.width!==config.width||plane.height!==config.height))throw new Error('Observed FITS geometry drifted.'); + const data=Buffer.alloc(width*height*3); + for(let y=0;y255)throw new Error('Observed FITS planes are not 8-bit display values.'); + data[(y*width+x)*3+c]=value; + } + } + return {data,width,height,channels:3}; +} + /** Intersect the three measured filters before resampling the publisher's RGB image. * The recipe names whether the longitude endpoint repeats. Reversal and the * declared offset put releases with different origins in the same body frame. */ @@ -222,6 +247,11 @@ function componentCoverage(map: RasterMap, config: Extract[first,last])}).missing; }); + return declaredCoverage(map,config,masks); +} +/** The rows a recipe declares unobserved, with any measured masks, laid out in the body frame: the map loses its color + * there and nothing is detected from pixel values. A source whose own file marks no missing sample takes this alone. */ +function declaredCoverage(map: RasterMap, config: {unobservedRows: readonly (readonly [number,number])[]; reverseLongitude: boolean; longitudeOffsetDegrees: number; longitudePeriod: number}, masks: readonly Uint8Array[]=[new Uint8Array(map.width*map.height)]) : RasterMap { const {width,height}=map,period=config.longitudePeriod,shift=config.longitudeOffsetDegrees/360*period; if(!Number.isInteger(shift)||!Number.isInteger(period)||![width,width-1].includes(period)||period<1)throw new Error('Map longitude offset must match the source grid.'); for(const [first,last] of config.unobservedRows){ @@ -280,8 +310,10 @@ export async function prepareObservedSurfaces({sourceDirectory,publicDirectory,c const fits=readFitsPrimary(inputs.get(dataset.source));if(fits.bitpix!==dataset.decode.bitpix||fits.width!==dataset.decode.width||fits.height!==dataset.decode.height)throw new Error('Observed FITS geometry drifted.'); let values=new Float32Array(fits.values);if(dataset.decode.continuation)values=continueBoundaryMean(values,{width:fits.width,height:fits.height,channels:1,...dataset.decode.continuation}); map=percentileFalseColor(values,fits.width,fits.height,dataset.decode.color); - }else throw new TypeError('Unsupported observation decoder.'); + }else if(dataset.decode.kind==='fits-planes')map=decodeFitsPlanes(inputs.get(dataset.source),dataset.decode); + else throw new TypeError('Unsupported observation decoder.'); if(dataset.coverage?.kind==='component-fits')map=componentCoverage(map,dataset.coverage,inputs); + if(dataset.coverage?.kind==='declared-rows')map=declaredCoverage(map,dataset.coverage); let calibration; if(dataset.calibration){const target=await decodeRaster(inputs.get(dataset.calibration.source),{crop:dataset.calibration.targetSample,channels:3});calibration=affineColorCalibration(map,target,dataset.calibration);} if(dataset.coverage?.kind==='boundary-mean')map={...map,data:continueBoundaryMean(map.data,{...map,...dataset.coverage})}; @@ -293,6 +325,8 @@ export async function prepareObservedSurfaces({sourceDirectory,publicDirectory,c if(calibration){const data=Buffer.allocUnsafe(map.data.length);for(let offset=0;offsetmap.data[i*4+3]===255?0:1); const nativePoleMap=map,transformedMap=await transformMap(map,dataset.transforms); if(dataset.atmosphereColor)transformedMap.atmosphereColor=brightTailColor(transformedMap.data,dataset.atmosphereColor);maps.set(dataset.id,transformedMap); diff --git a/packages/bake/src/photometry/limb.test.mts b/packages/bake/src/photometry/limb.test.mts index 8c5436fe0e3..f544b09baf0 100644 --- a/packages/bake/src/photometry/limb.test.mts +++ b/packages/bake/src/photometry/limb.test.mts @@ -22,6 +22,18 @@ test('limb factors are 1 at the flood-lit disc centre, follow the law elsewhere assert.ok(Math.abs(side - Math.exp(-0.5628 * Math.PI / 3) * (0.6424 * 2 * 0.5 / 1.5 + 0.3576 * 0.5)) < 1e-12); }); +test('a grazing texel under flood light keeps the held law only when its incidence is held with its floored emission', () => { + // The fitted edge is 80 degrees in these records. A normal 0.006 degrees from the limb, lit from the viewer, with the + // one-texel emission floor of a 256-pixel frame: the emission is floored, the incidence is not. + const law = limbLawFromRecords(paths, [minnaert(0.8), minnaert(0.7), minnaert(0.73)]), view = [0, 0, 1]; + const { incidence, emission, phase } = scatteringAngles([Math.sqrt(1 - 1e-8), 0, 1e-4], view, view, 1 / 256); + assert.ok(phase < 1e-6 && incidence > emission && emission > 80 * degrees); + // Passed as they are, the angles are not flood light to the law, and the texel falls to a fifth of the held value. + const edge = Math.cos(80 * degrees), held = limbFactors(law, emission, emission, phase), collapsed = limbFactors(law, incidence, emission, phase); + assert.ok(Math.abs(held[0] - edge ** 0.6) < 1e-12); + assert.ok(collapsed[0] < held[0] / 5); +}); + test('the overlay is exact for the reference color and for the channel that sets its alpha', () => { const reference = [180, 120, 60] as const, factors = [0.9, 0.5, 1.4]; const [r, g, b, alpha] = limbOverlay(factors, reference); diff --git a/packages/bake/src/photometry/whole-disc-color.test.mts b/packages/bake/src/photometry/whole-disc-color.test.mts index 5ecf36d46de..04fa10465e0 100644 --- a/packages/bake/src/photometry/whole-disc-color.test.mts +++ b/packages/bake/src/photometry/whole-disc-color.test.mts @@ -26,9 +26,9 @@ test('a whole-disc color record refuses unknown keys, a wrong schema and non-pos test('a map with its limb law divided out is tied through the law\'s disc means, which for Minnaert are 2 / (2k + 1)', async () => { const saturn = resolve(projectRoot(import.meta.url), 'src/objects/saturn/source'); - const law = await loadLimbLaw(saturn, ['photometry/opal-2025-minnaert-f631n.json', 'photometry/opal-2025-minnaert-f502n.json', 'photometry/opal-2025-minnaert-f467m.json']); + const law = await loadLimbLaw(saturn, ['photometry/opal-2025-minnaert-display-red.json', 'photometry/opal-2025-minnaert-display-green.json', 'photometry/opal-2025-minnaert-display-blue.json']); // Held at 86.3 degrees, the OPAL disc edge, the means stay within 0.001 of the full-disc Minnaert integral. - floodDiscMean(law).forEach((mean, channel) => assert.ok(Math.abs(mean - 2 / (2 * [0.8, 0.65, 0.86][channel]! + 1)) < 1e-3, `channel ${channel}: ${mean}`)); + floodDiscMean(law).forEach((mean, channel) => assert.ok(Math.abs(mean - 2 / (2 * [0.798, 0.698, 0.728][channel]! + 1)) < 1e-3, `channel ${channel}: ${mean}`)); assert.deepEqual(displayBandRatios(parseWholeDiscColor(record), [0.5, 1, 0.25]).ratios, { green: 0.4, blue: 1 }); assert.throws(() => displayBandRatios(parseWholeDiscColor(record), [1, 0, 1]), /disc means must be positive/u); }); diff --git a/src/objects/saturn/NOTICE.md b/src/objects/saturn/NOTICE.md index 655c06f5a1f..16f92ded495 100644 --- a/src/objects/saturn/NOTICE.md +++ b/src/objects/saturn/NOTICE.md @@ -8,12 +8,20 @@ CO-SR-UVIS-HSP-2/4-OCC-V3.0), from the PDS Ring-Moon Systems Node. The globe's limb law is the Minnaert table of the Hubble OPAL Saturn 2025 README (NASA, ESA and the OPAL team), transcribed as facts in -`source/photometry/`. The visible map's color balance is tied to a color +`source/photometry/` and weighted there over each display channel. The +visible map's color balance is tied to a color computed from E. Karkoschka's 1995 full-disc albedo spectrum of Saturn (Icarus 133, 1998), archived by the NASA PDS Atmospheres Node; the computed color is recorded in `source/photometry/`, and the table is cited, not redistributed. +The Cassini 2011 dataset is the contrast-enhanced color map of the PDS bundle +Cassini ISS Global Maps of Jupiter and Saturn (L. Li, R. A. West, X. Jiang +and B. Knowles; Cassini ISS, NASA/JPL-Caltech/Space Science Institute; NASA +PDS Atmospheres Node, doi:10.17189/rkkb-6y30), a public NASA archive product +described by Wang, Li, Jiang and West (2025, Scientific Data). No figure of +that paper is reproduced. + The ultraviolet and methane observation inputs are Hubble OPAL products from NASA, ESA, and the OPAL team. The atmospheric charts are NASA Planetary Spectrum Generator models. diff --git a/src/objects/saturn/README.md b/src/objects/saturn/README.md index 494f98da7b4..619f9b80b76 100644 --- a/src/objects/saturn/README.md +++ b/src/objects/saturn/README.md @@ -1,7 +1,8 @@ # Saturn sources Saturn combines a Hubble OPAL visible body map, Hubble spectral maps, a Cassini -UVIS ring opacity profile, and modeled atmosphere charts. +map of the 2011 storm clouds, a Cassini UVIS ring opacity profile, and modeled +atmosphere charts. Source selections, recorded trials and open questions are in the [investigation ledger](investigations.json). @@ -17,6 +18,7 @@ Source selections, recorded trials and open questions are in the [investigation | Interior | [Mankovich and Fuller (2021)](https://doi.org/10.1038/s41550-021-01448-3) and [Movshovitz density profiles](https://doi.org/10.7291/D1P07G) | | Atmosphere charts | [NASA Planetary Spectrum Generator](https://psg.gsfc.nasa.gov/), modeled 29 August 2026 | | Dated visible maps | Eight [OPAL](https://archive.stsci.edu/hlsp/opal) rotations, 2018 to 2025 (table below) | +| Storm clouds of 2011 | [Cassini ISS global color map](https://doi.org/10.17189/rkkb-6y30), contrast-enhanced, 2011-08-11 (NASA PDS Atmospheres Node; [Wang et al. 2025](https://doi.org/10.1038/s41597-025-04392-3)) | [Inputs](source/manifest.json) · [Recipe](object.json) · [Credits](NOTICE.md) · [Contributor guide](../README.md) @@ -53,17 +55,37 @@ full-disc albedo of Saturn, times a 5,772 K Planck spectrum, through the CIE [`source/photometry/karkoschka-1998-whole-disc-color.json`](source/photometry/karkoschka-1998-whole-disc-color.json). The tie allows for the limb law, so the flood-lit disc integrates to Karkoschka's color. The map then gets back its untied luminance with one -factor on all three channels, 1.269, and a soft shoulder keeps bright texels +factor on all three channels, 1.239, and a soft shoulder keeps bright texels from clipping. Spatial color differences stay the map's own. An independent spectrum agrees: Payne et al. (2026) gives green/red 0.863 and blue/red 0.589, where Karkoschka's spectrum gives 0.863 and 0.593. -Lighting. The material overlays put back the limb darkening OPAL removed, with -the Cycle 32 readme's Minnaert coefficients: k 0.80 in F631N (red) and 0.65 in -F502N (green). The blue channel takes F467M's k 0.86, not F395N's 0.40; with -F395N's k the limb turned gray-blue. The coefficients are recorded in -`source/photometry/opal-2025-minnaert-*.json`. A `#fff1ea` solar multiplier, -from the 5,772 K photosphere of NASA's +Lighting. The material overlays put back the limb darkening OPAL removed. The +Cycle 32 readme gives one Minnaert coefficient per filter: 0.40 in F395N, 0.86 +in F467M, 0.65 in F502N, 0.80 in F631N and 0.85 in F763M. A display channel +shows a range of wavelengths, so each channel takes those coefficients weighted +by the light it shows: Saturn's spectrum in sunlight through the CIE 1931 +observer, the computation behind the whole-disc color. Between two filters the +coefficient is interpolated in wavelength, a join that is ours, and the +methane-band filters FQ727N and FQ889N are left out. The result is k 0.798 for +red, 0.698 for green and 0.728 for blue, written by +[`display-limb.mts`](../../../packages/bake/authoring/saturn/display-limb.mts) +to `source/photometry/opal-2025-minnaert-display-*.json`. A weighted sum of +Minnaert laws is not a Minnaert law: the recorded power laws depart from the +weighted profiles by at most 0.004, 0.0001 and 0.028 of the disc-centre +brightness. Until 2026-10-05 each channel took its nearest filter (0.80, 0.65 +and 0.86): green darkened least and the limb turned olive. + +The law is held at the edge of its data, 86.3 degrees of emission, and under +flood light the incidence is held with it. Until 2026-10-05 the 256-pixel +frame floored a grazing texel's emission at one texel but not its incidence, +so the law was held on one angle only and fell to black on scattered texels of +the outermost ring. A texel is 8 screen pixels at 2x, and those texels read as +dark dashes along the limb. Past the silhouette the overlay loses its color, +so over space it only darkens; a texel the silhouette crosses keeps its color +by the share of it that lies inside. + +A `#fff1ea` solar multiplier, from the 5,772 K photosphere of NASA's [Sun fact sheet](https://nssdc.gsfc.nasa.gov/planetary/factsheet/sunfact.html), is multiplied into the surface during preparation. @@ -181,11 +203,72 @@ graticule marks it. The dates reuse the visible scene's rings and lighting. ![Dated OPAL map with the shared sequence controls](evidence/opal-dates-desktop.webp) +## Cassini 2011 + +The Cassini dataset is the contrast-enhanced color map of the PDS bundle +[Cassini ISS Global Maps of Jupiter and Saturn](https://doi.org/10.17189/rkkb-6y30) +(Li, West, Jiang and Knowles, 2023), described by +[Wang, Li, Jiang and West (2025)](https://doi.org/10.1038/s41597-025-04392-3). +Fifteen wide-angle frames of 2011-08-11, five each in the RED, GRN and BL1 +filters at 161 km per pixel, were mapped on a 0.1 degree grid of planetocentric +latitude and west longitude. The band of disturbed clouds around the north is +what the [great storm first seen on 5 December 2010](https://science.nasa.gov/resource/great-disturbances-2/) +left behind. + +It is a dataset, not the default body. Its detail is finer than the Hubble +map's: north of the equator the fine detail's correlation length along +longitude is 0.14 to 0.18 degrees, against 0.23 to 0.33 for Hubble's 2025 map. +But 17.7 % of the map holds no data, its values are 8-bit display levels +stretched per color, not natural color, and it shows 2011. + +Preparation checked the file before using it: + +- **Row order.** The first stored row is 90 degrees north, though the `LAT_C` + card reads `-90:0.1:90`. The storm, in the north, and the rings' shadow, in + the south with the Sun 10.7 degrees north, fix the order. +- **Longitude direction.** Stored column c is west longitude 0.1 c degrees, + though the `LON_W` card reads `360.0:-0.1:0.0`. Raw frame `W1691726068_1`, + projected with the geometry the + [PDS Ring-Moon Systems Node](https://opus.pds-rings.seti.org/opus/#/opusid=co-iss-w1691726068) + gives for it (sub-spacecraft longitude 297.1 degrees west), correlates 0.62 + with the prepared dataset's storm band and 0.23 with its mirror image. +- **A blank column.** The first stored column is zero on 1,595 of 1,801 rows. + The recipe crops it and takes the 0 degree meridian from the last column. +- **No data.** The file is zero from 79.0 degrees north and from 82.1 degrees + south to the poles (planetocentric), on a line at the equator where the + rings crossed the disc, and from 5.4 to 17.1 degrees south, where the rings' + shadow fell. The recipe names those rows and the dataset draws the gray grid + there. The rows from 1.9 to 5.3 degrees south are dimmed by the same shadow + and are shown as archived. + +[The observation recipe](source/preparation/observations.json) reads the +cube's three planes as red, green and blue, moves its rows from planetocentric +latitude to the mesh's own and changes no color. The columns are point +samples drawn as cells, 0.05 degrees east of their stated longitude. The +dataset is lit with the default map's limb law: the archive removed the +frames' own shading with an empirical method and publishes no coefficients. + ## Evidence Source inspection found every dated map correlates more strongly with its component FITS rows in stored order than after a north/south flip. +On 2026-10-05 the default view was captured in headless Chrome with a GPU, at +1440 by 900 and 2x pixel density, on css.earth v0.6524 and on this version, +and measured on the same rays through 32 degrees of the upper-left limb. Dark +dips deeper than 12 levels, 3 pixels inside the edge, went from 29 to 3; +Jupiter has none. Five pixels inside the edge, green/red went from 1.02 to +0.89 and blue/red from 0.55 to 0.66. With shadows on, the lit lower-left limb +went from 32 dips to 19 along 28 degrees. Safari was not measured: headless +WebKit drew only the body's fill disc. Left before, right after, under flood +light, at screen pixels: + +![The upper-left limb before and after](evidence/2026-10-05/limb-before-after.webp) + +The Cassini dataset in the app: + +![The Cassini 2011 dataset on Saturn](evidence/2026-10-05/cassini-2011-desktop.webp) + ## Known problems - Ultraviolet and methane views contain filled rows, and their rings are the visible-light opacity profile. @@ -193,6 +276,9 @@ component FITS rows in stored order than after a north/south flip. - Interior layers are illustrations. - Narrow ring features are widened and brightened for readability; they do not establish optical depth or fully resolved ringlets. - Rotation is accelerated. The camera, shadows and background orientation are presentation choices, and source observations come from different dates. -- The visible map's color balance is tied to one whole-disc spectrum from 1995; the 2025 map's own cloud colors are kept, but a seasonal change in Saturn's overall color since 1995 would not show. The tie sets channel ratios only; the map's overall brightness is still the archive TIF's arbitrary scale, kept at its untied mean, and its brightest 7 % of texels are compressed by a soft shoulder. -- The map's blue channel is F395N (violet) data, displayed as sRGB blue with the F467M limb law. The navigation portrait and context image still crop the untied TIF. +- The visible map's color balance is tied to one whole-disc spectrum from 1995; the 2025 map's own cloud colors are kept, but a seasonal change in Saturn's overall color since 1995 would not show. The tie sets channel ratios only; the map's overall brightness is still the archive TIF's arbitrary scale, kept at its untied mean, and its brightest 5 % of texels are compressed by a soft shoulder. +- The map's blue channel is F395N (violet) data, displayed as sRGB blue. Each display channel's limb law is a weighted mean of OPAL's per-filter coefficients, interpolated between filters. The navigation portrait and context image still crop the untied TIF. +- The visible map is Hubble's, 1,800 pixels around the planet. At the default view on a 2x screen one source pixel covers 1.8 screen pixels, against 0.9 on Jupiter, so Saturn is softer. A 4,096-pixel body image showed 4 % more fine detail than the shipped 2,048 in a composed default view (2026-10-05) and was not shipped. +- The Cassini 2011 dataset is the archive's contrast-enhanced color with no calibration, lit with the Hubble map's limb law under the present scene's rings. +- The limb overlay's frames are 256 pixels wide and drawn 4 times larger. With shadows on, the lit limb still shows dashes, fewer and fainter: 19 dark dips along 28 degrees of it, where css.earth v0.6524 had 32, and half the fine-scale brightness variation (8.0 against 18.6 levels). There the law brightens the limb and the overlay turns black past the silhouette. - The material overlay has one color and alpha per texel, so the per-channel limb law is exact for the prepared surface's mean color and approximate for colors far from it ([planet limbs](../../../docs/surface-preparation.md#planet-limbs-from-published-laws)). OPAL's coefficients are for near-zero phase; directional frames use them at every phase. diff --git a/src/objects/saturn/evidence/2026-10-05/cassini-2011-desktop.webp b/src/objects/saturn/evidence/2026-10-05/cassini-2011-desktop.webp new file mode 100644 index 00000000000..9dd343ee055 Binary files /dev/null and b/src/objects/saturn/evidence/2026-10-05/cassini-2011-desktop.webp differ diff --git a/src/objects/saturn/evidence/2026-10-05/limb-before-after.webp b/src/objects/saturn/evidence/2026-10-05/limb-before-after.webp new file mode 100644 index 00000000000..fd4769ed24b Binary files /dev/null and b/src/objects/saturn/evidence/2026-10-05/limb-before-after.webp differ diff --git a/src/objects/saturn/inventory.json b/src/objects/saturn/inventory.json index 657f2715820..684ffa548db 100644 --- a/src/objects/saturn/inventory.json +++ b/src/objects/saturn/inventory.json @@ -4,8 +4,8 @@ { "location": "public", "filename": "saturn-arrival.webp", - "bytes": 175096, - "sha256": "3d6ed2e3590e81b78b1fe662003c5e99c3e8d230b5744e85613c3e3c94dfdd7a" + "bytes": 173194, + "sha256": "e13617566b5f8e833c74562db1644f31e9ad76988c9fa4372b6720bbc5ab967f" }, { "location": "public", @@ -16,26 +16,32 @@ { "location": "public", "filename": "saturn-billboard.webp", - "bytes": 15440, - "sha256": "6243bb6438cb81b067ea5ebfcd0ab12ddfb40fb637138a5cbe24e17e727499aa" + "bytes": 14814, + "sha256": "8792f94998e86d02bb7c3b82b21fbfc3931bbfcbc02524ff2d9a9763b2b55dea" + }, + { + "location": "public", + "filename": "saturn-dataset-cassini-2011.webp", + "bytes": 1714, + "sha256": "f8748ba5bdeed02003a4696e998412722511f6b62e1f125eed7092a211e4178e" }, { "location": "public", "filename": "saturn-dataset-methane.webp", - "bytes": 630, - "sha256": "6b7adf5d734a2fe72d10806fb3d66cb6efacaa6b98cf363621916a5e13dd0c16" + "bytes": 604, + "sha256": "2e0fc46c753f77483c052722d42e1409c54ba86f0a147ad94b93e42cd8609b5f" }, { "location": "public", "filename": "saturn-dataset-normal.webp", - "bytes": 324, - "sha256": "b6c385126cc0db4ab5bf74304016b0caa204f62bcc338d19353bff98612cf144" + "bytes": 314, + "sha256": "451c831e84cb833ccd9d4348bcb02d13664eada0a06d017285130d6cc4371495" }, { "location": "public", "filename": "saturn-dataset-ultraviolet.webp", - "bytes": 418, - "sha256": "ad97a7bf159fdba2a8957bd4fac472b8b5691981180342cf52ca0a5f27187912" + "bytes": 426, + "sha256": "4e3e9e53278fa66869933505ade7b508c2fd2006fd5ead3e22612620c5d9211b" }, { "location": "public", @@ -100,626 +106,626 @@ { "location": "public", "filename": "saturn-interior-outer-poles-methane@2x.webp", - "bytes": 9012, - "sha256": "f924b5c8f0e996627434fa0942fb71586ff28a09c1f91ec15e6cbc043901a381" + "bytes": 8214, + "sha256": "c29aba67309be52859f5647574af45f8a1e788077a13778b62d5e34c45b94c17" }, { "location": "public", "filename": "saturn-interior-outer-poles-ultraviolet@2x.webp", - "bytes": 8518, - "sha256": "5697e1118539843ca4cf4fc18f5befac62547722c4651b64c90b5cd2c6cd2c71" + "bytes": 7862, + "sha256": "d9d43d1f18bbc862564d3c1982ae43ceae5ded2c31e2507041141e6ef558362d" }, { "location": "public", "filename": "saturn-orbit-material-methane-no-shadows-shadowless.webp", - "bytes": 16536, - "sha256": "22a037ec82ed6385d2c1c9f6ba53ce53f1494876f130a7d455ac7fad23757b06" + "bytes": 15098, + "sha256": "9ed852dd770eed97587411866473f379e603dbd552e1951e16eb0a12091dd502" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-no-shadows-shadowless.webp", - "bytes": 16814, - "sha256": "b8678ae844e8a9d306eba564e9e1b6d8945d03ef8ed5a451bb8acc25d2103044" + "bytes": 15094, + "sha256": "82834d73ae7193d6502644c7bcb71d0661300f6b477fcea35bb9d58a15575c9f" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-00.webp", - "bytes": 443866, - "sha256": "f16e1b5eb36976e61c1ca2ecb941159fb036835f10e82ad67b7ee58d2a04c1d3" + "bytes": 417426, + "sha256": "aaca7dc17e52090270da53c3cd330a4908959c999eb6616f1d32cc290e0e069c" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-01.webp", - "bytes": 461072, - "sha256": "ca00451f51485b7fb04192c7d6094b239e93dd95a3444acbfc9eeac65fc4c137" + "bytes": 418238, + "sha256": "3f87e3712b1e2bdf7e68db051e08a946bc1890bb7c036cd0fd506c54f13b7fe3" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-02.webp", - "bytes": 439062, - "sha256": "22eb056a7e950861ee3b3fed7eb506647d7835924393c979c23181e7f91c9048" + "bytes": 412890, + "sha256": "61e13ba2ab5102c5595a01241ad5ff7b2fa2c7bdd05f79edf35cb9dfb2a73b76" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-03.webp", - "bytes": 452114, - "sha256": "522bb8c53aa6a2454b396964f4f8637ae393eae98003c157cc93456c6095addc" + "bytes": 411524, + "sha256": "408b1a0e8eb6275bdd82c6ae7dce22eea323efb999d1421863b06045ec04a08f" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-04.webp", - "bytes": 451202, - "sha256": "27c179a979a4b8c0d7402a42698a50605bf5479ac99f3ad8e1366041c603b4dc" + "bytes": 406696, + "sha256": "11ccb7a44360f2ca3a9948e924692e0b7f385889898631c7630fef4177d3ff18" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-05.webp", - "bytes": 448604, - "sha256": "f668c759c99e098f1bcff0fb32985ac6fa340bc941dc05c2026b6ba662485edd" + "bytes": 404816, + "sha256": "c6a4c2806a3b8b417f3216031b2abfe3896cf8b45737c69fc0349b0c08b12c2f" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-06.webp", - "bytes": 430164, - "sha256": "c29a5a41675ffe791c7e89c7afa2a90114a6d890a078e885b6a46dd68d818907" + "bytes": 399356, + "sha256": "3367a2f09691ab4e0255cf021b9f4945d3d8338a8224204fc8d5fa776e75eb7a" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-07.webp", - "bytes": 418342, - "sha256": "32c6eeb4db3266f2bd190cf3ff1a4edc1fd8e4e1979c4abc1988334fbf3de48b" + "bytes": 391658, + "sha256": "85920fff944bde9199fe5d1aec292aeeed3c4da64b5b4a251783834be42afe43" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-08.webp", - "bytes": 414240, - "sha256": "a79b397259caccbf5c621dc9b4ab62b55927d9fe3b5ab63b8a5bce77547da4bf" + "bytes": 382648, + "sha256": "ce67aa1203dc67104ad50b0077c654d20fd59bbe3cbc9e55be539b1d407cd138" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-09.webp", - "bytes": 405666, - "sha256": "480357664203f137e5ecdd4796824e04387c0cd5127ac0e4a67ce4a4cfe1a358" + "bytes": 376430, + "sha256": "a3e771b2267020fb84a00543b95180e1f4afa709d8959e772f149424a2b924d4" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-10.webp", - "bytes": 398826, - "sha256": "9ba4452895eae88426a4a5655d18304cca63014d57483f0786e01ba6f30447ca" + "bytes": 369358, + "sha256": "18d29d79d4bf8127743d29b48efd2fbf9d3d6617ec7924a4416d5af608673b5d" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-11.webp", - "bytes": 390352, - "sha256": "afbb9278799d26837ab2ff381d1f164c0eb4f8bcd8c4dce8da5461bceb8ca3f8" + "bytes": 363278, + "sha256": "64062fa2e85d95813bd78c1b54079baa001996820cc26f6ee46d324d427e08c2" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-12.webp", - "bytes": 379372, - "sha256": "45ef86f8b373cade0a1edd263ef01e74bf084c62b1707505342438eab2b5c1b1" + "bytes": 352964, + "sha256": "7bed33ea4dc31de3a3f6ea3570fbb97e03b29b3a3b5f9e60ac55b792663d2fff" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-13.webp", - "bytes": 371490, - "sha256": "8514d4412c74a0cc57b31e07b508d3d27226901716371eac21210941f41dbce8" + "bytes": 344702, + "sha256": "6f760bf4b5096238feefa633fd80902c4c34e08aa7cac8d2fca38b594708010d" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-14.webp", - "bytes": 361170, - "sha256": "2897bfca7797b29ea3c5c194a2ce58d48069d99893d88b879e0046469862fdf2" + "bytes": 332528, + "sha256": "ec7bdc71707f7056cbaed148f9d9dabca033298aec987e5486b33430cd21aee4" }, { "location": "public", "filename": "saturn-orbit-material-methane-ringless-stream-row-15.webp", - "bytes": 354802, - "sha256": "02742fe0b89f4718fdd2441345d24e65249453e519d1af2869ff5046322e86d4" + "bytes": 326556, + "sha256": "4eea83f4490854dd93f5e364bb1221ed8745ed013700182c96796510a5e4414f" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-00.webp", - "bytes": 684242, - "sha256": "0638ae973f51103ac84f2664000920f0ea7afa905b7bc1ef88d3f42f953db3ab" + "bytes": 663518, + "sha256": "f1bcf70391daf889d2fc7642d28a22c05d015f2270981c56ca7da45b7681a489" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-01.webp", - "bytes": 642970, - "sha256": "5b468968c5e6680468ac4b78527852252539e8c30d773380a9f7c9a639997ee4" + "bytes": 605160, + "sha256": "16edb698e0a8be01d1ea6e6952ce574fdddfe679ad18ae78916ac98737a2a29d" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-02.webp", - "bytes": 610942, - "sha256": "9adf7049c4a8f16adcc359d0832c531ae236e0b5a076d06b34e7d39eaa12e912" + "bytes": 567602, + "sha256": "753462fa7ee9155a5d3346ad62e723e14012e46f0ca6ce5168c8a8f4d7aaad2e" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-03.webp", - "bytes": 650712, - "sha256": "f959e80d1e9dca0c20f55263431398390a00a39b46d1225210144ccd45387d0b" + "bytes": 612218, + "sha256": "93afb2ae515938fae573addf565c2a27bb94433af33ac78f32b5ac4e7a088bea" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-04.webp", - "bytes": 660982, - "sha256": "965236cd637fa13edf822c1835e5b78dc9f9dc20033520d2927e7ed984523ebd" + "bytes": 627398, + "sha256": "d63f815ce5274e265fdcd34e1c70f590b9ef1c917bba7f686b98729aac679b5d" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-05.webp", - "bytes": 618762, - "sha256": "cab2058016e0080fd557b7dfacd2b64ae448e5955784e743cfa20ec1cab605d1" + "bytes": 591056, + "sha256": "c72fac4288e28c12c115e5fc0fc0a0f4860c4465bc761468d6f4de444f023c89" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-06.webp", - "bytes": 530532, - "sha256": "eb2e2fe7c3f2576c91d326857d2fd440dd7c53a3cc2e5cdf4018afa73575ec4f" + "bytes": 499144, + "sha256": "b6b7c2a5c6915113658931ff6d1c8f2635300fc1cc24995f4ef8ef1139419218" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-07.webp", - "bytes": 490648, - "sha256": "b66ab5cb3a7699e17636c8c09f2748a45cbf0f7e56af9973817b2811f2976fb2" + "bytes": 461880, + "sha256": "6bd62d16b8a82419236b0c3dc58fff84e2465457ef1c1c383478c67f8afa09d6" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-08.webp", - "bytes": 481530, - "sha256": "a4bae52c57122c03e6d20c084a20f5bf76c66139fabd1ce04716e80f70bfcb73" + "bytes": 451602, + "sha256": "cc62d9e3fb94a907c45b74a4d1539c80c1e628a1a3321cd7824c60eb10e050d8" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-09.webp", - "bytes": 473210, - "sha256": "d5beedce8adda4ad4e2a5807524d978be6dc1d9b17c08f673d48b62d5341d275" + "bytes": 444408, + "sha256": "3770214c8c7bfdbd40c577cbea064193ff8a35174b6c9f2d0e95988e606bf0b5" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-10.webp", - "bytes": 453176, - "sha256": "b63f86f29236afcdf12811d492a35d1f09766188a310ea20588b983d581feb75" + "bytes": 426764, + "sha256": "a1554d2f85b66d42e932721bb76d721eee808c3eec3d7d43900c4864cec4d6e3" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-11.webp", - "bytes": 429172, - "sha256": "f415b5a9c64f2bde513c52ca12a5a19200d4f3db4e55768355007660321d3459" + "bytes": 404298, + "sha256": "893553ec8d27fec634c196d9133191224678313c2f399b347ed69fda09425fa1" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-12.webp", - "bytes": 397108, - "sha256": "0eb90b3473c9f0cbb92afb836fd41e9d3bd05c25ee7e63996d8f4a48dba5548b" + "bytes": 370808, + "sha256": "5cafce63742d66ab7d26a547520b72df6cd61acce7a997aa513d8dd837264dbd" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-13.webp", - "bytes": 375378, - "sha256": "ce757a283ce8037758e15e5a2525efbbb060e0e14475f6760e07641e5ec9b6ea" + "bytes": 348474, + "sha256": "433cae3b884d3cb90f6d612819a09a6df99ba480f8f9c784ff5a51a9cd22f2dd" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-14.webp", - "bytes": 359208, - "sha256": "fbac8be58b3408094a0af09f9fa48852000833e4d250608e81d1f9680a5761e7" + "bytes": 332748, + "sha256": "fd54d387ec47a478c8e66f70ff106cd3507f05bd8da23446fdf9ddad5ec8fb56" }, { "location": "public", "filename": "saturn-orbit-material-methane-stream-row-15.webp", - "bytes": 353650, - "sha256": "5a6d0a781e31464095368ccf3070b9d6544891267676a6646c6e09a40f49e7e8" + "bytes": 326530, + "sha256": "e910cc46b9b50bd96ec5657e18e77d9955617280a94fafa8ee5cb57ee7bbf085" }, { "location": "public", "filename": "saturn-orbit-material-normal-no-shadows-shadowless.webp", - "bytes": 17912, - "sha256": "5cbd37a74458c355ad3a28cff751dcdbf092020aa890a6f00e17b7345043602f" + "bytes": 15610, + "sha256": "2b1fcda994256bc86ad7ac5b320deae477e5b260d84fe090be1fa7d0b1bb6c14" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-no-shadows-shadowless.webp", - "bytes": 17742, - "sha256": "02b19530764e24ebafa06ae7d4052ae23bec2dd678c517ed26994c90bf84fc7a" + "bytes": 15420, + "sha256": "7e3026696ec589f0b58f187ea4a98671a859c4b7430fad7ea12f84f6c6c8a11f" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-00.webp", - "bytes": 575540, - "sha256": "f6f3e7b9a36e79892a40ca75ca80da04fe8a53675ed944d08846c488ce28fd2e" + "bytes": 522428, + "sha256": "ca6892ddc43ea4dc5dbbb18793618167d4137c05305c8fce7c92c19d7bfeca8a" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-01.webp", - "bytes": 575832, - "sha256": "60815169ea4299fc554122c36e89e28176ca9156d6cc9d7a357779ca0f1093c8" + "bytes": 520518, + "sha256": "21030a4e0b7bded8342ea2d8109b02472742ba521ae8bb6b54dac36deb66ab9a" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-02.webp", - "bytes": 573692, - "sha256": "cf845d6e3518b7d52f27b2d461da1c30ecfdb7adf5d880dff519a689fbb923e5" + "bytes": 509802, + "sha256": "ef1764e3764040e2d8118dd9915dd72239e84a057f4f7ba2a7e978d7531b1c10" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-03.webp", - "bytes": 566028, - "sha256": "bd5cff5edcb245a321fb81dd7b48dcce00277c1901b65ffd8d32ef708a727a4d" + "bytes": 504702, + "sha256": "5f94f87058f0fae166e115c7cb5f20df47967e04ac3f71ba684d1a1cf42f3900" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-04.webp", - "bytes": 553918, - "sha256": "473e4ae54d06407415b56fdc60f8707c8a788c8dcd45754eac54dfa690dccde1" + "bytes": 492488, + "sha256": "287ce3cb4f6b0d4e82c18035f8d3e2ba3cdcb887fb202d05f78269e3f1d7d13a" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-05.webp", - "bytes": 540688, - "sha256": "875b99e8d7d328486189d63299f5c5e93c95374aeec971f6bd8d023258e93936" + "bytes": 484332, + "sha256": "c1d4555cb50245187b9cebecc72b31ead3abd3398cb20cbb16cc3f0538435248" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-06.webp", - "bytes": 524804, - "sha256": "f05371fff0d8105ae1f6709d2a53a579cafcdcd2b99922e8b0dcedd31fc9292c" + "bytes": 468960, + "sha256": "b001b09e8c2eee6931fe7365943bfb71c7ef08fce4d900e6305abf61aa8b82b9" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-07.webp", - "bytes": 509746, - "sha256": "57c1a5cd6aebf669909428733845b873888fd8f06ae940f05316e0f09f0e1dcc" + "bytes": 457356, + "sha256": "e191b5cc3628e0191281f06c6df8e3b371c500d69ffe0eff49a2f0c713448dfa" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-08.webp", - "bytes": 496076, - "sha256": "7d74f94d378af765cd9611fa812c44d0b089d3be5bf3fcc89842514ac974dc29" + "bytes": 441614, + "sha256": "df1069f7e5a02b4badf187714a13685420a62e2ddb195e88051283c9bd4934e5" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-09.webp", - "bytes": 482934, - "sha256": "8c2d3e2e70535628d4390e87c26a0243020cf2be4301a4ece7313629df3169ce" + "bytes": 429826, + "sha256": "01cdb0902ce329e38c67a1ceaa21b30400c7192df51c2a3911d4bcd19452fc0e" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-10.webp", - "bytes": 468842, - "sha256": "fe5eccb5de895f3fe5a86798bdd11378e8c673f758a99689a0742c7cbc54dba5" + "bytes": 418138, + "sha256": "7e716e94cb98d8a77edbba42444a02a4589449928e4ae11d986789d3eb300bdd" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-11.webp", - "bytes": 459104, - "sha256": "5d4eef659081e9e87950cd4d0a0d15f5c293d5c56606bfc99e3c2633a1b54d84" + "bytes": 410220, + "sha256": "205b080ab51bf0b648340252ebef2698d3d5a169ce5532adfd413c2839dc562d" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-12.webp", - "bytes": 453796, - "sha256": "dfbe2527b88ee488bfbb76b791ae36845a26a0149affa5808bb471aed3c5722b" + "bytes": 402496, + "sha256": "fe1958cc4f3b07e8ab1eb9e9266c43ccd0459b8ba050e0af704680fcd744abc1" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-13.webp", - "bytes": 446070, - "sha256": "fae066696d94542aefbfda27bc646a61aa5f0289d6f14bae8504ff113e2d5bdb" + "bytes": 393744, + "sha256": "260510148d75cea813197232169be18aac01f3ec33e2e8c8561aeafb98267b9f" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-14.webp", - "bytes": 435036, - "sha256": "41bb2a67f040a633a19f455e98396d5b7614871cbf88830fb0416e698be3df5d" + "bytes": 383114, + "sha256": "e2aadcef97ea148e86157df129d971b716f7020832d5e2d7557ab01af32d9d10" }, { "location": "public", "filename": "saturn-orbit-material-normal-ringless-stream-row-15.webp", - "bytes": 427718, - "sha256": "aa3e8d2697f508842e243c72e8b97dce21868dd6ad823b7bc338f01808f2334b" + "bytes": 375372, + "sha256": "b1c502a945f6be12f119195ad858999ee3299dab96d863b5b5040c265d58729e" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-00.webp", - "bytes": 814498, - "sha256": "d744d440695738b8fb1d3505c9eaa9ac3e7175202ac020f4de88bcd1543f9fad" + "bytes": 740406, + "sha256": "04bd94e1e74fc93d2c49c3d94887ef2c642216c30b5a3d837b10091c3177a307" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-01.webp", - "bytes": 775022, - "sha256": "25c1b934e009b5877432731ed8e060d97019c2ed5cf62e9149b394ed0c2494af" + "bytes": 696660, + "sha256": "530b47133c304578afa09003daf916702fffad963474c8eda8c9e392189309d4" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-02.webp", - "bytes": 761066, - "sha256": "cc2fdc505be3edd350971d645fd9e0662d8bffb1bb4dbd211b1b5f1c9c620010" + "bytes": 669638, + "sha256": "216f476b3cda927098396981f058d7f253c76e785bf113017fb7a00bb0f4d8c2" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-03.webp", - "bytes": 805614, - "sha256": "08681e884c392732130bfafa111ca74ffb6702c7b30cd81b99fa560e54b581a0" + "bytes": 720058, + "sha256": "cc86ea4e6c5e5f97cb32fb6770ceedc490aa84781533fc90f13ce874f587e5a0" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-04.webp", - "bytes": 814846, - "sha256": "d9e1e798af89ccd4ca8de95cebac2748fca89fcf4c3640bf62d071e5b3820e8d" + "bytes": 728884, + "sha256": "00622134a6c4c26d116927dfec47c8693b78164c50b1c2396d3ca17ee5baba34" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-05.webp", - "bytes": 739054, - "sha256": "91e55ea67de7f9b80fe477bcb9e2f8d8d9decbfdfb3dbf111194498391f796ac" + "bytes": 679090, + "sha256": "b51db6d4f447c6a275eecc29079e11471106130e3e1611c171bd64992899a85a" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-06.webp", - "bytes": 649726, - "sha256": "1c2a231924e015d4a14f53c80dd579e5ac1f88dc18e5881ac9599842f132b939" + "bytes": 584834, + "sha256": "29497f6d10b210cfd828f90c8e233fdd66b21dcd14fae54370adaa90b0538fcb" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-07.webp", - "bytes": 606556, - "sha256": "0c689be8597ff5f86e72b5cf4f075f59a9702000c25eefa265c63b041f2517bf" + "bytes": 546286, + "sha256": "dcd04442101c0de6786393418bd632f801c7bc86bcf721e02e26c69f48e6c4b0" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-08.webp", - "bytes": 575336, - "sha256": "461b02a6648524ba287b05487b2fb6abac432d7807d4b1c0aaea44bee34f08fe" + "bytes": 520776, + "sha256": "031d63d7d8b97a9bdf86bbe126e320666d9af780edb2ee7f181bea481d0519a4" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-09.webp", - "bytes": 556394, - "sha256": "8c6405fa063e91b039cfb5b5d25825449310ddd230dc9fdaa1a4a7cb9d0d33d5" + "bytes": 501420, + "sha256": "9bfcdab916845dddec8c5b6798ae1d5183fe3a7eb060269a8b7b5a3574316c2c" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-10.webp", - "bytes": 530640, - "sha256": "114f42f5f1e292b15467874748804b2ea3c4a9aa89c2adef5b2873568dbe3dc2" + "bytes": 478852, + "sha256": "95f3bd06aea5bf6b58c8652296d4bbbde6c00143bc43a473b116bdcd023340ac" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-11.webp", - "bytes": 502482, - "sha256": "e75e47db0f040d82f966feed626e2c1f077fa56e6961df166393452fc75d73b0" + "bytes": 455058, + "sha256": "8ea76b8afef580ad5f4422f88ba82ffd13c1c0f7048e1413644d606ee52105d1" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-12.webp", - "bytes": 469484, - "sha256": "b2d21253c7ca5115370f4f5255ca94f5462d72803f69a6ff1d45e1b9bd30d6ed" + "bytes": 422716, + "sha256": "5944e745db168e334622a3c29e9c4decda6906728ad6456c21c4cd145e9216c1" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-13.webp", - "bytes": 449516, - "sha256": "ca76edd3cd6cdd995218f23da4ba5996739e49781573eaee0ba7e4f2889e5b20" + "bytes": 398396, + "sha256": "114ae112e12a2f4ff8788b86b99e5037dc519623ea58695ca180d890f989305a" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-14.webp", - "bytes": 427964, - "sha256": "aaff5a295dbdf5e884072a02514cf50120ed8d0239d3aadfdfc9c4e6a96da810" + "bytes": 380300, + "sha256": "dd8db1175f01a4b275b28c4c3daee513edfd16f641745263f6b7e266b03c3f2f" }, { "location": "public", "filename": "saturn-orbit-material-stream-row-15.webp", - "bytes": 419666, - "sha256": "6652a8b5be16ad57d09c7c9c6a017a1e9aa77c79f7944755714c33e65825b737" + "bytes": 369716, + "sha256": "9ddd85a09ab52963105de08e7d2b012daede8388ecc31babf92247984efbb498" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-no-shadows-shadowless.webp", - "bytes": 15126, - "sha256": "e68da98d0b468078d2c7d7c3f44aa9ef666dd24b14b8e9b09d3d6fecc0b70c59" + "bytes": 13942, + "sha256": "3968ba70f96b911f93c63e6a8fb826bcd609253d2c15122751c278e92e4c45ac" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-no-shadows-shadowless.webp", - "bytes": 15388, - "sha256": "61f6891f9be29079757fa10ae5acc1e00e47ce367f856da530d804b1938ed7df" + "bytes": 13786, + "sha256": "79c4f2110ec1c3906b9c40cefe2349a30b5bc916550f89fb23581edaf1e1166c" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-00.webp", - "bytes": 417014, - "sha256": "81bf753367e374b5e1c9ab281952649e0039e062e689e95dede8d256de420a3c" + "bytes": 390858, + "sha256": "cd2ee5317255f7579e8592fe41636d44887e5e422873d10fe1a0d094a9df3eae" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-01.webp", - "bytes": 415528, - "sha256": "f820653b75cf3dd30f55c9389be67b12eb1394eecdb92245bf47c4e81575f4a9" + "bytes": 387924, + "sha256": "ecd794c04d81a890a222ee79f3ead5d81133cc7a64bdecbf99be5a46b1e242f2" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-02.webp", - "bytes": 412008, - "sha256": "1c05ce21f76d294ad762c5bbaa79caeab34dc7cebaa81b4a797108db5e3b30db" + "bytes": 384154, + "sha256": "d504b79a26deb3709e1baa5273b95d6e78bbde32625cb4fde5a94973c6e9a4f6" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-03.webp", - "bytes": 408878, - "sha256": "4aad3edfc7ea17baab16c77f94be9dff67a11bfd58c95ac7a8dc642f6914138f" + "bytes": 384806, + "sha256": "ae000840287c81243d889cb1c28358dbe9b0cd547e804c9fd80cd1c9cefd028c" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-04.webp", - "bytes": 419744, - "sha256": "d4789537a89d8f8120125aadac160154e40bac6653d48a50074a1d4ce5d67252" + "bytes": 382418, + "sha256": "57c2de8065f3c7aa59d2f5b68694f0a328019f628bcaf1f00701db1b93cea1a3" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-05.webp", - "bytes": 406980, - "sha256": "206263fdbeaa92c24918022951a4f4d3425e4ad137b4df3d46b62f482e5e9cb4" + "bytes": 388018, + "sha256": "bcc4b1aa088f51613aed2eab8349b994284755efb523ccf9eadbb2359b4992b2" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-06.webp", - "bytes": 409418, - "sha256": "0bc4690bb8fd1f8b72ebebd6fbb015f9419517639e9ff236922ea3a8ff85fb80" + "bytes": 389390, + "sha256": "6052eaf09541f4962cec3c252ddcf3997c31664b25fb4049738ecea3f7d744fe" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-07.webp", - "bytes": 403344, - "sha256": "1761017d59f00def89f9ccde0c65d136f1d5b89133d506ed98af9e4894331768" + "bytes": 385284, + "sha256": "27c6a91768ce382592cd199b8680a60a039992c75575d51503c3f2c8cb57e6a5" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-08.webp", - "bytes": 394688, - "sha256": "963e7b0cda4ea793e095245109bf8555366f6ef724dd1090ece5f442111dabe5" + "bytes": 376586, + "sha256": "7475dfefbddf489c5a10149b168f291b58f69db5eb5111505dede0051fd857a9" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-09.webp", - "bytes": 387008, - "sha256": "1a36bca4ef8ab2a99583013fa9400e7be4cc097b359923fbbd620c7f7af3f8bd" + "bytes": 368402, + "sha256": "c9d4383ecd7b24bfa1e8be08de7e90ba7504417e5cd446eb6c04a7e062129106" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-10.webp", - "bytes": 377514, - "sha256": "661f01e5035f8ff34d8edf75dc6a008018cb9538a3e95e24bf432ddf355832cf" + "bytes": 361570, + "sha256": "58ffe4df044b0a38354ef4fcec7f9f89adcb0eb2329e0498bd82e6767bf1dce1" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-11.webp", - "bytes": 369224, - "sha256": "d59b4ac756fe5f310491391ac8df2ff3257880ce4f8aa8600e9b7318e2fc356c" + "bytes": 355926, + "sha256": "8894c0da848e7a11b6fdc464bc5280b9ded248b1c0800a03a06c3debba348cb3" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-12.webp", - "bytes": 362236, - "sha256": "2fe289ddd1b714df767623a1001d2bb42e05e515e10067dded0986831613ff29" + "bytes": 349532, + "sha256": "97a2531a0806d4cfb76b2dd1746b8e88db71006d138bcedba80ffcc70f051e68" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-13.webp", - "bytes": 354388, - "sha256": "6b91d9de9aa277f2ba83ee23e248b2f651bafe95d5422cca63bb7009bd849913" + "bytes": 342540, + "sha256": "ab40248bda9f945561d63435b0d2791a942f923a7d5cf7ecc8a83ff655d54b7e" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-14.webp", - "bytes": 344552, - "sha256": "a5f0582cafe9fe21819b79a543ef2670fdf1254e9d8ca197357562bc5de7de49" + "bytes": 330782, + "sha256": "0e5b2099bdaa4082e785fdc3041683f77a2ebac16dafe47420f091c17d9ea595" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-ringless-stream-row-15.webp", - "bytes": 337600, - "sha256": "292c5d37bf0b4c23c009ac226840fcc933ca4092ed1cb363fa2369da6b0849d8" + "bytes": 323592, + "sha256": "9a610663301d286a9e0e227142b3b71d8316fac6ede51c8c3af97316a3004515" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-00.webp", - "bytes": 649260, - "sha256": "242aa891753e940db5fa491e44c7b41e41e654863d76e77bda6a29e136b16fc7" + "bytes": 630546, + "sha256": "0e45a72b54f9177cc1df9f7c18de867bb9af3046a94a7b4283c06ba9bda146ce" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-01.webp", - "bytes": 610350, - "sha256": "2769d8de3eb21129815f5bb978bf5c00367852b112a19d53b3feb7a2afe2ade6" + "bytes": 573366, + "sha256": "4a8dfd6522fbce94e034ac1c2eda3246b4ce9e50e0e25a2f98a78f5f534462ab" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-02.webp", - "bytes": 579796, - "sha256": "4923539e58462d489ab03c87d56c4251f9cda68ae3a60955c22904a0eefc59f6" + "bytes": 534600, + "sha256": "b70a5cb1d81cbf5f1461618307eb694e86d0eb8ec0902e8a232b7f455a860811" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-03.webp", - "bytes": 616200, - "sha256": "2e9207ce5650cc19d3229702c7e896e3a5cbcdf867160dfacb0de94ae33fafb3" + "bytes": 580536, + "sha256": "dc1dd1dad457647a05f8349ef77ac3c3526b57584eeb3a4fcc94e62f5ee2170a" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-04.webp", - "bytes": 620474, - "sha256": "5097265166b059113c6709cc1b231a27ee22df376ecc77a8a8fce6677a1d005c" + "bytes": 599086, + "sha256": "91726ef37350e737c5ca664956ffefc9e8ad9e81d6b5a17ab502abd6ab163b20" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-05.webp", - "bytes": 583942, - "sha256": "3a41e8632752a4110e1b128cd0471a881d3a3f2e83fe2ccf0cc4402c433b056c" + "bytes": 570288, + "sha256": "bad66efa73cfae940be2acdd29ebc61d07ccb8d2701cf81bcdb48d9601a17906" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-06.webp", - "bytes": 504206, - "sha256": "1c80922dc160ec0e66740a66e6d0ceffc98aaeb3d21b888de96d402789a0e0f6" + "bytes": 492530, + "sha256": "0b0080b8ddf773d2c2cf1b995e6a7f9a13c714fc45f1ac98eb8ace1399d3d797" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-07.webp", - "bytes": 471502, - "sha256": "d11cb91e5a9a7a7a4d7b0b5fb313ac608131cdc6937f63289dcd673bf487c64a" + "bytes": 454332, + "sha256": "466deb535f02ebe4826a73d38f0fdbbd852964c96f094cc936323b210678ce71" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-08.webp", - "bytes": 462490, - "sha256": "f5639ca1288c5195bc22affc340d94c15e086bc0b34e08b0879bf65c6c71abac" + "bytes": 442870, + "sha256": "b27d325992b08caf65a63fc558ba42aecdde0bba27a509b2295d37cf6477f299" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-09.webp", - "bytes": 455742, - "sha256": "5266abffc6c252262ebb1dd8cabd2e853e41dc184ba9f4be985e42d97f3a712b" + "bytes": 435632, + "sha256": "346afbf4823368fa3b98caa093f6794f923e2fe7d10385595c677cf4a1fe4c79" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-10.webp", - "bytes": 436758, - "sha256": "983e40d83159c3068eede82ae623e023bd88f0e415aa7ddf75f81fc233d6ad4f" + "bytes": 421056, + "sha256": "78a665cefbc671039d9d3ea10fbafa88d690f86a7eae596b9b83d95f1c734f0c" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-11.webp", - "bytes": 411414, - "sha256": "03047c27fe260f86e9521e58254854a265de0094f0a62f9432629feda5620afe" + "bytes": 399226, + "sha256": "3c06fee7b5944bcf615fb671bc87c5fd1674116a983ea5d91591cff23559374d" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-12.webp", - "bytes": 382070, - "sha256": "108cf822c981582610a740112e7ae99bf92e1021c4f042f5a7ac18a464ab6545" + "bytes": 368354, + "sha256": "4eb258ada59187253de4f8fec0eb3c77b1922e27491ef374fd1ba9ed51ee78ee" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-13.webp", - "bytes": 359584, - "sha256": "f00959e60419b4838fb0550b22dbad6c88f383636559335c09a0bb5738eacf95" + "bytes": 346118, + "sha256": "2047dec3f60b47ca0f3d1e118f110d3043605fab11cb84206cc88140e681eb18" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-14.webp", - "bytes": 343574, - "sha256": "c5638c7370871514a47fa74c6b0969acc3c242ea40de2e2fee780165efd3708f" + "bytes": 330722, + "sha256": "1d5b2c3f92d6f349e8004739fcedd23f77113e5c1f9348d5709a14dd1dc8b04f" }, { "location": "public", "filename": "saturn-orbit-material-ultraviolet-stream-row-15.webp", - "bytes": 337728, - "sha256": "11dcd14629467f4bf8511b5fd329e8b77cabb6a1648cbcd29f017dc81bdb2af6" + "bytes": 326594, + "sha256": "56042ab306d4c11767c13a5e5e2830756d08e110f7c0f651f887b935bc010b18" }, { "location": "public", @@ -727,17 +733,23 @@ "bytes": 5288, "sha256": "ae375119c70ef2c0017fb8e3c3291390bfd5cf5737df09b9f99eb773dfc63c6e" }, + { + "location": "public", + "filename": "saturn-poles-cassini-2011.webp", + "bytes": 54038, + "sha256": "46db446f7ff9837409d80a1e50671007b147e3670e034eb4f90fa17fed5aba19" + }, { "location": "public", "filename": "saturn-poles-methane.webp", - "bytes": 38466, - "sha256": "47a2cb3ad9fb093e549baf9d0923651122781512c5ff2b6170eed37edf86f71d" + "bytes": 39636, + "sha256": "b3c604ab2f82a29fb878e442f735d5a3d9240b367d9f40888ad54648744c71ec" }, { "location": "public", "filename": "saturn-poles-ultraviolet.webp", - "bytes": 38698, - "sha256": "a11560fcfeec2677d92401dad52199fe8c96d42f84833384ee252f8cfeda078e" + "bytes": 38472, + "sha256": "15767339d51c41474cd8172b768db53c6876e4443a290c0728d8bdda9f899478" }, { "location": "public", @@ -790,8 +802,8 @@ { "location": "public", "filename": "saturn-poles.webp", - "bytes": 55912, - "sha256": "1cf1726407a6a76be2c0fcaa5a94f0a8512de20ebbb5ea3c2caddf1f750da0df" + "bytes": 55736, + "sha256": "b2517af310ffb2fd9cd511225e61ab98015911c0b5175fd3721c02b17cea0276" }, { "location": "public", @@ -820,8 +832,14 @@ { "location": "public", "filename": "saturn-surface-body.jpg", - "bytes": 725768, - "sha256": "fabc0b970bafe693376cc36126144aa0d90e07029b40d1ab638df3cc13d64660" + "bytes": 722961, + "sha256": "c84b6b2f03d34927a18b52ec7d276cb9018b81e0d5e525389ec983044edd0bd2" + }, + { + "location": "public", + "filename": "saturn-surface-cassini-2011@2x.webp", + "bytes": 614528, + "sha256": "6fa9cf5e2c7abdb6e0790d3087b87593c40fa3e12d7d660b3a4a9fd0c4a5b78b" }, { "location": "public", @@ -910,14 +928,14 @@ { "location": "prepared", "filename": "controls.json", - "bytes": 4767, - "sha256": "b29fcb34227b57185043b15177a96abdcbb065147ed03246a46effc952fbc788" + "bytes": 4891, + "sha256": "fa6ab54610332f559f159aafa9567235c64315d8536fe9f33a5d291e0beec0aa" }, { "location": "prepared", "filename": "datasets.json", - "bytes": 10231, - "sha256": "1fc145a4211198dcbe691e9bd4f536348b63d1abe9e2cbff7d24f40c237a41c2" + "bytes": 10926, + "sha256": "6c7a77331933f09e0edc27ac652ef1df300e947bb6323c4b367f3c257396acc3" }, { "location": "prepared", @@ -934,32 +952,38 @@ { "location": "prepared", "filename": "material-datasets.json", - "bytes": 8530, - "sha256": "7d1946bcd4d0e6a4373372743f0106bd4208f3806a0cdc4a6f7d937bbc0dff43" + "bytes": 9206, + "sha256": "de66a099b1a334a21b9cc1d46fd46b51c7a6e00ecc1b96969924dc480f93a8ac" }, { "location": "prepared", "filename": "minimaps.json", - "bytes": 1247, - "sha256": "0ae1a17183d6f4f727dbb6e63b7d26e3448586503c9d797444a691e91286860d" + "bytes": 1359, + "sha256": "b89f78bb76e4fa1489d742e314fe973ad509faf7fb9c1c5db7c46b208f938473" + }, + { + "location": "prepared", + "filename": "minimaps/cassini-2011.webp", + "bytes": 5554, + "sha256": "a1f7a67e9930234deabbd404cffa6b639d46dbbae89eb815e604ab91b1bd3f19" }, { "location": "prepared", "filename": "minimaps/methane.webp", - "bytes": 2662, - "sha256": "c97a1d9818fa625e863732b0fc5bda93c3a3353bded7cf348e845b4800e72a5e" + "bytes": 2638, + "sha256": "31c041d6444e4304dba73a2a99334b88798bf8ff27356e9efa709bd015a9cd19" }, { "location": "prepared", "filename": "minimaps/normal.webp", - "bytes": 3820, - "sha256": "7f7e5fc0006249e635aa20c876d376daef5df4383546166a7a1e87644145d429" + "bytes": 3572, + "sha256": "ec5c87fd3c9060ed9120463ff606782ce3b88c4fd066502dfce8aa6068f2a7cb" }, { "location": "prepared", "filename": "minimaps/ultraviolet.webp", - "bytes": 2392, - "sha256": "4282621d4db371beb90065df8eb1c0bd1d5feab2609623dac6c4bb26faf12b63" + "bytes": 2352, + "sha256": "b7c629ef603eef9d0c4fe6e29dcc56cc2638575d363a65e1e0f460a7e53e39d5" }, { "location": "prepared", @@ -1012,14 +1036,14 @@ { "location": "prepared", "filename": "runtime.json", - "bytes": 564010, - "sha256": "74fc47af2453858f87283791d61554547553aea4b77488a80fa8a1ae38c3e6dc" + "bytes": 569277, + "sha256": "523c0855db40fae828b47c47e561b57d766d8cc0e2993536d61906293c287613" }, { "location": "prepared", "filename": "scene.json", "bytes": 1468863, - "sha256": "73188fca6ca53f2a705bc48fe1f4e2bcd3977e13e53867521e365e8e4f428e56" + "sha256": "c13dc933f496cdbd884cf784c8ea780b2aa7a633312e7f18387f375c431c6597" }, { "location": "prepared", @@ -1036,14 +1060,14 @@ { "location": "prepared", "filename": "text.json", - "bytes": 4178, - "sha256": "3b8d7d0f211d95871137a1a110edcc550001932eb866f5084b663fa9520ccd93" + "bytes": 4649, + "sha256": "48cb1949d31dda06fd68a1c31cff8eee0770be08a425e0c9f99d955a7dc7e0dd" }, { "location": "prepared", "filename": "views.json", "bytes": 8408, - "sha256": "b63848a6270c7207339d63b62eccbb708ef37af91977612a225ffb8a107d7454" + "sha256": "a323ba014e45e7199edccb58d7ec8f9a4639969625575ea694f55d4a578caac0" }, { "location": "prepared", diff --git a/src/objects/saturn/investigations.json b/src/objects/saturn/investigations.json index c73ab502ae0..f446e208a2f 100644 --- a/src/objects/saturn/investigations.json +++ b/src/objects/saturn/investigations.json @@ -12,6 +12,16 @@ "https://archive.stsci.edu/hlsp/opal/opal-saturn-cycle-32" ] }, + { + "id": "double-size-visible-map", + "subject": "The visible body image at 4,096 by 2,048", + "status": "excluded", + "finding": "The body image is 2,048 by 1,024 texels from a 1,800 by 900 source; Jupiter's source is 3,600 by 1,800. At the default view on a 2x screen one Saturn source pixel covers 1.8 screen pixels (disc radius 511 px), one of Jupiter's 0.9. A 4,096 by 2,048 image prepared from the same source held 4 % more fine detail than the shipped size in a composed flood-lit default view (mean absolute Laplacian 1.188 against 1.140), so the larger image was not shipped: the softness is the source's.", + "revisitWhen": "A sharper complete source map exists.", + "evidence": [ + "https://archive.stsci.edu/hlsp/opal/opal-saturn-cycle-32" + ] + }, { "id": "opal-unobserved-rows", "subject": "Unobserved rows in the OPAL visible map", @@ -33,6 +43,29 @@ "https://science.nasa.gov/photojournal/saturns-hexagon-as-summer-solstice-approaches/" ] }, + { + "id": "cassini-iss-2011-color-map", + "subject": "Cassini ISS contrast-enhanced global color map of 2011-08-11", + "status": "included", + "finding": "A dataset beside the Hubble dates: the PDS bundle's contrast-enhanced RED/GRN/BL1 map (3,601 by 1,801 samples, 0.1 degrees, planetocentric latitude, west longitude; frames at 161 km per pixel). Its three FITS planes hold 8-bit display values. The first stored row is 90 N although the LAT_C card reads -90:0.1:90; stored column c is west longitude 0.1 c degrees although the LON_W card reads 360.0:-0.1:0.0 (raw frame W1691726068_1 correlates 0.62 with the prepared storm band, 0.23 with its mirror); the first stored column is zero on 1,595 of 1,801 rows and is cropped. Rows 0-110, 898-906, 954-1071 and 1721-1800 are zero (17.7 % of the map) and draw the gray grid; rows 919-953 are dimmed by the rings' shadow and shown as archived. No color change is applied.", + "evidence": [ + "https://doi.org/10.17189/rkkb-6y30", + "https://doi.org/10.1038/s41597-025-04392-3", + "https://opus.pds-rings.seti.org/opus/#/opusid=co-iss-w1691726068", + "https://science.nasa.gov/resource/great-disturbances-2/" + ] + }, + { + "id": "cassini-2011-default-map", + "subject": "Cassini's 2011 color map as the default visible body", + "status": "excluded", + "finding": "Measured against the Hubble 2025 map: the fine detail's correlation length along longitude is 0.14 to 0.18 degrees between 20 and 60 N against Hubble's 0.23 to 0.33, and a composed default view at the app's on-screen size held 1.27 times Hubble's fine detail (3.9 times in the contrast-enhanced product). But 17.7 % of the map is blank, and with the shadow-dimmed rows about 21 degrees of latitude around the equator would be an interpolated fill; the values are 8-bit (40 to 57 levels per color in the archive's unenhanced product) and from 2011. The user kept Hubble as the default and took the Cassini map as a dataset, 2026-10-05.", + "revisitWhen": "A calibrated global color map without the ring-shadow band is released.", + "evidence": [ + "https://doi.org/10.17189/rkkb-6y30", + "https://doi.org/10.1038/s41597-025-04392-3" + ] + }, { "id": "uvis-alpha-virginis-occultation", "subject": "Cassini UVIS HSP alpha Virginis occultation, 2006 day 285", @@ -218,18 +251,17 @@ { "id": "dated-test-run", "subject": "Dated test or browser run for this record", - "status": "unresolved", - "finding": "The evidence section states that the record describes source cross-checks and retains a fixed-material comparison image but cites no dated test or browser run, and that the comparison image is not an input to the runtime view.", - "revisitWhen": "A dated test or browser run for this Saturn version is recorded in the package.", + "status": "included", + "finding": "The README's evidence section records a browser run of 2026-10-05: the default view in headless Chrome with a GPU at 2x pixel density, on css.earth v0.6524 and on the version that changed the limb, measured on the same rays, with the before-and-after image it displays.", "evidence": [ - "https://github.com/layoutit/css.earth/blob/main/src/objects/saturn/README.md#L19" + "https://github.com/layoutit/css.earth/blob/main/src/objects/saturn/README.md#evidence" ] }, { "id": "limb-law", "subject": "Photometric law for the lighting overlays", "status": "included", - "finding": "Hubble OPAL Minnaert coefficients of the color map per channel (red F631N 0.80, green F502N 0.65, blue F467M 0.86), the correction OPAL removed before projection, put back relative to the flood-lit disc centre. Blue took F395N's 0.40 until 2026-09-25; see opal-f467m-blue-limb.", + "finding": "Hubble OPAL's Minnaert coefficients, the correction OPAL removed before projection, put back relative to the flood-lit disc centre, one law per display channel: red 0.798, green 0.698, blue 0.728. Each is the README's coefficients weighted by the light the channel shows; see display-channel-limb-law. Each channel took its nearest filter (0.80, 0.65, 0.86) from 2026-09-25 to 2026-10-05, and blue took F395N's 0.40 before that; see opal-f467m-blue-limb.", "evidence": [ "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt" ] @@ -256,7 +288,7 @@ "id": "karkoschka-whole-disc-color", "subject": "Color tie of the OPAL visible map to Karkoschka's 1995 whole-disc spectrum", "status": "included", - "finding": "The OPAL color TIF is arbitrarily scaled per channel and rendered 1.4 times too blue (map mean blue/red 0.65 in linear light). Karkoschka's full-disc albedo of Saturn (phase 5.7 degrees, rings edge-on, PDS 1995LOW.TAB) times a 5,772 K Planck spectrum through the CIE 1931 observer gives linear sRGB 0.6168, 0.4713, 0.2982 (#ceb794), green/red 0.7640 and blue/red 0.4834. Divided by the limb law's disc means (0.7694, 0.8698, 0.7354) the map targets are green/red 0.6759 and blue/red 0.5057; the solar-tinted map measured 0.9272 and 0.6953 (cosine-weighted, linear), so the gains are red 1, green 0.729, blue 0.7273. The swatch's independent Payne et al. (2026) spectrum agrees under a D65 white: green/red 0.863 and blue/red 0.589 against Karkoschka's 0.863 and 0.593.", + "finding": "The OPAL color TIF is arbitrarily scaled per channel and rendered 1.4 times too blue (map mean blue/red 0.65 in linear light). Karkoschka's full-disc albedo of Saturn (phase 5.7 degrees, rings edge-on, PDS 1995LOW.TAB) times a 5,772 K Planck spectrum through the CIE 1931 observer gives linear sRGB 0.6168, 0.4713, 0.2982 (#ceb794), green/red 0.7640 and blue/red 0.4834. Divided by the limb law's disc means (0.7706, 0.8350, 0.8146) the map targets are green/red 0.7052 and blue/red 0.4573; the solar-tinted map measured 0.9317 and 0.6975 (cosine-weighted, linear), so the gains are red 1, green 0.7569, blue 0.6556. With the nearest-filter law of 2026-09-25 the targets were 0.6759 and 0.5057. The swatch's independent Payne et al. (2026) spectrum agrees under a D65 white: green/red 0.863 and blue/red 0.589 against Karkoschka's 0.863 and 0.593.", "evidence": [ "https://pds-atmospheres.nmsu.edu/PDS/data/gbat_0001/data/1995low.lbl", "https://doi.org/10.1006/icar.1998.5913", @@ -277,18 +309,39 @@ }, { "id": "opal-f467m-blue-limb", - "subject": "Minnaert coefficient for the visible map's blue channel", - "status": "included", - "finding": "The blue channel uses F467M's k 0.86 from the OPAL Cycle 32 README table (\"F467M 0.86 .00507\"), the README filter closest to the sRGB blue primary. F395N's 0.40 is violet light that carries 13 % of the true-color blue channel in Saturn's sunlit spectrum; with it the limb brightened in blue and turned gray-blue (b* -16 at the rim, 0.95-0.99 of the radius) in the app. The map's blue data are still F395N.", + "subject": "One README filter per display channel: F631N, F502N and F467M", + "status": "excluded", + "finding": "From 2026-09-25 to 2026-10-05 each display channel took the README filter nearest its primary: red F631N 0.80, green F502N 0.65, blue F467M 0.86 (\"F467M 0.86 .00507\"; before that blue took F395N's 0.40 and the limb turned gray-blue, b* -16 at the rim). Green then darkened least and the limb turned olive: on css.earth v0.6524 the default view's green/red ratio rose from 0.78, 200 pixels inside the edge, to 1.02, 5 pixels inside, where Jupiter's goes from 0.87 to 0.91. No single filter is a channel's law; see display-channel-limb-law.", "evidence": [ "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt" + ], + "revisitWhen": "Nothing: a channel's law is weighted over the wavelengths it shows." + }, + { + "id": "display-channel-limb-law", + "subject": "Limb law of each display channel, weighted over the README's filters", + "status": "included", + "finding": "The README gives one Minnaert coefficient per filter (F395N 0.40, F467M 0.86, F502N 0.65, F631N 0.80, F763M 0.85); a display channel shows a range of wavelengths. Each channel's flood-lit profile is the sum of mu^(2k - 1) over 380-780 nm, k interpolated in wavelength between filters and held outside them (a join that is ours), weighted by Saturn's albedo (1995LOW.TAB) times a 5,772 K Planck spectrum through the CIE 1931 observer into linear sRGB; the same spectrum reproduces the whole-disc color record. The methane-band filters FQ727N and FQ889N are left out. The Minnaert exponents that fit those profiles over the disc out to 86.3 degrees are red 0.798, green 0.698 and blue 0.728, departing from the profiles by at most 0.004, 0.0001 and 0.028. At 0.95 of the radius green/red is 1.26 times its centre value and blue/red 1.18 (1.42 and 0.87 with one filter per channel): the limb pales instead of turning olive. On screen, 5 pixels inside the edge of the default view, green/red went from 1.02 to 0.89 and blue/red from 0.55 to 0.66 (2026-10-05).", + "evidence": [ + "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt", + "https://pds-atmospheres.nmsu.edu/PDS/data/gbat_0001/data/1995low.lbl", + "https://github.com/layoutit/css.earth/blob/main/packages/bake/authoring/saturn/display-limb.mts" + ] + }, + { + "id": "limb-overlay-outline", + "subject": "Dark dashes along the limb of the flood-lit overlay", + "status": "included", + "finding": "Under flood light the lane floored a grazing texel's emission at one texel (1/256) but not its incidence, so the limb law was held at the edge of its data on the emission alone and fell to about 2 % of the centre on scattered texels of the outermost ring: alpha up to 235 where the held law gives 136. At 8 screen pixels per texel they read as dark dashes: 29 dark dips along 32 degrees of limb on css.earth v0.6524 at 2x pixel density, none on Jupiter. Serving the frame with those 462 texels capped at 136 left 4 dips, and hiding the overlay showed the dashes were not the body's seams (more overlap between faces changed nothing). The lane now holds the incidence with the emission under flood light: 3 dips on the same rays, 2026-10-05. The overlay's color past the silhouette is also kept by each texel's share inside (4 x 4 samples). Under flood light that alone left the count at 31; with shadows on, where the law brightens the lit limb and the overlay turns black past the silhouette, it took the lit lower-left limb from 32 dips to 19 along 28 degrees and the fine-scale brightness variation from 18.6 to 8.0 levels.", + "evidence": [ + "https://github.com/layoutit/css.earth/blob/main/src/objects/saturn/README.md#visible-body" ] }, { "id": "tied-map-luminance", "subject": "Brightness of the color-tied visible map", "status": "included", - "finding": "The tie keeps red and lowers green and blue, so the map lost a fifth of its cosine-weighted luminance (0.426 to 0.337) and Saturn looked brown in the app (whole-disc Y 0.315 against 0.409 on main). The tied map now gets back its untied mean luminance with one factor on all three channels, 1.264, then a soft shoulder, 0.8 + 0.2 (1 - exp(-(m - 0.8) / 0.2)), on each texel's brightest channel, all channels by the same amount: 4.35 % of texels, none clipped, ratios kept. The factor alone clips red on 0.5 % of texels; the largest factor that clips nothing, 1.085, leaves the whole disc at Y 0.326. Chosen by the user on a preview built with the lane's own code, 2026-09-25.", + "finding": "The tie keeps red and lowers green and blue, so the map loses cosine-weighted luminance (0.426 to 0.337 with the law of 2026-09-25) and Saturn looked brown in the app (whole-disc Y 0.315 against 0.409 on main). The tied map gets back its untied mean luminance with one factor on all three channels, 1.239 (1.264 on 2026-09-25, 1.269 after the ring-hidden rows were refilled), then a soft shoulder, 0.8 + 0.2 (1 - exp(-(m - 0.8) / 0.2)), on each texel's brightest channel, all channels by the same amount: 5.22 % of texels, none clipped, ratios kept. Chosen by the user on a preview built with the lane's own code, 2026-09-25.", "evidence": [ "https://doi.org/10.1006/icar.1998.5913" ] diff --git a/src/objects/saturn/object.json b/src/objects/saturn/object.json index 6c3b3f8f42b..0d0d4286f5c 100644 --- a/src/objects/saturn/object.json +++ b/src/objects/saturn/object.json @@ -135,6 +135,11 @@ "id": "visible-2025a", "source": "observations", "material": "projective" + }, + { + "id": "cassini-2011", + "source": "observations", + "material": "projective" } ] } diff --git a/src/objects/saturn/source/cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.xml b/src/objects/saturn/source/cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.xml new file mode 100644 index 00000000000..bf6a0e146e1 --- /dev/null +++ b/src/objects/saturn/source/cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.xml @@ -0,0 +1,215 @@ + + + + + + + + urn:nasa:pds:co_iss_global-maps:data_derived:cassini_iss_rgb_saturn_global_color_map_contrast_enhance + 1.0 + Cassini ISS RGB Saturn Global Color Map Contrast Enhance + 1.15.0.0 + Product_Observational + + + 2023-05-08 + 1.0 + Initial Template Creation + + + 2023-11-01 + 1.0 + Corrections/Edits from Lien Resolution + + + + + + 2011-08-11T03:05:09.695Z + 2011-08-11T11:40:11.412Z + + + Science + Derived + + Visible + Atmosphere + Atmospheres + Structure + + + + Cassini-Huygens + Mission + + urn:nasa:pds:context:investigation:mission.cassini-huygens + data_to_investigation + + + + Cassini + + Cassini + Host + + urn:nasa:pds:context:instrument_host:spacecraft.co + is_instrument_host + + + + Imaging Science Subsystem - Narrow Angle for CO + Instrument + + urn:nasa:pds:context:instrument:issna.co + is_instrument + + + + Imaging Science Subsystem - Wide Angle for CO + Instrument + + urn:nasa:pds:context:instrument:isswa.co + is_instrument_host + + + + + Saturn + Planet + + urn:nasa:pds:context:target:planet.saturn + data_to_target + + + + + + data_map + cartography_parameters_to_image_object + + + + 360 + 0 + 90 + -90 + + + + + + + Equirectangular + + 0.0 + 0.0 + 180.0 + + + + Row and Column + + 161 + 161 + 0.1 + 0.1 + + + + 0.0 + 90.0 + + + + Planetocentric + 60268 + 60268 + 54364 + Positive West + + + + + + + data_map + display_settings_to_array + + + + These settings apply to all images (Array_2D_Image) in the label. + + Sample + Left to Right + Line + Bottom to Top + + + + + + + Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits + file + 2018-05-14T16:57:55Z + +
+ header_map + 0 + 2880 + FITS 3.0 +
+ + data_map + 2880 + 3 + Last Index Fastest + Global map, color 3-band + + IEEE754MSBSingle + + + Line + 3601 + 1 + + + Sample + 1801 + 2 + + + Band + 3 + 3 + + + -999.9 + + +
+ + + Image_Information/Saturn_ISS_RGB_global_map_inf.txt + image-list + 2042 + List of images and geometry used in this product. + + + text-file + 0 + 2042 + 7-Bit ASCII Text + Supplemental text listing original images used in this map product. + Carriage-Return Line-Feed + + +
diff --git a/src/objects/saturn/source/cassini-iss/Saturn_ISS_RGB_global_map_inf.txt b/src/objects/saturn/source/cassini-iss/Saturn_ISS_RGB_global_map_inf.txt new file mode 100644 index 00000000000..310ac67d4da --- /dev/null +++ b/src/objects/saturn/source/cassini-iss/Saturn_ISS_RGB_global_map_inf.txt @@ -0,0 +1,23 @@ +RED filter +name time img_size resolution sub_solar_lat sub_craft_lat phs_angle east_lon west_lon +W1691726068_1.IMG 2011-223T03:05:09.695Z 1024 160.61820 10.689789 0.23610450 41.835512 208.42201 25.969197 +W1691733777_1.IMG 2011-223T05:13:38.630Z 1024 160.79694 10.691002 0.23729859 42.064955 280.52115 98.071726 +W1691741486_1.IMG 2011-223T07:22:07.582Z 1024 160.79987 10.692215 0.23848606 42.293993 352.62108 170.17480 +W1691749195_1.IMG 2011-223T09:30:36.533Z 1024 161.10974 10.693428 0.23966712 42.522668 64.721581 242.27820 +W1691756904_1.IMG 2011-223T11:39:05.484Z 1024 161.24385 10.694641 0.24084199 42.751020 136.82262 314.38190 + +GRN filter +name time img_size resolution sub_solar_lat sub_craft_lat phs_angle east_lon west_lon +W1691726101_1.IMG 2011-223T03:05:42.693Z 1024 160.61900 10.689794 0.23610962 41.836495 208.73062 26.277830 +W1691733810_1.IMG 2011-223T05:14:11.633Z 1024 160.79768 10.691007 0.23730369 42.065936 280.82982 98.380408 +W1691741519_1.IMG 2011-223T07:22:40.584Z 1024 160.79987 10.692220 0.23849113 42.294973 352.92974 170.48347 +W1691749228_1.IMG 2011-223T09:31:09.536Z 1024 161.11035 10.693433 0.23967216 42.523646 65.030253 242.58688 +W1691756937_1.IMG 2011-223T11:39:38.487Z 1024 161.24439 10.694646 0.24084701 42.751997 137.13129 314.69058 + +BL1 filter +name time img_size resolution sub_solar_lat sub_craft_lat phs_angle east_lon west_lon +W1691726134_1.IMG 2011-223T03:06:15.618Z 1024 160.61979 10.689799 0.23611474 41.837476 209.03856 26.585780 +W1691733843_1.IMG 2011-223T05:14:44.558Z 1024 160.79841 10.691012 0.23730878 42.066915 281.13776 98.688360 +W1691741552_1.IMG 2011-223T07:23:13.509Z 1024 160.79987 10.692225 0.23849619 42.295951 353.23768 170.79143 +W1691749261_1.IMG 2011-223T09:31:42.460Z 1024 161.11095 10.693438 0.23967719 42.524622 65.338187 242.89483 +W1691756970_1.IMG 2011-223T11:40:11.412Z 1024 161.24494 10.694651 0.24085201 42.752972 137.43924 314.99854 \ No newline at end of file diff --git a/src/objects/saturn/source/content/object.json b/src/objects/saturn/source/content/object.json index 044df2e9939..acb9c0c51ed 100644 --- a/src/objects/saturn/source/content/object.json +++ b/src/objects/saturn/source/content/object.json @@ -381,6 +381,23 @@ "path": "../opal-dates/hlsp_opal_hst_wfc3-uvis_saturn-2025a_f395n-f502n-f631n_v1_globalmap.tif", "url": "https://archive.stsci.edu/hlsp/opal/opal-saturn-cycle-32" } + }, + { + "id": "cassini-2011", + "label": "Cassini 2011", + "shortLabel": "Cassini", + "thumbnail": "/scenes/saturn/saturn-dataset-cassini-2011.webp", + "surface": "/scenes/saturn/saturn-surface-cassini-2011@2x.webp", + "poles": "/scenes/saturn/saturn-poles-cassini-2011.webp", + "falseColor": false, + "noData": true, + "qualification": "Cassini ISS · 2011-08-11 · enhanced color", + "notes": "Cassini wide-angle frames of 2011-08-11 in the RED, GRN and BL1 filters, mapped by Li, West, Jiang and Knowles (NASA PDS Atmospheres Node). This is the archive's contrast-enhanced color map: each color was stretched on its own, so the colors are stronger than the eye would see. The band of disturbed clouds around the north is what the great storm first seen on 5 December 2010 left behind. Gray grid marks what the map does not hold: both polar caps, the rings' line at the equator and the rings' shadow from 5 to 17 degrees south. The darker strip above that band is the edge of the same shadow. The scene lighting and rings follow the existing body presentation, not the observation date.", + "source": { + "id": "cassini-iss-2011-color-map-enhanced", + "path": "../cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "url": "https://doi.org/10.17189/rkkb-6y30" + } } ] }, diff --git a/src/objects/saturn/source/manifest.json b/src/objects/saturn/source/manifest.json index 6d3583e014d..57906460236 100644 --- a/src/objects/saturn/source/manifest.json +++ b/src/objects/saturn/source/manifest.json @@ -1676,6 +1676,105 @@ "kind": "local", "reason": "Project-authored processing settings for the separately recorded source observations." } + }, + { + "id": "cassini-iss-2011-color-map-enhanced", + "path": "cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "origin": "https://pds-atmospheres.nmsu.edu/PDS/data/PDS4/co_iss_global-maps/data_derived/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "credit": "L. Li, R. A. West, X. Jiang and B. Knowles; Cassini ISS, NASA/JPL-Caltech/Space Science Institute; NASA Planetary Data System, Atmospheres Node", + "license": "NASA PDS archive data, urn:nasa:pds:co_iss_global-maps::1.0; not copyrighted, cite the data set", + "acquisition": "Product of the PDS bundle Cassini ISS Global Maps of Jupiter and Saturn: the contrast-enhanced RED/GRN/BL1 color map of Saturn, 2011-08-11. Taken on 2026-10-05 from the Internet Archive's 2024-11-18 capture of the bundle archive, co_iss_global-maps.tar.gz, while the node refused connections; the file has the 77,829,120 bytes the PDS registry lists. See preparation/acquisition.json.", + "redistribution": "checked scientific archive product with full credit", + "consumers": [ + "datasets" + ], + "capture": { + "attributions": [ + { + "kind": "facility", + "facilityId": "cassini", + "missionId": "cassini", + "evidence": "https://doi.org/10.17189/rkkb-6y30" + } + ] + }, + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "pds-cassini-iss-global-maps-v1", + "role": "material", + "evidence": "Product cassini_iss_rgb_saturn_global_color_map_contrast_enhance of the bundle's derived data collection; its label is stored beside it.", + "locator": "data_derived/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits" + } + ] + } + }, + { + "id": "cassini-iss-2011-color-map-enhanced-label", + "path": "cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.xml", + "origin": "https://pds-atmospheres.nmsu.edu/PDS/data/PDS4/co_iss_global-maps/data_derived/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.xml", + "credit": "L. Li, R. A. West, X. Jiang and B. Knowles; Cassini ISS, NASA/JPL-Caltech/Space Science Institute; NASA Planetary Data System, Atmospheres Node", + "license": "NASA PDS archive data, urn:nasa:pds:co_iss_global-maps::1.0; not copyrighted, cite the data set", + "acquisition": "PDS4 label of the color map, from the same capture of the bundle archive: map projection, planetocentric latitude, positive-west longitude, radii and array layout.", + "redistribution": "checked scientific archive product with full credit", + "consumers": [ + "datasets" + ], + "capture": { + "attributions": [ + { + "kind": "facility", + "facilityId": "cassini", + "missionId": "cassini", + "evidence": "https://doi.org/10.17189/rkkb-6y30" + } + ] + }, + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "pds-cassini-iss-global-maps-v1", + "role": "material", + "evidence": "PDS4 label of the color map product.", + "locator": "data_derived/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.xml" + } + ] + } + }, + { + "id": "cassini-iss-2011-color-map-enhanced-frames", + "path": "cassini-iss/Saturn_ISS_RGB_global_map_inf.txt", + "origin": "https://pds-atmospheres.nmsu.edu/PDS/data/PDS4/co_iss_global-maps/data_derived/Image_Information/Saturn_ISS_RGB_global_map_inf.txt", + "credit": "L. Li, R. A. West, X. Jiang and B. Knowles; Cassini ISS, NASA/JPL-Caltech/Space Science Institute; NASA Planetary Data System, Atmospheres Node", + "license": "NASA PDS archive data, urn:nasa:pds:co_iss_global-maps::1.0; not copyrighted, cite the data set", + "acquisition": "The bundle's list of the fifteen wide-angle frames the color map was made from, with each frame's time, resolution, phase angle and sub-spacecraft and sub-solar latitudes; from the same capture of the bundle archive.", + "redistribution": "checked scientific archive product with full credit", + "consumers": [ + "datasets" + ], + "capture": { + "attributions": [ + { + "kind": "facility", + "facilityId": "cassini", + "missionId": "cassini", + "evidence": "https://doi.org/10.17189/rkkb-6y30" + } + ] + }, + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "pds-cassini-iss-global-maps-v1", + "role": "material", + "evidence": "Supplemental product of the color map: the frames and geometry it was made from.", + "locator": "data_derived/Image_Information/Saturn_ISS_RGB_global_map_inf.txt" + } + ] + } } ], "generatedIntermediates": [ @@ -1696,14 +1795,14 @@ }, { "path": "photometry/opal-2025-minnaert-f631n.json", - "purpose": "Hubble OPAL Minnaert coefficient of the 2025a color map (F631N, F502N, F395N), red channel (F631N): the correction OPAL divided out of the map, which the lighting overlay puts back.", + "purpose": "Hubble OPAL Minnaert coefficient of F631N from the 2025 Saturn README: the correction OPAL divided out of that filter's map, and one of the filters the display channels' limb law weighs.", "sourceBinding": { "kind": "catalogued", "references": [ { "catalogueId": "reference-opal-saturn-2025-readme", "role": "method", - "evidence": "The OPAL README's Minnaert table gives k = 0.8 for F631N, the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", + "evidence": "The OPAL README's Minnaert table gives k = 0.80 for F631N (\"F631N 0.80 .00362\"), the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", "locator": "Minnaert table, F631N row" } ] @@ -1714,14 +1813,14 @@ }, { "path": "photometry/opal-2025-minnaert-f502n.json", - "purpose": "Hubble OPAL Minnaert coefficient of the 2025a color map (F631N, F502N, F395N), green channel (F502N): the correction OPAL divided out of the map, which the lighting overlay puts back.", + "purpose": "Hubble OPAL Minnaert coefficient of F502N from the 2025 Saturn README: the correction OPAL divided out of that filter's map, and one of the filters the display channels' limb law weighs.", "sourceBinding": { "kind": "catalogued", "references": [ { "catalogueId": "reference-opal-saturn-2025-readme", "role": "method", - "evidence": "The OPAL README's Minnaert table gives k = 0.65 for F502N, the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", + "evidence": "The OPAL README's Minnaert table gives k = 0.65 for F502N (\"F502N 0.65 .00285\"), the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", "locator": "Minnaert table, F502N row" } ] @@ -1732,14 +1831,14 @@ }, { "path": "photometry/opal-2025-minnaert-f467m.json", - "purpose": "Hubble OPAL Minnaert coefficient for the blue channel of the 2025a color map: the F467M coefficient, not the F395N filter the channel was imaged in, because the map's display color is tied to Saturn's true color, whose blue channel is carried by light near 467 nm. The lighting overlay puts it back relative to the flood-lit disc centre.", + "purpose": "Hubble OPAL Minnaert coefficient of F467M from the 2025 Saturn README: the correction OPAL divided out of that filter's map, and one of the filters the display channels' limb law weighs.", "sourceBinding": { "kind": "catalogued", "references": [ { "catalogueId": "reference-opal-saturn-2025-readme", "role": "method", - "evidence": "The OPAL README's Minnaert table gives k = 0.86 for F467M (\"F467M 0.86 .00507\"), the correction applied to remove limb darkening before projection; F467M is the README filter closest to the sRGB blue primary. The F395N coefficient, 0.40, belongs to violet light that the true-color blue channel barely contains (13 % of it comes from 380-429 nm in Saturn's sunlit spectrum). The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", + "evidence": "The OPAL README's Minnaert table gives k = 0.86 for F467M (\"F467M 0.86 .00507\"), the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", "locator": "Minnaert table, F467M row" } ] @@ -1765,6 +1864,114 @@ "origin": "https://pds-atmospheres.nmsu.edu/PDS/data/gbat_0001/data/1995low.tab", "credit": "E. Karkoschka (1998), Icarus 133, 134-146, archived by the NASA Planetary Data System Atmospheres Node; color computation: cssEarth.", "acquisition": "Color computed once from the cited PDS table and recorded in a project document; the table itself is not stored with the body." + }, + { + "path": "photometry/opal-2025-minnaert-f395n.json", + "purpose": "Hubble OPAL Minnaert coefficient of F395N from the 2025 Saturn README: the correction OPAL divided out of that filter's map, and one of the filters the display channels' limb law weighs.", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "reference-opal-saturn-2025-readme", + "role": "method", + "evidence": "The OPAL README's Minnaert table gives k = 0.40 for F395N (\"F395N 0.40 .00231\"), the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", + "locator": "Minnaert table, F395N row" + } + ] + }, + "origin": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt", + "credit": "Hubble OPAL program (A. A. Simon, M. H. Wong and team), NASA/ESA/STScI; transcription: cssEarth.", + "acquisition": "Parameter values transcribed from the cited source into a project document; no data file was downloaded." + }, + { + "path": "photometry/opal-2025-minnaert-f763m.json", + "purpose": "Hubble OPAL Minnaert coefficient of F763M from the 2025 Saturn README: the correction OPAL divided out of that filter's map, and one of the filters the display channels' limb law weighs.", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "reference-opal-saturn-2025-readme", + "role": "method", + "evidence": "The OPAL README's Minnaert table gives k = 0.85 for F763M (\"F763M 0.85 .00376\"), the correction applied to remove limb darkening before projection. The fitted phase range is the Earth-based phase of the map's epoch, 2.4-2.5 degrees on 29-30 August 2025, from JPL Horizons; the README states no angle ranges. The fitted emission range ends at 86.3 degrees: the outermost pixel of the WFC3/UVIS disc at the map's epoch (angular diameter from JPL Horizons, 0.0395 arcsec UVIS pixels), beyond which the observation resolved nothing.", + "locator": "Minnaert table, F763M row" + } + ] + }, + "origin": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt", + "credit": "Hubble OPAL program (A. A. Simon, M. H. Wong and team), NASA/ESA/STScI; transcription: cssEarth.", + "acquisition": "Parameter values transcribed from the cited source into a project document; no data file was downloaded." + }, + { + "path": "photometry/opal-2025-minnaert-display-red.json", + "purpose": "Saturn's Minnaert limb coefficient for the display's red channel: the OPAL README's coefficients weighted by the light that channel shows. The lighting overlay puts it back relative to the flood-lit disc centre.", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "reference-opal-saturn-2025-readme", + "role": "method", + "evidence": "Derived by packages/bake/authoring/saturn/display-limb.mts from the README's coefficients for F395N, F467M, F502N, F631N and F763M (0.40, 0.86, 0.65, 0.80, 0.85; the per-filter records beside this one), interpolated in wavelength between filters and held outside them, each wavelength weighted by its share of the sRGB red channel: k = 0.798. A weighted sum of Minnaert laws is not a Minnaert law; this exponent fits the weighted profile over the flood-lit disc out to 86.3 degrees, where the power law departs from the profile by at most 0.004. The methane-band filters FQ727N and FQ889N are left out. The phase and emission ranges are the per-filter records'.", + "locator": "Minnaert table, F395N, F467M, F502N, F631N and F763M rows" + }, + { + "catalogueId": "karkoschka-1998-jovian-spectrophotometry-1995-low", + "role": "reference", + "evidence": "The weights are the SATURN ALBEDO column of 1995LOW.TAB times a 5,772 K Planck spectrum through the CIE 1931 2 degree observer into linear sRGB, the computation of the whole-disc color record; the command checks that the same spectrum reproduces that record's channel ratios.", + "locator": "1995LOW.TAB, columns 2 (air wavelength) and 5 (Saturn albedo), 380-780 nm" + } + ] + }, + "origin": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt", + "credit": "Hubble OPAL program (A. A. Simon, M. H. Wong and team), NASA/ESA/STScI; E. Karkoschka (1998), Icarus 133, 134-146; weighting: cssEarth.", + "acquisition": "Derived by packages/bake/authoring/saturn/display-limb.mts from the per-filter records and the cited PDS table; the table itself is not stored with the body." + }, + { + "path": "photometry/opal-2025-minnaert-display-green.json", + "purpose": "Saturn's Minnaert limb coefficient for the display's green channel: the OPAL README's coefficients weighted by the light that channel shows. The lighting overlay puts it back relative to the flood-lit disc centre.", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "reference-opal-saturn-2025-readme", + "role": "method", + "evidence": "Derived by packages/bake/authoring/saturn/display-limb.mts from the README's coefficients for F395N, F467M, F502N, F631N and F763M (0.40, 0.86, 0.65, 0.80, 0.85; the per-filter records beside this one), interpolated in wavelength between filters and held outside them, each wavelength weighted by its share of the sRGB green channel: k = 0.698. A weighted sum of Minnaert laws is not a Minnaert law; this exponent fits the weighted profile over the flood-lit disc out to 86.3 degrees, where the power law departs from the profile by at most 0.0001. The methane-band filters FQ727N and FQ889N are left out. The phase and emission ranges are the per-filter records'.", + "locator": "Minnaert table, F395N, F467M, F502N, F631N and F763M rows" + }, + { + "catalogueId": "karkoschka-1998-jovian-spectrophotometry-1995-low", + "role": "reference", + "evidence": "The weights are the SATURN ALBEDO column of 1995LOW.TAB times a 5,772 K Planck spectrum through the CIE 1931 2 degree observer into linear sRGB, the computation of the whole-disc color record; the command checks that the same spectrum reproduces that record's channel ratios.", + "locator": "1995LOW.TAB, columns 2 (air wavelength) and 5 (Saturn albedo), 380-780 nm" + } + ] + }, + "origin": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt", + "credit": "Hubble OPAL program (A. A. Simon, M. H. Wong and team), NASA/ESA/STScI; E. Karkoschka (1998), Icarus 133, 134-146; weighting: cssEarth.", + "acquisition": "Derived by packages/bake/authoring/saturn/display-limb.mts from the per-filter records and the cited PDS table; the table itself is not stored with the body." + }, + { + "path": "photometry/opal-2025-minnaert-display-blue.json", + "purpose": "Saturn's Minnaert limb coefficient for the display's blue channel: the OPAL README's coefficients weighted by the light that channel shows. The lighting overlay puts it back relative to the flood-lit disc centre.", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "reference-opal-saturn-2025-readme", + "role": "method", + "evidence": "Derived by packages/bake/authoring/saturn/display-limb.mts from the README's coefficients for F395N, F467M, F502N, F631N and F763M (0.40, 0.86, 0.65, 0.80, 0.85; the per-filter records beside this one), interpolated in wavelength between filters and held outside them, each wavelength weighted by its share of the sRGB blue channel: k = 0.728. A weighted sum of Minnaert laws is not a Minnaert law; this exponent fits the weighted profile over the flood-lit disc out to 86.3 degrees, where the power law departs from the profile by at most 0.028. The methane-band filters FQ727N and FQ889N are left out. The phase and emission ranges are the per-filter records'.", + "locator": "Minnaert table, F395N, F467M, F502N, F631N and F763M rows" + }, + { + "catalogueId": "karkoschka-1998-jovian-spectrophotometry-1995-low", + "role": "reference", + "evidence": "The weights are the SATURN ALBEDO column of 1995LOW.TAB times a 5,772 K Planck spectrum through the CIE 1931 2 degree observer into linear sRGB, the computation of the whole-disc color record; the command checks that the same spectrum reproduces that record's channel ratios.", + "locator": "1995LOW.TAB, columns 2 (air wavelength) and 5 (Saturn albedo), 380-780 nm" + } + ] + }, + "origin": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025_all_v1_readme.txt", + "credit": "Hubble OPAL program (A. A. Simon, M. H. Wong and team), NASA/ESA/STScI; E. Karkoschka (1998), Icarus 133, 134-146; weighting: cssEarth.", + "acquisition": "Derived by packages/bake/authoring/saturn/display-limb.mts from the per-filter records and the cited PDS table; the table itself is not stored with the body." } ] } diff --git a/src/objects/saturn/source/photometry/opal-2025-minnaert-display-blue.json b/src/objects/saturn/source/photometry/opal-2025-minnaert-display-blue.json new file mode 100644 index 00000000000..01132a82a98 --- /dev/null +++ b/src/objects/saturn/source/photometry/opal-2025-minnaert-display-blue.json @@ -0,0 +1,25 @@ +{ + "schema": "cssearth-photometric-model@1", + "id": "opal-2025-minnaert-display-blue", + "instrument": "Hubble WFC3/UVIS (OPAL)", + "filter": "sRGB blue: the README's continuum filters weighted by Saturn's sunlit spectrum through the CIE 1931 observer", + "quantity": "radiance-factor", + "model": { + "family": "separable", + "disk": { + "family": "minnaert", + "coefficient": 0.728, + "coefficientPerDegree": 0 + } + }, + "fit": { + "phaseDegrees": [ + 2.4, + 2.5 + ], + "emissionDegrees": [ + 0, + 86.3 + ] + } +} diff --git a/src/objects/saturn/source/photometry/opal-2025-minnaert-display-green.json b/src/objects/saturn/source/photometry/opal-2025-minnaert-display-green.json new file mode 100644 index 00000000000..1e1bf3e5478 --- /dev/null +++ b/src/objects/saturn/source/photometry/opal-2025-minnaert-display-green.json @@ -0,0 +1,25 @@ +{ + "schema": "cssearth-photometric-model@1", + "id": "opal-2025-minnaert-display-green", + "instrument": "Hubble WFC3/UVIS (OPAL)", + "filter": "sRGB green: the README's continuum filters weighted by Saturn's sunlit spectrum through the CIE 1931 observer", + "quantity": "radiance-factor", + "model": { + "family": "separable", + "disk": { + "family": "minnaert", + "coefficient": 0.698, + "coefficientPerDegree": 0 + } + }, + "fit": { + "phaseDegrees": [ + 2.4, + 2.5 + ], + "emissionDegrees": [ + 0, + 86.3 + ] + } +} diff --git a/src/objects/saturn/source/photometry/opal-2025-minnaert-display-red.json b/src/objects/saturn/source/photometry/opal-2025-minnaert-display-red.json new file mode 100644 index 00000000000..a9d239ee740 --- /dev/null +++ b/src/objects/saturn/source/photometry/opal-2025-minnaert-display-red.json @@ -0,0 +1,25 @@ +{ + "schema": "cssearth-photometric-model@1", + "id": "opal-2025-minnaert-display-red", + "instrument": "Hubble WFC3/UVIS (OPAL)", + "filter": "sRGB red: the README's continuum filters weighted by Saturn's sunlit spectrum through the CIE 1931 observer", + "quantity": "radiance-factor", + "model": { + "family": "separable", + "disk": { + "family": "minnaert", + "coefficient": 0.798, + "coefficientPerDegree": 0 + } + }, + "fit": { + "phaseDegrees": [ + 2.4, + 2.5 + ], + "emissionDegrees": [ + 0, + 86.3 + ] + } +} diff --git a/src/objects/saturn/source/photometry/opal-2025-minnaert-f395n.json b/src/objects/saturn/source/photometry/opal-2025-minnaert-f395n.json new file mode 100644 index 00000000000..1b069052278 --- /dev/null +++ b/src/objects/saturn/source/photometry/opal-2025-minnaert-f395n.json @@ -0,0 +1,25 @@ +{ + "schema": "cssearth-photometric-model@1", + "id": "opal-2025-minnaert-f395n", + "instrument": "Hubble WFC3/UVIS (OPAL)", + "filter": "F395N, 395 nm", + "quantity": "radiance-factor", + "model": { + "family": "separable", + "disk": { + "family": "minnaert", + "coefficient": 0.4, + "coefficientPerDegree": 0 + } + }, + "fit": { + "phaseDegrees": [ + 2.4, + 2.5 + ], + "emissionDegrees": [ + 0, + 86.3 + ] + } +} diff --git a/src/objects/saturn/source/photometry/opal-2025-minnaert-f763m.json b/src/objects/saturn/source/photometry/opal-2025-minnaert-f763m.json new file mode 100644 index 00000000000..86c25835180 --- /dev/null +++ b/src/objects/saturn/source/photometry/opal-2025-minnaert-f763m.json @@ -0,0 +1,25 @@ +{ + "schema": "cssearth-photometric-model@1", + "id": "opal-2025-minnaert-f763m", + "instrument": "Hubble WFC3/UVIS (OPAL)", + "filter": "F763M, 763 nm", + "quantity": "radiance-factor", + "model": { + "family": "separable", + "disk": { + "family": "minnaert", + "coefficient": 0.85, + "coefficientPerDegree": 0 + } + }, + "fit": { + "phaseDegrees": [ + 2.4, + 2.5 + ], + "emissionDegrees": [ + 0, + 86.3 + ] + } +} diff --git a/src/objects/saturn/source/preparation/acquisition.json b/src/objects/saturn/source/preparation/acquisition.json index 640cc5d0c69..e75d9cb9dea 100644 --- a/src/objects/saturn/source/preparation/acquisition.json +++ b/src/objects/saturn/source/preparation/acquisition.json @@ -393,6 +393,14 @@ ], "path": "opal-dates/hlsp_opal_hst_wfc3-uvis_saturn-2025a_f631n_v1_globalmap.fits", "url": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025a_f631n_v1_globalmap.fits" + }, + { + "kind": "download", + "groups": [ + "refresh" + ], + "path": "cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "url": "https://pds-atmospheres.nmsu.edu/PDS/data/PDS4/co_iss_global-maps/data_derived/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits" } ] } diff --git a/src/objects/saturn/source/preparation/geometry.json b/src/objects/saturn/source/preparation/geometry.json index 25e51a9bf5f..d33f9487ca2 100644 --- a/src/objects/saturn/source/preparation/geometry.json +++ b/src/objects/saturn/source/preparation/geometry.json @@ -9,9 +9,9 @@ }, "limb": { "models": [ - "photometry/opal-2025-minnaert-f631n.json", - "photometry/opal-2025-minnaert-f502n.json", - "photometry/opal-2025-minnaert-f467m.json" + "photometry/opal-2025-minnaert-display-red.json", + "photometry/opal-2025-minnaert-display-green.json", + "photometry/opal-2025-minnaert-display-blue.json" ] }, "colorTie": "photometry/karkoschka-1998-whole-disc-color.json", diff --git a/src/objects/saturn/source/preparation/observations.json b/src/objects/saturn/source/preparation/observations.json index 560a32b0bdb..600fa35552c 100644 --- a/src/objects/saturn/source/preparation/observations.json +++ b/src/objects/saturn/source/preparation/observations.json @@ -256,6 +256,14 @@ "origin": "https://archive.stsci.edu/missions/hlsp/opal/cycle32/saturn/hlsp_opal_hst_wfc3-uvis_saturn-2025a_f631n_v1_globalmap.fits", "credit": "NASA, ESA, A. A. Simon, M. H. Wong and the OPAL team", "license": "CC BY 4.0" + }, + { + "id": "cassini-iss-2011-color-map-enhanced", + "path": "cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "expectedBytes": 77829120, + "origin": "https://pds-atmospheres.nmsu.edu/PDS/data/PDS4/co_iss_global-maps/data_derived/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "credit": "L. Li, R. A. West, X. Jiang and B. Knowles; Cassini ISS, NASA/JPL-Caltech/Space Science Institute; NASA Planetary Data System, Atmospheres Node", + "license": "NASA PDS archive data, urn:nasa:pds:co_iss_global-maps::1.0; not copyrighted, cite the data set" } ], "datasets": [ @@ -1186,6 +1194,132 @@ "longitudePeriod": 1800 }, "planetographicAxisRatio": 1.1086012802589948 + }, + { + "id": "cassini-2011", + "source": "cassini-iss/Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance.fits", + "decode": { + "kind": "fits-planes", + "bitpix": -32, + "width": 3601, + "height": 1801, + "firstRow": "north", + "crop": { + "left": 1, + "top": 0, + "width": 3600, + "height": 1801 + } + }, + "products": [ + { + "kind": "surface", + "filename": "saturn-surface-cassini-2011.webp", + "transforms": [ + { + "kind": "resize", + "width": 2048, + "height": 1024 + } + ], + "packing": { + "bandCount": 16, + "gutter": 16 + }, + "encoding": { + "quality": 80, + "smartSubsample": true, + "effort": 6 + } + }, + { + "kind": "surface", + "filename": "saturn-surface-cassini-2011@2x.webp", + "transforms": [ + { + "kind": "resize", + "width": 4096, + "height": 2048 + } + ], + "packing": { + "bandCount": 16, + "gutter": 32 + }, + "encoding": { + "quality": 80, + "smartSubsample": true, + "effort": 6 + } + }, + { + "kind": "poles", + "filename": "saturn-poles-cassini-2011.webp", + "projection": { + "tileSize": 512, + "poles": [ + "north", + "south", + "north", + "south", + "north", + "south", + "north", + "south" + ], + "latitudeSegments": 16, + "angularMode": "orthographic", + "sampling": "bilinear-wrapped" + }, + "encoding": { + "quality": 80, + "smartSubsample": true, + "effort": 6, + "alphaQuality": 100 + } + }, + { + "kind": "thumbnail", + "filename": "saturn-dataset-cassini-2011.webp", + "transforms": [ + { + "kind": "resize", + "width": 132, + "height": 72 + } + ], + "encoding": { + "quality": 80, + "smartSubsample": true, + "effort": 6 + } + } + ], + "coverage": { + "kind": "declared-rows", + "unobservedRows": [ + [ + 0, + 110 + ], + [ + 898, + 906 + ], + [ + 954, + 1071 + ], + [ + 1721, + 1800 + ] + ], + "reverseLongitude": true, + "longitudeOffsetDegrees": -0.1, + "longitudePeriod": 3600 + }, + "planetocentricSampleRows": 1.1086012802589948 } ] } diff --git a/src/objects/saturn/text.json b/src/objects/saturn/text.json index 7e6cec7aaa4..17ee4c27d35 100644 --- a/src/objects/saturn/text.json +++ b/src/objects/saturn/text.json @@ -152,6 +152,20 @@ "locator": "Rotation 2025a; FITS DATE-OBS" } ] + }, + "cassini-2011": { + "title": "Cassini 2011-08-11", + "detail": "RED / GRN / BL1, enhanced", + "summary": "Clouds of the great storm circle the north, in the archive's enhanced color. Gray grid marks what Cassini did not map.", + "sources": [ + { + "catalogueId": "pds-cassini-iss-global-maps-v1", + "url": "https://doi.org/10.17189/rkkb-6y30", + "label": "NASA PDS · Cassini ISS global maps", + "checked": "2026-10-05", + "locator": "Cassini_ISS_RGB_Saturn_global_color_map_contrast_enhance; FITS DATE-STA" + } + ] } } } diff --git a/src/sources/pds-cassini-iss-global-maps-v1.json b/src/sources/pds-cassini-iss-global-maps-v1.json new file mode 100644 index 00000000000..bc088c7e9a0 --- /dev/null +++ b/src/sources/pds-cassini-iss-global-maps-v1.json @@ -0,0 +1,46 @@ +{ + "id": "pds-cassini-iss-global-maps-v1", + "kind": "data-product", + "identityLevel": "release", + "title": "Cassini ISS Global Maps of Jupiter and Saturn", + "version": "1.0", + "identifiers": [ + { + "type": "doi", + "value": "10.17189/rkkb-6y30" + }, + { + "type": "PDS4 LIDVID", + "value": "urn:nasa:pds:co_iss_global-maps::1.0" + } + ], + "creators": [ + "L. Li", + "R. A. West", + "X. Jiang", + "B. Knowles" + ], + "publisher": "NASA Planetary Data System, Atmospheres Node", + "publicationDate": "2023", + "links": [ + { + "role": "landing", + "url": "https://doi.org/10.17189/rkkb-6y30", + "label": "PDS bundle" + }, + { + "role": "original", + "url": "https://doi.org/10.1038/s41597-025-04392-3", + "label": "Wang, Li, Jiang and West (2025), the maps' data descriptor" + } + ], + "evidence": [ + { + "url": "https://pds.nasa.gov/ds-view/pds/viewBundle.jsp?identifier=urn%3Anasa%3Apds%3Aco_iss_global-maps&version=1.0", + "checkedOn": "2026-10-05", + "locator": "IDENTIFIER, NAME, AUTHORS, PUBLICATION YEAR and DIGITAL OBJECT IDENTIFIER" + } + ], + "relations": [], + "statements": [] +}