mirror of
https://github.com/acamarata/nrel-spa.git
synced 2026-06-30 19:04:25 +00:00
Major fix to core time calculations
This commit is contained in:
parent
322342d978
commit
b1c7f638ae
8 changed files with 323 additions and 98 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,2 +1,8 @@
|
|||
node_modules/
|
||||
.env
|
||||
|
||||
# Ignore NREL SPA C sources and binaries
|
||||
/bin/spa
|
||||
/bin/spa_cli
|
||||
/bin/*.c
|
||||
/bin/*.h
|
||||
|
|
@ -14,3 +14,10 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
- Moved timezone to main args and changed default behavior (major)
|
||||
- Updated test cases and readme to reflect new usage (minor)
|
||||
|
||||
## [1.3.0] - 2025-05-04
|
||||
|
||||
- Major update to fix discrepancies between original C and this implementation
|
||||
- Folder "bin" added to compile and test against original C version
|
||||
- This NPM now gives the exact same results as the original NREL-SPA
|
||||
|
||||
88
bin/README.md
Normal file
88
bin/README.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# bin/README.md
|
||||
|
||||
This folder contains the C reference executable (`spa_cli`) and the JavaScript test harness (`test.js`) to compare your JS port of the NREL Solar Position Algorithm (SPA) against the original C implementation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* **Node.js** (v14+) installed on your machine.
|
||||
* **C compiler** (e.g. `gcc`) supporting C99.
|
||||
|
||||
## Files
|
||||
|
||||
* `spa_cli.c` – C CLI wrapper to parse command-line arguments into the SPA structure and output Sunrise, Solar Noon, Sunset in `HH:MM:SS` format.
|
||||
* `spa.c`, `spa.h` – The NREL SPA reference source (download separately).
|
||||
* `test.js` – Node.js script that runs 10 diverse test cases through both `spa_cli` and your JS port (`getSpa`) and prints a side-by-side comparison.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Download the NREL SPA source**
|
||||
|
||||
```bash
|
||||
cd bin
|
||||
curl -O https://midcdmz.nrel.gov/spa/spa.c
|
||||
curl -O https://midcdmz.nrel.gov/spa/spa.h
|
||||
```
|
||||
|
||||
2. **Copy or create `spa_cli.c`**
|
||||
Place the `spa_cli.c` file (provided alongside this README) into this folder.
|
||||
|
||||
3. **Compile the C executable**
|
||||
|
||||
```bash
|
||||
gcc -std=c99 -O2 -o spa_cli spa.c spa_cli.c -lm
|
||||
```
|
||||
|
||||
* **Do NOT** include `spa_tester.c` for this purpose. The custom `spa_cli.c` handles all required argument parsing and output.
|
||||
|
||||
4. **Install Node.js dependencies**
|
||||
From the project root (one level up):
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
This ensures your JS port (`index.js` and `dist/spa.js`) is available.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Inside the `bin/` folder, execute:
|
||||
|
||||
```bash
|
||||
node test.js
|
||||
```
|
||||
|
||||
You should see a table with each city/date, and matching Sunrise, Solar Noon, and Sunset times from both the C reference and your JS implementation.
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
Location | Date | C Rise | JS Rise | C Noon | JS Noon | C Set | JS Set
|
||||
-----------------------------------------------------------------------------------
|
||||
New York Summer | 2025-06-21 | 05:25:03 | 05:25:03 | 12:57:56 | 12:57:56 | 20:30:35 | 20:30:35
|
||||
...
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
* If you update your JS port (`index.js`), rerun `node test.js` to verify that drift remains within a second.
|
||||
* Ensure `spa_cli` is executable (`chmod +x spa_cli`) and located in the same directory as `test.js`.
|
||||
|
||||
## Results
|
||||
|
||||
Results from my personal tests when comparing original C version to this JS version is below:
|
||||
|
||||
```
|
||||
% node test.js
|
||||
Location | Date | C Rise | JS Rise | C Noon | JS Noon | C Set | JS Set
|
||||
-----------------------------------------------------------------------------------
|
||||
New York Summer | 2025-06-21 | 05:25:03 | 05:25:03 | 12:57:56 | 12:57:56 | 20:30:35 | 20:30:35
|
||||
New York Winter | 2025-12-21 | 07:16:41 | 07:16:41 | 11:54:19 | 11:54:19 | 16:31:56 | 16:31:56
|
||||
London Summer | 2025-06-21 | 04:43:07 | 04:43:07 | 13:02:22 | 13:02:22 | 21:21:37 | 21:21:37
|
||||
London Winter | 2025-12-21 | 08:03:52 | 08:03:52 | 11:58:42 | 11:58:42 | 15:53:32 | 15:53:32
|
||||
Tokyo Summer | 2025-06-21 | 04:25:52 | 04:25:52 | 11:43:00 | 11:43:00 | 19:00:22 | 19:00:22
|
||||
Sydney Winter | 2025-06-21 | 07:00:12 | 07:00:12 | 11:56:56 | 11:56:56 | 16:53:52 | 16:53:52
|
||||
Reykjavik Mids | 2025-06-21 | 02:55:10 | 02:55:10 | 13:29:38 | 13:29:38 | 00:03:54 | 00:03:54
|
||||
Cape Town Summer | 2025-12-21 | 05:31:55 | 05:31:55 | 12:44:28 | 12:44:28 | 19:57:01 | 19:57:01
|
||||
Quito Equinox | 2025-03-20 | 06:17:54 | 06:17:54 | 12:21:10 | 12:21:10 | 18:24:25 | 18:24:25
|
||||
Tromso Polar | 2025-12-21 | N/A | N/A | N/A | N/A | N/A | N/A
|
||||
```
|
||||
88
bin/test.js
Normal file
88
bin/test.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// bin/test.js
|
||||
// Run 10 diverse test cases through both your JS port and the C reference (spa_cli) and compare.
|
||||
|
||||
'use strict';
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const { getSpa } = require('../index');
|
||||
|
||||
// Constants for C invocation
|
||||
const DELTA_UT1 = 0.0;
|
||||
const DELTA_T = 67.0;
|
||||
const SLOPE = 0.0;
|
||||
const AZM_ROT = 0.0;
|
||||
const ATMOS_REF = 0.5667;
|
||||
|
||||
// Helper to format fractional hours into "HH:MM:SS" or "N/A"
|
||||
function formatJS(hour) {
|
||||
if (typeof hour !== 'number' || isNaN(hour) || hour < 0 || hour >= 24) {
|
||||
return 'N/A';
|
||||
}
|
||||
const total = Math.round(hour * 3600);
|
||||
const H = Math.floor(total / 3600);
|
||||
const M = Math.floor((total % 3600) / 60);
|
||||
const S = total % 60;
|
||||
return [H, M, S]
|
||||
.map(v => v.toString().padStart(2, '0'))
|
||||
.join(':');
|
||||
}
|
||||
|
||||
// Ten diverse test cases
|
||||
const cases = [
|
||||
{ label: 'New York Summer', dateUTC: new Date(Date.UTC(2025, 5, 21, 0,0,0)), lat: 40.7128, lon: -74.0060, tz: -4, elevation: 10, pressure: 1013, temperature: 20 },
|
||||
{ label: 'New York Winter', dateUTC: new Date(Date.UTC(2025,11,21, 0,0,0)), lat: 40.7128, lon: -74.0060, tz: -5, elevation: 10, pressure: 1013, temperature: 5 },
|
||||
{ label: 'London Summer', dateUTC: new Date(Date.UTC(2025, 5, 21, 0,0,0)), lat: 51.5074, lon: -0.1278, tz: 1, elevation: 11, pressure: 1013, temperature: 18 },
|
||||
{ label: 'London Winter', dateUTC: new Date(Date.UTC(2025,11,21, 0,0,0)), lat: 51.5074, lon: -0.1278, tz: 0, elevation: 11, pressure: 1013, temperature: 7 },
|
||||
{ label: 'Tokyo Summer', dateUTC: new Date(Date.UTC(2025, 5, 21, 0,0,0)), lat: 35.6895, lon: 139.6917, tz: 9, elevation: 40, pressure: 1013, temperature: 22 },
|
||||
{ label: 'Sydney Winter', dateUTC: new Date(Date.UTC(2025, 5, 21, 0,0,0)), lat: -33.8688,lon: 151.2093, tz: 10, elevation: 58, pressure: 1013, temperature: 15 },
|
||||
{ label: 'Reykjavik Mids', dateUTC: new Date(Date.UTC(2025, 5, 21, 0,0,0)), lat: 64.1466, lon: -21.9426,tz: 0, elevation: 0, pressure: 1013, temperature: 10 },
|
||||
{ label: 'Cape Town Summer', dateUTC: new Date(Date.UTC(2025,11,21, 0,0,0)), lat: -33.9249,lon: 18.4241, tz: 2, elevation: 25, pressure: 1013, temperature: 18 },
|
||||
{ label: 'Quito Equinox', dateUTC: new Date(Date.UTC(2025, 2, 20, 0,0,0)), lat: -0.1807, lon: -78.4678,tz: -5, elevation:2850, pressure: 789, temperature: 14 },
|
||||
{ label: 'Tromso Polar', dateUTC: new Date(Date.UTC(2025,11,21, 0,0,0)), lat: 69.6492, lon: 18.9553,tz: 1, elevation: 0, pressure: 1013, temperature: -2 }
|
||||
];
|
||||
|
||||
// Print header
|
||||
console.log(
|
||||
'Location | Date | C Rise | JS Rise | C Noon | JS Noon | C Set | JS Set'
|
||||
);
|
||||
console.log('-'.repeat(83));
|
||||
|
||||
cases.forEach(({ label, dateUTC, lat, lon, tz, elevation, pressure, temperature }) => {
|
||||
const dateStr = dateUTC.toISOString().slice(0,10);
|
||||
|
||||
// JS calculation
|
||||
const jsResult = getSpa(dateUTC, lat, lon, tz, { elevation, pressure, temperature });
|
||||
const jsRise = formatJS(jsResult.sunrise);
|
||||
const jsNoon = formatJS(jsResult.solarNoon);
|
||||
const jsSet = formatJS(jsResult.sunset);
|
||||
|
||||
// C reference via spa_cli
|
||||
const cli = path.join(__dirname, 'spa_cli');
|
||||
const args = [
|
||||
dateUTC.getUTCFullYear(), dateUTC.getUTCMonth()+1, dateUTC.getUTCDate(),
|
||||
dateUTC.getUTCHours(), dateUTC.getUTCMinutes(), dateUTC.getUTCSeconds(),
|
||||
DELTA_UT1, DELTA_T, tz,
|
||||
lon, lat,
|
||||
elevation, pressure, temperature,
|
||||
SLOPE, AZM_ROT, ATMOS_REF
|
||||
].map(String);
|
||||
|
||||
const stdout = spawnSync(cli, args, { encoding: 'utf8' }).stdout || '';
|
||||
let cRise = 'N/A', cNoon = 'N/A', cSet = 'N/A';
|
||||
stdout.split(/\r?\n/).forEach(line => {
|
||||
const match = line.match(/(\d{2}:\d{2}:\d{2})/);
|
||||
if (match) {
|
||||
const key = line.toLowerCase();
|
||||
if (key.includes('sunrise')) cRise = match[1];
|
||||
else if (key.includes('solar noon')) cNoon = match[1];
|
||||
else if (key.includes('sunset')) cSet = match[1];
|
||||
}
|
||||
});
|
||||
|
||||
// Print row
|
||||
console.log(
|
||||
`${label.padEnd(16)} | ${dateStr} | ${cRise.padEnd(8)} | ${jsRise.padEnd(8)} | ` +
|
||||
`${cNoon.padEnd(8)} | ${jsNoon.padEnd(8)} | ${cSet.padEnd(8)} | ${jsSet}`
|
||||
);
|
||||
});
|
||||
175
index.js
175
index.js
|
|
@ -1,89 +1,118 @@
|
|||
// index.js
|
||||
const spa = require('./dist/spa');
|
||||
|
||||
/**
|
||||
* Convert fractional hours to HH:MM:SS.mmm (rounding total seconds)
|
||||
*/
|
||||
function fractalTime(fractionalHour) {
|
||||
const hours = Math.floor(fractionalHour);
|
||||
const minutes = Math.floor((fractionalHour - hours) * 60);
|
||||
const seconds = Math.floor((fractionalHour * 3600) - (hours * 3600) - (minutes * 60));
|
||||
const ms = Math.floor((fractionalHour * 3600000) - (hours * 3600000) - (minutes * 60000) - (seconds * 1000));
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
|
||||
const totalSec = Math.round(fractionalHour * 3600);
|
||||
const H = Math.floor(totalSec / 3600);
|
||||
const rem = totalSec - H * 3600;
|
||||
const M = Math.floor(rem / 60);
|
||||
const S = rem - M * 60;
|
||||
const ms = Math.round((fractionalHour * 3600 - Math.floor(fractionalHour * 3600)) * 1000);
|
||||
return `${H.toString().padStart(2,'0')}:` +
|
||||
`${M.toString().padStart(2,'0')}:` +
|
||||
`${S.toString().padStart(2,'0')}.` +
|
||||
`${ms.toString().padStart(3,'0')}`;
|
||||
}
|
||||
|
||||
function adjustForCustomAngle(baseSpaData, zenithAngle) {
|
||||
let adjustedData = { ...baseSpaData };
|
||||
const standardZenith = 90.83;
|
||||
const angleDifference = zenithAngle - standardZenith;
|
||||
const timeAdjustment = angleDifference / 360 * 24;
|
||||
adjustedData.sunrise -= timeAdjustment;
|
||||
adjustedData.sunset += timeAdjustment;
|
||||
return adjustedData;
|
||||
/**
|
||||
* Re-solve hour-angle for a custom zenith angle Zdeg (in degrees)
|
||||
*/
|
||||
function adjustForCustomAngle(base, Zdeg) {
|
||||
const φ = base.latitude * Math.PI/180;
|
||||
const δ = base.delta * Math.PI/180;
|
||||
const Z = Zdeg * Math.PI/180;
|
||||
const cosH0 = (Math.cos(Z) - Math.sin(φ) * Math.sin(δ)) /
|
||||
(Math.cos(φ) * Math.cos(δ));
|
||||
if (cosH0 < -1 || cosH0 > 1) {
|
||||
return { ...base, sunrise: NaN, sunset: NaN };
|
||||
}
|
||||
const H0h = (Math.acos(cosH0) * 180/Math.PI) / 15;
|
||||
return {
|
||||
...base,
|
||||
sunrise: base.suntransit - H0h,
|
||||
sunset: base.suntransit + H0h
|
||||
};
|
||||
}
|
||||
|
||||
function getSpa(date, lat, lng, tz = 0, params = null, angles = []) {
|
||||
let data = new spa.SpaData();
|
||||
data.year = date.getFullYear();
|
||||
data.month = date.getMonth() + 1; // JavaScript months are 0-indexed
|
||||
data.day = date.getDate();
|
||||
data.hour = date.getHours();
|
||||
data.minute = date.getMinutes();
|
||||
data.second = date.getSeconds();
|
||||
data.longitude = lng;
|
||||
data.latitude = lat;
|
||||
data.timezone = tz;
|
||||
/**
|
||||
* Core SPA data calculation (raw fractional hours)
|
||||
* @param {Date} date - JavaScript Date (UTC)
|
||||
* @param {number} lat
|
||||
* @param {number} lng
|
||||
* @param {number} tz - timezone offset in hours (e.g. -4 for EDT)
|
||||
* @param {object} params - { elevation, pressure, temperature, delta_ut1, delta_t, slope, azm_rotation, atmos_refract }
|
||||
* @param {number[]} angles - custom zenith angles (deg) for twilight
|
||||
*/
|
||||
function getSpa(date, lat, lng, tz = 0, params = {}, angles = []) {
|
||||
const d = new spa.SpaData();
|
||||
// Use UTC components and explicit tz
|
||||
d.year = date.getUTCFullYear();
|
||||
d.month = date.getUTCMonth() + 1;
|
||||
d.day = date.getUTCDate();
|
||||
d.hour = date.getUTCHours();
|
||||
d.minute = date.getUTCMinutes();
|
||||
d.second = date.getUTCSeconds();
|
||||
d.longitude = lng;
|
||||
d.latitude = lat;
|
||||
d.timezone = tz;
|
||||
|
||||
// Set default values if optional parameters are not provided
|
||||
data.elevation = params?.elevation ?? 50;
|
||||
data.pressure = params?.pressure ?? 1013.25;
|
||||
data.temperature = params?.temperature ?? 15;
|
||||
data.function = spa.SPA_ALL;
|
||||
// Align defaults to reference C code
|
||||
d.elevation = params.elevation ?? 0;
|
||||
d.pressure = params.pressure ?? 1013;
|
||||
d.temperature = params.temperature ?? 15;
|
||||
d.delta_ut1 = params.delta_ut1 ?? 0;
|
||||
d.delta_t = params.delta_t ?? 67;
|
||||
d.slope = params.slope ?? 0;
|
||||
d.azm_rotation = params.azm_rotation ?? 0;
|
||||
d.atmos_refract= params.atmos_refract?? 0.5667;
|
||||
|
||||
let result = spa.spa_calculate(data);
|
||||
let output = {};
|
||||
// Only compute ZA and rise/transit/set
|
||||
d.function = spa.SPA_ZA_RTS;
|
||||
|
||||
if (result === 0) {
|
||||
output = {
|
||||
zenith: data.zenith,
|
||||
azimuth: data.azimuth,
|
||||
sunrise: data.sunrise,
|
||||
solarNoon: data.suntransit,
|
||||
sunset: data.sunset
|
||||
};
|
||||
const rc = spa.spa_calculate(d);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`SPA calculation failed with code ${rc}`);
|
||||
}
|
||||
|
||||
if (angles.length > 0) {
|
||||
output.angles = angles.map(angle => {
|
||||
let customSpaData = adjustForCustomAngle({ ...data }, angle);
|
||||
return {
|
||||
sunrise: customSpaData.sunrise,
|
||||
sunset: customSpaData.sunset
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('SPA Calculation failed');
|
||||
}
|
||||
// Base outputs
|
||||
const output = {
|
||||
zenith: d.zenith,
|
||||
azimuth: d.azimuth,
|
||||
sunrise: d.sunrise,
|
||||
solarNoon: d.suntransit,
|
||||
sunset: d.sunset
|
||||
};
|
||||
|
||||
return output;
|
||||
// Custom angles (twilight)
|
||||
if (angles.length) {
|
||||
output.angles = angles.map(Z => {
|
||||
const c = adjustForCustomAngle(d, Z);
|
||||
return { sunrise: c.sunrise, sunset: c.sunset };
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function calcSpa(date, lat, lng, tz = 0, params = null, angles = []) {
|
||||
let rawData = getSpa(date, lat, lng, tz, params, angles);
|
||||
rawData.sunrise = fractalTime(rawData.sunrise);
|
||||
rawData.solarNoon = fractalTime(rawData.solarNoon);
|
||||
rawData.sunset = fractalTime(rawData.sunset);
|
||||
|
||||
if (rawData.angles) {
|
||||
rawData.angles = rawData.angles.map(angleData => {
|
||||
return {
|
||||
sunrise: fractalTime(angleData.sunrise),
|
||||
sunset: fractalTime(angleData.sunset)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return rawData;
|
||||
/**
|
||||
* Same as getSpa, but formats sunrise/noon/sunset to strings
|
||||
*/
|
||||
function calcSpa(date, lat, lng, tz = 0, params = {}, angles = []) {
|
||||
const raw = getSpa(date, lat, lng, tz, params, angles);
|
||||
return {
|
||||
zenith: raw.zenith,
|
||||
azimuth: raw.azimuth,
|
||||
sunrise: fractalTime(raw.sunrise),
|
||||
solarNoon: fractalTime(raw.solarNoon),
|
||||
sunset: fractalTime(raw.sunset),
|
||||
angles: raw.angles ? raw.angles.map(a => ({
|
||||
sunrise: fractalTime(a.sunrise),
|
||||
sunset: fractalTime(a.sunset)
|
||||
})) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSpa,
|
||||
calcSpa,
|
||||
fractalTime
|
||||
};
|
||||
module.exports = { getSpa, calcSpa, fractalTime, adjustForCustomAngle };
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "nrel-spa",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"description": "NREL SPA native implementation in JS",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
55
test.js
55
test.js
|
|
@ -1,34 +1,41 @@
|
|||
// tested.js
|
||||
const { getSpa, calcSpa } = require('./index');
|
||||
|
||||
// Use current date/time
|
||||
const date = new Date();
|
||||
console.log(date)
|
||||
console.log(`Current Date: ${date.toString()}\n`);
|
||||
|
||||
/* NYC - minimum params
|
||||
const city = "New York"
|
||||
/*
|
||||
// Example: New York with minimum params
|
||||
const city = "New York";
|
||||
const lat = 40.7128;
|
||||
const lng = -74.006;
|
||||
const tz = -5;
|
||||
const params = null
|
||||
const angles = []
|
||||
const lng = -74.0060;
|
||||
const tz = -5; // Eastern Standard Time
|
||||
const params = null;
|
||||
const angles = [];
|
||||
*/
|
||||
|
||||
// Jakarta - all params
|
||||
const city = "Jakarta"
|
||||
const lat = -6.2088
|
||||
const lng = 106.8456
|
||||
const tz = 0
|
||||
const elevation = 18
|
||||
const temperature = 26.56
|
||||
const pressure = 1017
|
||||
const params = {elevation, temperature, pressure}
|
||||
const angles = [63.435]
|
||||
// Jakarta with all params
|
||||
const city = "Jakarta";
|
||||
const lat = -6.2088;
|
||||
const lng = 106.8456;
|
||||
const tz = 7; // UTC+7
|
||||
const params = {
|
||||
elevation: 18, // meters
|
||||
temperature: 26.56, // °C
|
||||
pressure: 1017 // mbar
|
||||
};
|
||||
const angles = [63.435]; // example custom zenith angle
|
||||
|
||||
console.log(`Test: ${city} (lat: ${lat}, lng: ${lng}, UTC${tz >= 0 ? '+' : ''}${tz})\n`);
|
||||
|
||||
// Get results
|
||||
const get = getSpa(date, lat, lng, tz, params, angles);
|
||||
const calc = calcSpa(date, lat, lng, tz, params, angles);
|
||||
// Raw fractional outputs
|
||||
const raw = getSpa(date, lat, lng, tz, params, angles);
|
||||
// Formatted HH:MM:SS outputs
|
||||
const formatted = calcSpa(date, lat, lng, tz, params, angles);
|
||||
|
||||
// Print results
|
||||
console.log(`\nTest: ${city} with current Date():\n`)
|
||||
console.log("getSpa =", get, "\n");
|
||||
console.log("calcSpa =", calc, "\n");
|
||||
console.log('getSpa (raw fractional values):');
|
||||
console.log(JSON.stringify(raw, null, 2), '\n');
|
||||
|
||||
console.log('calcSpa (formatted HH:MM:SS):');
|
||||
console.log(JSON.stringify(formatted, null, 2), '\n');
|
||||
Loading…
Reference in a new issue