amaran-light-cli
Version:
Command line tool for controlling Aputure Amaran lights via WebSocket to a local Amaran desktop app.
292 lines • 15.4 kB
JavaScript
import SunCalc from 'suncalc';
const { getTimes } = SunCalc;
import { CurveType, calculateCCT } from '../daylightSimulation/cctUtil.js';
import { CCT_DEFAULTS } from '../daylightSimulation/constants.js';
describe('calculateCCT', () => {
const NYC_LAT = 40.7128;
const NYC_LON = -74.006;
// Use the actual defaults from the module for all tests
const MIN_INTENSITY_PCT = CCT_DEFAULTS.intensityMinPct;
const MAX_INTENSITY_PCT = CCT_DEFAULTS.intensityMaxPct;
const MIN_CCT = CCT_DEFAULTS.cctMinK;
const MAX_CCT = CCT_DEFAULTS.cctMaxK;
const MIN_INTENSITY_API = MIN_INTENSITY_PCT * 10; // API format (50)
const MAX_INTENSITY_API = MAX_INTENSITY_PCT * 10; // API format (1000)
describe('NYC circadian lighting tests', () => {
let testDate;
let _sunrise;
let sunset;
let solarNoon;
let nightEnd;
let night;
beforeAll(() => {
// Use a fixed date for consistent test results
testDate = new Date('2024-06-21T12:00:00Z'); // Summer solstice
const times = getTimes(testDate, NYC_LAT, NYC_LON);
_sunrise = times.sunrise;
sunset = times.sunset;
solarNoon = times.solarNoon;
nightEnd = times.nightEnd;
night = times.night;
});
it('should return minimum CCT and intensity before nightEnd', () => {
const beforeNightEnd = new Date(nightEnd.getTime() - 60_000); // 1 minute before
const result = calculateCCT(NYC_LAT, NYC_LON, beforeNightEnd, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
expect(result.cct).toBe(MIN_CCT);
expect(result.intensity).toBe(MIN_INTENSITY_API);
});
it('should return minimum CCT and intensity at nightEnd edge', () => {
const atNightEnd = new Date(nightEnd.getTime());
const result = calculateCCT(NYC_LAT, NYC_LON, atNightEnd, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
// At the exact nightEnd moment, should be at minimum for empirical curves
expect(result.cct).toBe(MIN_CCT);
expect(result.intensity).toBe(MIN_INTENSITY_API);
});
it('should return maximum CCT and intensity at solar noon', () => {
const atNoon = new Date(solarNoon.getTime());
const result = calculateCCT(NYC_LAT, NYC_LON, atNoon, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
// At solar noon (middle of the day), should be at maximum
expect(result.cct).toBe(MAX_CCT);
expect(result.intensity).toBe(MAX_INTENSITY_API);
});
it('should return minimum CCT and intensity at night edge', () => {
const atNight = new Date(night.getTime());
const result = calculateCCT(NYC_LAT, NYC_LON, atNight, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
// At the exact night moment, should be at minimum
expect(result.cct).toBe(MIN_CCT);
expect(result.intensity).toBe(MIN_INTENSITY_API);
});
it('should return minimum CCT and intensity after night', () => {
const afterNight = new Date(night.getTime() + 60_000); // 1 minute after
const result = calculateCCT(NYC_LAT, NYC_LON, afterNight, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
expect(result.cct).toBe(MIN_CCT);
expect(result.intensity).toBe(MIN_INTENSITY_API);
});
it('should have smooth progression from nightEnd to noon', () => {
const quarterWay = new Date(nightEnd.getTime() + (solarNoon.getTime() - nightEnd.getTime()) / 4);
const result = calculateCCT(NYC_LAT, NYC_LON, quarterWay, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
// Should be between minimum and maximum values
expect(result.cct).toBeGreaterThan(MIN_CCT);
expect(result.cct).toBeLessThan(MAX_CCT);
expect(result.intensity).toBeGreaterThan(MIN_INTENSITY_API);
expect(result.intensity).toBeLessThan(MAX_INTENSITY_API);
});
it('should have smooth progression from noon to sunset', () => {
const threeQuartersWay = new Date(solarNoon.getTime() + (sunset.getTime() - solarNoon.getTime()) / 2);
const result = calculateCCT(NYC_LAT, NYC_LON, threeQuartersWay, {
intensityMinPct: MIN_INTENSITY_PCT,
intensityMaxPct: MAX_INTENSITY_PCT,
cctMinK: MIN_CCT,
cctMaxK: MAX_CCT,
});
// Should be between minimum and maximum values
expect(result.cct).toBeGreaterThan(MIN_CCT);
expect(result.cct).toBeLessThan(MAX_CCT);
expect(result.intensity).toBeGreaterThan(MIN_INTENSITY_API);
expect(result.intensity).toBeLessThan(MAX_INTENSITY_API);
});
});
describe('Edge cases', () => {
it('should handle midnight times', () => {
const midnight = new Date('2024-06-21T04:00:00Z'); // Midnight in NYC
const result = calculateCCT(NYC_LAT, NYC_LON, midnight);
expect(result.cct).toBe(MIN_CCT);
expect(result.intensity).toBe(MIN_INTENSITY_API);
});
it('should return valid results for different locations', () => {
const LONDON_LAT = 51.5074;
const LONDON_LON = -0.1278;
const testDate = new Date('2024-06-21T12:00:00Z');
const result = calculateCCT(LONDON_LAT, LONDON_LON, testDate);
expect(result.cct).toBeGreaterThanOrEqual(MIN_CCT);
expect(result.cct).toBeLessThanOrEqual(MAX_CCT);
expect(result.intensity).toBeGreaterThanOrEqual(MIN_INTENSITY_API); // 5% minimum
expect(result.intensity).toBeLessThanOrEqual(MAX_INTENSITY_API);
});
it('should use default options when none provided', () => {
const result = calculateCCT(NYC_LAT, NYC_LON, new Date('2024-06-21T12:00:00Z'));
expect(result.cct).toBeGreaterThanOrEqual(MIN_CCT);
expect(result.cct).toBeLessThanOrEqual(MAX_CCT);
expect(result.intensity).toBeGreaterThanOrEqual(MIN_INTENSITY_API); // 5% minimum
expect(result.intensity).toBeLessThanOrEqual(MAX_INTENSITY_API);
});
it('should use default date if not provided', () => {
const result = calculateCCT(NYC_LAT, NYC_LON);
expect(result.cct).toBeGreaterThanOrEqual(MIN_CCT);
expect(result.cct).toBeLessThanOrEqual(MAX_CCT);
expect(result.intensity).toBeGreaterThanOrEqual(MIN_INTENSITY_API); // 5% minimum
expect(result.intensity).toBeLessThanOrEqual(MAX_INTENSITY_API);
});
});
describe('Return value format', () => {
it('should return rounded integer values', () => {
const testDate = new Date('2024-06-21T12:00:00Z');
const result = calculateCCT(NYC_LAT, NYC_LON, testDate);
expect(Number.isInteger(result.cct)).toBe(true);
expect(Number.isInteger(result.intensity)).toBe(true);
});
it('should have correct properties', () => {
const testDate = new Date('2024-06-21T12:00:00Z');
const result = calculateCCT(NYC_LAT, NYC_LON, testDate);
expect(result).toHaveProperty('cct');
expect(result).toHaveProperty('intensity');
expect(result).toHaveProperty('lightOutput');
expect(typeof result.cct).toBe('number');
expect(typeof result.intensity).toBe('number');
expect(typeof result.lightOutput).toBe('number');
});
});
describe('Intensity ranges', () => {
const NYC_LAT = 40.7128;
const NYC_LON = -74.006;
it(`intensity stays within [${MIN_INTENSITY_API}, ${MAX_INTENSITY_API}] across a whole day (hourly samples)`, () => {
const base = new Date('2024-06-21T00:00:00Z'); // summer solstice
for (let hour = 0; hour < 24; hour++) {
const sample = new Date(base.getTime() + hour * 60 * 60 * 1000);
const { intensity } = calculateCCT(NYC_LAT, NYC_LON, sample);
expect(intensity).toBeGreaterThanOrEqual(MIN_INTENSITY_API); // 5% minimum
expect(intensity).toBeLessThanOrEqual(MAX_INTENSITY_API);
}
});
it('intensity stays within range across seasons and locations (spot samples)', () => {
const cases = [
// Dates: solstices and equinoxes
'2024-03-20T00:00:00Z', // vernal equinox
'2024-06-21T00:00:00Z', // summer solstice
'2024-09-22T00:00:00Z', // autumnal equinox
'2024-12-21T00:00:00Z', // winter solstice
];
const locations = [
{ name: 'NYC', lat: 40.7128, lon: -74.006 },
{ name: 'London', lat: 51.5074, lon: -0.1278 },
{ name: 'Sydney', lat: -33.8688, lon: 151.2093 },
];
const offsetsHours = [0, 6, 12, 18]; // midnight, morning, noon, evening (UTC)
for (const iso of cases) {
const base = new Date(iso);
for (const { lat, lon } of locations) {
for (const h of offsetsHours) {
const d = new Date(base.getTime() + h * 60 * 60 * 1000);
const { intensity } = calculateCCT(lat, lon, d);
expect(intensity).toBeGreaterThanOrEqual(MIN_INTENSITY_API); // 5% minimum
expect(intensity).toBeLessThanOrEqual(MAX_INTENSITY_API);
}
}
}
});
it('should respect intensityMinPct even when maxLux scaling would go below it', () => {
const noon = new Date('2024-06-21T16:00:00Z');
// lightOutput at noon in NYC is roughly 15000-20000 lux.
// If we set maxLux to 1,000,000, scaling factor is ~0.02 (2%).
// If intensityMinPct is 10%, result should be 100 (10%).
const result = calculateCCT(NYC_LAT, NYC_LON, noon, {
intensityMinPct: 10,
maxLux: 1000000,
});
expect(result.intensity).toBeGreaterThanOrEqual(100);
expect(result.intensity).toBeLessThan(150);
});
it('should respect intensityMaxPct even when maxLux scaling would go above it', () => {
const noon = new Date('2024-06-21T16:00:00Z');
// lightOutput at noon NYC is ~15000-20000 lux.
// If we set maxLux to 5000, scaling factor is ~3-4 (300-400%).
// If intensityMaxPct is 80%, result should be 800 (80%).
const result = calculateCCT(NYC_LAT, NYC_LON, noon, {
intensityMaxPct: 80,
maxLux: 5000,
});
expect(result.intensity).toBe(800);
});
});
describe('LightOutput (Lux) seasonal variation', () => {
it('should have higher lightOutput in summer than winter at noon (NYC)', () => {
const summerNoon = new Date('2024-06-21T16:00:00Z'); // Approx solar noon NYC (12:00 EDT)
const winterNoon = new Date('2024-12-21T17:00:00Z'); // Approx solar noon NYC (12:00 EST)
const summerResult = calculateCCT(NYC_LAT, NYC_LON, summerNoon, {}, CurveType.CIE_DAYLIGHT);
const winterResult = calculateCCT(NYC_LAT, NYC_LON, winterNoon, {}, CurveType.CIE_DAYLIGHT);
expect(summerResult.lightOutput).toBeGreaterThan(winterResult.lightOutput ?? 0);
expect(winterResult.lightOutput).toBeGreaterThan(0);
});
it('should have 0 lightOutput at night', () => {
const midnight = new Date('2024-06-21T04:00:00Z');
const result = calculateCCT(NYC_LAT, NYC_LON, midnight, {}, CurveType.CIE_DAYLIGHT);
expect(result.lightOutput).toBe(0);
});
});
describe('Weather Modifiers', () => {
const noon = new Date('2024-06-21T16:00:00Z'); // Approx solar noon NYC
it('should reduce intensity and lightOutput with cloudCover', () => {
const clearResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 0 },
});
const cloudyResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 0.5 },
});
const overcastResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 1.0 },
});
expect(cloudyResult.intensity).toBeLessThan(clearResult.intensity);
expect(overcastResult.intensity).toBeLessThan(cloudyResult.intensity);
expect(cloudyResult.lightOutput).toBeLessThan(clearResult.lightOutput || 0);
expect(overcastResult.lightOutput).toBeLessThan(cloudyResult.lightOutput || 0);
});
it('should shift CCT towards 6500K with cloudCover', () => {
// Assuming clear sky noon is usually cooler (>5500K) or warmer depending on curve,
// but let's check relative shift.
const clearResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 0 },
}, CurveType.PHYSICS);
const overcastResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 1.0 },
}, CurveType.PHYSICS);
// Physics curve at noon is usually blueish (>6500K) or whitish.
// If we force a very warm time (morning), clouds should cool it.
// If we force a very cool time (noon blue sky), clouds should warm it (neutralize it).
// Let's just check it moves towards 6500.
const target = 6500;
const clearDiff = Math.abs(clearResult.cct - target);
const overcastDiff = Math.abs(overcastResult.cct - target);
expect(overcastDiff).toBeLessThan(clearDiff);
});
it('should reduce intensity further with precipitation', () => {
const cloudyResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 0.5, precipitation: 'none' },
});
const rainyResult = calculateCCT(NYC_LAT, NYC_LON, noon, {
weather: { cloudCover: 0.5, precipitation: 'rain' },
});
expect(rainyResult.intensity).toBeLessThan(cloudyResult.intensity);
});
});
});
//# sourceMappingURL=cctUtil.test.js.map