Weighted least-squares calibration of Islamic prayer time depression angles from observed mosque announcement data. Uses golden-section search to minimize the sum of squared residuals independently for Fajr and Isha. Internal Jean Meeus solar ephemeris — zero runtime dependencies. API: calibrateAngles, scoreAngles, predictFajr, predictIsha. Full TypeScript, dual CJS/ESM via tsup. 32 ESM tests, 6 CJS tests, all passing on Node 20/22/24.
3.7 KiB
Guide: Integrating with pray-calc
pray-calc-ml fits angles from data. pray-calc uses those angles to generate prayer times. This guide shows how to connect them.
Installation
npm install pray-calc pray-calc-ml
Step 1: Collect observations
Build a dataset of recorded mosque times. See Guide: Collecting Observations for details.
const observations = [
{ date: new Date('2024-06-01'), lat: 40.71, lng: -74.01, tz: -4, fajr: 3.75, isha: 21.58 },
{ date: new Date('2024-07-01'), lat: 40.71, lng: -74.01, tz: -4, fajr: 3.82, isha: 21.52 },
{ date: new Date('2024-08-01'), lat: 40.71, lng: -74.01, tz: -4, fajr: 4.15, isha: 21.12 },
{ date: new Date('2024-09-01'), lat: 40.71, lng: -74.01, tz: -4, fajr: 4.58, isha: 20.58 },
];
Step 2: Calibrate
import { calibrateAngles } from 'pray-calc-ml';
const result = calibrateAngles(observations);
const { fajrAngle, ishaAngle } = result.angles;
console.log(`Fajr: ${fajrAngle.toFixed(2)}°, Isha: ${ishaAngle.toFixed(2)}°`);
console.log(`RMS error: ${result.rmsMinutes.toFixed(2)} min`);
Step 3: Generate prayer times with pray-calc
Pass the calibrated angles to pray-calc's getTimes() via the angles option:
import { getTimes } from 'pray-calc';
const today = new Date();
const lat = 40.71, lng = -74.01, tz = -4;
const times = getTimes(today, lat, lng, tz, { angles: { fajrAngle, ishaAngle } });
console.log(times.Fajr, times.Sunrise, times.Dhuhr, times.Asr, times.Maghrib, times.Isha);
Step 4: Verify the fit
Before deploying, check the RMS and residuals to confirm the calibration quality:
import { scoreAngles } from 'pray-calc-ml';
// Compare ISNA standard against your observations
const isnaScore = scoreAngles(observations, 15, 15);
console.log(`ISNA RMS: ${isnaScore.rmsMinutes.toFixed(2)} min`);
console.log(`ISNA Fajr bias: ${isnaScore.fajrBiasMinutes.toFixed(1)} min`);
// Compare calibrated angles
const calibScore = scoreAngles(observations, fajrAngle, ishaAngle);
console.log(`Calibrated RMS: ${calibScore.rmsMinutes.toFixed(2)} min`);
If the calibrated RMS is more than 3 minutes, something is off — either the observations are inconsistent, the location coordinates are wrong, or the UTC offset changed mid-dataset.
Caching the calibration
Run calibration once offline and store the resulting angles. There is no need to calibrate at runtime.
// Store in your config or database
const config = {
fajrAngle: 15.2,
ishaAngle: 14.8,
};
// At runtime, just use the stored angles
const times = getTimes(new Date(), lat, lng, tz, { angles: config });
Handling seasonal drift
Depression angles are constants — they do not change with season. The calibration finds a single angle that minimizes error across your entire dataset. If you find that the calibrated angle gives a large error in one season (e.g. winter), you may need more observations from that season to stabilize the fit.
You can also run separate calibrations per season and check consistency:
const winterObs = observations.filter(o => {
const m = o.date.getMonth();
return m === 11 || m <= 1; // Dec, Jan, Feb
});
const summerObs = observations.filter(o => {
const m = o.date.getMonth();
return m >= 5 && m <= 7; // Jun, Jul, Aug
});
const winter = calibrateAngles(winterObs);
const summer = calibrateAngles(summerObs);
const diff = Math.abs(winter.angles.fajrAngle - summer.angles.fajrAngle);
if (diff > 0.5) {
console.warn(`Seasonal inconsistency: ${diff.toFixed(2)}° spread. Check data quality.`);
}
Home | API Reference | Architecture | Guide: Collecting Observations