falakpy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
falakpy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: falakpy
3
+ Version: 0.1.0
4
+ Summary: Small Package Tool for Islamic Astronomy Related Computation and Visualization.
5
+ Author-email: Muhamad Syazwan Faid <syazwaned90@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/msyazwanfaid/falakpy
8
+ Project-URL: Issues, https://github.com/msyazwanfaid/falakpy/issues
9
+ Keywords: Islamic Astronomy,qibla,prayer time,hijri calendar
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: skyfield
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "falakpy"
7
+ version = "0.1.0"
8
+ description = "Small Package Tool for Islamic Astronomy Related Computation and Visualization."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [{ name = "Muhamad Syazwan Faid", email = "syazwaned90@gmail.com" }]
13
+ keywords = ["Islamic Astronomy", "qibla", "prayer time", "hijri calendar"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Topic :: Software Development :: Libraries",
18
+ "Typing :: Typed"
19
+ ]
20
+
21
+ dependencies = ["skyfield"] # keep it stdlib-only for the example
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/msyazwanfaid/falakpy"
25
+ Issues = "https://github.com/msyazwanfaid/falakpy/issues"
26
+
27
+ [tool.setuptools]
28
+ package-dir = {"" = "src"}
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ """falakpy package initializer.
3
+
4
+ Provides modules for:
5
+ - qibla direction computation
6
+ - prayer time calculation
7
+ - lunar crescent observation
8
+ """
9
+
10
+ from . import qibla, prayertime, lunar
11
+
12
+ __all__ = ["qibla", "prayertime", "lunar"]
@@ -0,0 +1,126 @@
1
+ def observedata(lat, lon, ele, tz, year, m, d, csv_filename="crescent_data.csv"):
2
+ from datetime import date, timedelta
3
+ import csv
4
+ from skyfield.api import load, wgs84
5
+ from skyfield import almanac
6
+ from skyfield.units import Angle
7
+ from skyfield.earthlib import refraction
8
+ from numpy import arccos
9
+
10
+ ts = load.timescale()
11
+ eph = load('de440s.bsp')
12
+ earth, sun, moon = eph['earth'], eph['sun'], eph['moon']
13
+
14
+ # Observer (works across Skyfield versions)
15
+ try:
16
+ location = wgs84.latlon(lat, lon, elevation_m=ele, center=earth)
17
+ except TypeError:
18
+ location = earth + wgs84.latlon(lat, lon, elevation_m=ele)
19
+
20
+ # Local-day window in UTC
21
+ t0 = ts.utc(year, m, d, 0 - tz, 0, 0)
22
+ t1 = ts.utc(year, m, d, 24 - tz, 0, 0)
23
+
24
+ # Apparent horizon for upper-limb sunset, with height & refraction
25
+ earth_radius_m = 6378136.6
26
+ side_over_hyp = earth_radius_m / (earth_radius_m + ele)
27
+ h_geom = Angle(radians=-arccos(side_over_hyp)) # dip from observer height
28
+ r = refraction(0.0, temperature_C=15.0, pressure_mbar=1013.25)
29
+ solar_radius_deg = 16/60
30
+ horizon_deg = -r + h_geom.degrees - solar_radius_deg
31
+
32
+ # Sunset within the local-day window
33
+ set_times_sun, _ = almanac.find_settings(location, sun, t0, t1, horizon_degrees=horizon_deg)
34
+ if len(set_times_sun) == 0:
35
+ raise RuntimeError("No sunset found in this local-day window.")
36
+ t_sunset = set_times_sun[0]
37
+ dt_sunset_local = t_sunset.utc_datetime() + timedelta(hours=tz)
38
+ sunset_str = dt_sunset_local.strftime("%H:%M:%S")
39
+
40
+ # Moonset within the local-day window (may be missing)
41
+ set_times_moon, _ = almanac.find_settings(location, moon, t0, t1, horizon_degrees=horizon_deg)
42
+ if len(set_times_moon) == 0:
43
+ t_moonset = None
44
+ moonset_str = "—"
45
+ lag_str = "—"
46
+ else:
47
+ t_moonset = set_times_moon[0]
48
+ dt_moonset_local = t_moonset.utc_datetime() + timedelta(hours=tz)
49
+ moonset_str = dt_moonset_local.strftime("%H:%M:%S")
50
+
51
+ # Lag = moonset - sunset (ensure positive same-evening)
52
+ lag_td = (t_moonset.utc_datetime() - t_sunset.utc_datetime())
53
+ if lag_td.total_seconds() < 0:
54
+ lag_td += timedelta(days=1)
55
+ hh = int(lag_td.total_seconds() // 3600)
56
+ mm = int((lag_td.total_seconds() % 3600) // 60)
57
+ ss = int(lag_td.total_seconds() % 60)
58
+ lag_str = f"{hh:02d}:{mm:02d}:{ss:02d}"
59
+
60
+ # Alt/Az at actual sunset
61
+ alt_moon, az_moon, _ = location.at(t_sunset).observe(moon).apparent().altaz()
62
+ alt_sun, az_sun, _ = location.at(t_sunset).observe(sun).apparent().altaz()
63
+
64
+ moon_alt_deg = float(alt_moon.degrees)
65
+ daz_deg = abs(float(az_moon.degrees) - float(az_sun.degrees))
66
+ arcv_deg = abs(moon_alt_deg - float(alt_sun.degrees)) # Arc of Vision
67
+ arcl_deg = float(
68
+ location.at(t_sunset).observe(sun).apparent()
69
+ .separation_from(location.at(t_sunset).observe(moon).apparent()).degrees
70
+ ) # Arc of Light
71
+
72
+ # Moon age at sunset: (JD_sunset - JD_conjunction_before)*24
73
+ jd_sunset = t_sunset.tt
74
+ # Search ±5 days around date; pick last conjunction BEFORE sunset
75
+ t0c = ts.utc(year, m, d - 5)
76
+ t1c = ts.utc(year, m, d + 5)
77
+ oc_func = almanac.oppositions_conjunctions(eph, eph['Moon'])
78
+ times_oc, events_oc = almanac.find_discrete(t0c, t1c, oc_func)
79
+ jd_conj = None
80
+ for ti, ei in zip(times_oc, events_oc):
81
+ if ei == 1 and ti.tt <= jd_sunset: # 1 = conjunction
82
+ jd_conj = ti.tt
83
+ moon_age_hours = (jd_sunset - jd_conj) * 24.0 if jd_conj is not None else float('nan')
84
+
85
+ # Build row
86
+ date_str = date(year, m, d).strftime("%Y-%m-%d")
87
+ header = ["Date", "Sunset", "Moonset", "Lag Time", "Moon Age", "Moon Alt", "DAZ", "ArcV", "ArcL"]
88
+ row = [
89
+ date_str,
90
+ sunset_str,
91
+ moonset_str,
92
+ lag_str,
93
+ f"{moon_age_hours:.2f}",
94
+ f"{moon_alt_deg:.2f}",
95
+ f"{daz_deg:.2f}",
96
+ f"{arcv_deg:.2f}",
97
+ f"{arcl_deg:.2f}",
98
+ ]
99
+
100
+ # Pretty print
101
+ print("\n+------------+----------+----------+----------+----------+----------+----------+----------+----------+")
102
+ print("| Date | Sunset | Moonset | LagTime | MoonAge | MoonAlt | DAZ | ArcV | ArcL |")
103
+ print("+------------+----------+----------+----------+----------+----------+----------+----------+----------+")
104
+ print(f"| {date_str} | {sunset_str:8} | {moonset_str:8} | {lag_str:8} | "
105
+ f"{moon_age_hours:8.2f} | {moon_alt_deg:8.2f} | {daz_deg:8.2f} | {arcv_deg:8.2f} | {arcl_deg:8.2f} |")
106
+ print("+------------+----------+----------+----------+----------+----------+----------+----------+----------+")
107
+
108
+ # Save CSV
109
+ with open(csv_filename, "w", newline="") as f:
110
+ writer = csv.writer(f)
111
+ writer.writerow(header)
112
+ writer.writerow(row)
113
+
114
+ print(f"\n✅ Saved to CSV file: {csv_filename}")
115
+
116
+ return {
117
+ "Date": date_str,
118
+ "Sunset": sunset_str,
119
+ "Moonset": moonset_str,
120
+ "Lag Time": lag_str,
121
+ "Moon Age (h)": f"{moon_age_hours:.2f}",
122
+ "Moon Alt (deg)": f"{moon_alt_deg:.2f}",
123
+ "DAZ (deg)": f"{daz_deg:.2f}",
124
+ "ArcV (deg)": f"{arcv_deg:.2f}",
125
+ "ArcL (deg)": f"{arcl_deg:.2f}",
126
+ }
@@ -0,0 +1,514 @@
1
+ def subuh(lat,long,ele,tz,y,m,d):
2
+ from datetime import datetime, timedelta, date
3
+ from skyfield.api import load, wgs84
4
+ from skyfield import almanac
5
+ import numpy as np
6
+
7
+ # -------- Settings --------
8
+ lat_location = lat
9
+ long_location = long
10
+ ele = ele
11
+ timezone = tz # UTC+8 (Malaysia)
12
+ year = y
13
+ month = m
14
+ day = d
15
+
16
+ the_day = date(year, month, day)
17
+
18
+ target_alt_deg = -17.99 # e.g., astronomical twilight
19
+ tolerance_alt = 0.01 # ± degrees around target altitude
20
+
21
+
22
+ # -------- Skyfield setup --------
23
+ ts = load.timescale()
24
+ eph = load('de440s.bsp')
25
+ earth, sun = eph['earth'], eph['sun']
26
+ location = earth + wgs84.latlon(lat_location, long_location, elevation_m=ele)
27
+
28
+ def make_altitude_predicate(target_alt_deg: float, tolerance_alt: float):
29
+ """True when Sun altitude is within [target - tol, target + tol]."""
30
+ lo = target_alt_deg - tolerance_alt
31
+ hi = target_alt_deg + tolerance_alt
32
+
33
+ def in_alt_band(t):
34
+ alt, az, _ = location.at(t).observe(sun).apparent().altaz()
35
+ return (alt.degrees >= lo) & (alt.degrees <= hi)
36
+
37
+ # hints for Skyfield’s search
38
+ in_alt_band.step_days = 0.00001
39
+ in_alt_band.rough_period = 0.5 # ≈12 h between crossings
40
+ return in_alt_band
41
+
42
+ pred = make_altitude_predicate(target_alt_deg, tolerance_alt)
43
+
44
+ def local_from_utc(dt_utc):
45
+ return dt_utc + timedelta(hours=timezone)
46
+
47
+ def sun_altitude_intervals_for_day(d: date):
48
+ """Return [(entry_time_utc, exit_time_utc)] for one local day where altitude is in band."""
49
+ t0 = ts.utc(year, month, day, 0 - timezone, 0, 0)
50
+ t1 = ts.utc(year, month, day, 12 - timezone, 0, 0)
51
+
52
+ times, states = almanac.find_discrete(t0, t1, pred)
53
+ intervals = []
54
+
55
+ # if we start inside True state, prepend opening time
56
+ is_true_at_start = bool(pred(t0)) if np.isscalar(pred(t0)) else bool(pred(t0).any())
57
+ if is_true_at_start:
58
+ times = ts.tt_jd(np.insert(times.tt, 0, t0.tt))
59
+ states = np.insert(states, 0, True)
60
+
61
+ # build [entry, exit) pairs for True segments
62
+ for i in range(len(times) - 1):
63
+ if states[i]:
64
+ intervals.append((times[i], times[i + 1]))
65
+
66
+ # if last state continues to t1
67
+ if len(times) > 0 and states[-1]:
68
+ intervals.append((times[-1], t1))
69
+
70
+ return intervals
71
+
72
+ # -------- Run for one day --------
73
+ intervals = sun_altitude_intervals_for_day(the_day)
74
+
75
+ #print(f"Sun altitude windows for {the_day.isoformat()} (local UTC{timezone:+d}), "
76
+ # f"alt ≈ {target_alt_deg}° ±{tolerance_alt}°")
77
+
78
+ if not intervals:
79
+ print(" — none —")
80
+ else:
81
+ for idx, (entry, exit_) in enumerate(intervals, start=1):
82
+ utc_entry = entry.utc_datetime()
83
+ utc_exit = exit_.utc_datetime()
84
+ loc_entry = local_from_utc(utc_entry)
85
+ loc_exit = local_from_utc(utc_exit)
86
+ duration_min = (utc_exit - utc_entry).total_seconds() / 60.0
87
+
88
+ # optional: altitude & azimuth at entry
89
+ alt_e, az_e, _ = location.at(entry).observe(sun).apparent().altaz()
90
+
91
+ # print(f" #{idx}: {loc_entry.strftime('%H:%M:%S')} → {loc_exit.strftime('%H:%M:%S')}"
92
+ # f" | duration: {duration_min:.2f} min"
93
+ # f" | alt_entry: {alt_e.degrees:.2f}° az: {az_e.degrees:.2f}°")
94
+ z=loc_entry.strftime('%H:%M:%S')
95
+ return z
96
+
97
+ def syuruk(lat,long,ele,tz,y,m,d):
98
+ from datetime import datetime, timedelta, date
99
+ from skyfield.api import load, wgs84
100
+ from skyfield import almanac
101
+ import numpy as np
102
+
103
+ # -------- Settings --------
104
+ lat_location = lat
105
+ long_location = long
106
+ ele = ele
107
+ timezone = tz # UTC+8 (Malaysia)
108
+ year = y
109
+ month = m
110
+ day = d
111
+
112
+ ts = load.timescale()
113
+ eph = load('de440s.bsp')
114
+ planets = load('de440s.bsp')
115
+ earth = planets['earth']
116
+ sun = planets['sun']
117
+ location = earth + wgs84.latlon(lat_location, long_location, elevation_m=ele)
118
+
119
+ t0 = ts.utc(year, month, day-1)
120
+ t1 = ts.utc(year, month, day)
121
+
122
+ from skyfield.units import Angle
123
+ from numpy import arccos
124
+ from skyfield.earthlib import refraction
125
+
126
+ altitude_m = ele
127
+ earth_radius_m = 6378136.6
128
+ side_over_hypotenuse = earth_radius_m / (earth_radius_m + altitude_m)
129
+ h = Angle(radians=-arccos(side_over_hypotenuse))
130
+ solar_radius_degrees = 16 / 60
131
+ r = refraction(0.0, temperature_C=15.0, pressure_mbar=1030.0)
132
+
133
+ t, y = almanac.find_risings(location, sun, t0, t1, horizon_degrees=-r + h.degrees - solar_radius_degrees)
134
+ h, m, s = t.utc.hour, t.utc.minute, t.utc.second
135
+ syuruk_time = float(np.array(h).item() + np.array(m).item()/60 + np.array(s).item()/3600 + timezone)
136
+ syuruk_time %= 24 # Ensure 24-hour clock format
137
+
138
+ syuruk_time = float(syuruk_time)
139
+ degrees = int(syuruk_time)
140
+ decimal_part = syuruk_time - degrees
141
+ minutes_total = decimal_part * 60
142
+ minutes = int(minutes_total)
143
+ seconds = round((minutes_total - minutes) * 60)
144
+ sun_astro = location.at(ts.utc(year, month, day, h, m, s)).observe(sun)
145
+ sun_alt, _, _ = sun_astro.apparent().altaz()
146
+ sun_alt, _, _ = sun_astro.apparent().altaz()
147
+ if sun_alt.degrees >= 0:
148
+ syuruk = "Syuruk Does Not Occur"
149
+ else:
150
+ syuruk = f"{degrees}:{minutes}:{seconds}"
151
+
152
+ return syuruk
153
+
154
+ def zuhur(lat,long,ele,tz,y,m,d):
155
+ from datetime import datetime, timedelta, date
156
+ from skyfield.api import load, wgs84
157
+ from skyfield import almanac
158
+ import numpy as np
159
+
160
+ # -------- Settings --------
161
+ lat_location = lat
162
+ long_location = long
163
+ ele = ele
164
+ timezone = tz # UTC+8 (Malaysia)
165
+ year = y
166
+ month = m
167
+ day = d
168
+
169
+ ts = load.timescale()
170
+ eph = load('de440s.bsp')
171
+ planets = load('de440s.bsp')
172
+ earth = planets['earth']
173
+ sun = planets['sun']
174
+ location = earth + wgs84.latlon(lat_location, long_location, elevation_m=ele)
175
+
176
+ t0 = ts.utc(year, month, day-1)
177
+ t1 = ts.utc(year, month, day)
178
+
179
+ from skyfield.units import Angle
180
+ from numpy import arccos
181
+ from skyfield.earthlib import refraction
182
+
183
+ t = almanac.find_transits(location, sun, t0, t1)
184
+ h = t.utc.hour
185
+ m = t.utc.minute
186
+ s = t.utc.second
187
+
188
+ #print(hour_solar_transit)
189
+ #print(minutes_solar_transit )
190
+ #print(second_solar_transit)
191
+ #zuhur_time = hour_solar_transit + (minutes_solar_transit / 60) + (second_solar_transit / 3600 ) + timezone + 0.017778
192
+ zuhur_time = float(np.array(h).item() + np.array(m).item()/60 + np.array(s).item()/3600 + timezone+ 0.017778)
193
+
194
+
195
+ zuhur_time = float(zuhur_time)
196
+ degrees = int(zuhur_time )
197
+ decimal_part = zuhur_time - degrees
198
+ minutes_total = decimal_part * 60
199
+ minutes = int(minutes_total)
200
+
201
+ seconds = round((minutes_total - minutes) * 60)
202
+ #print(f"{degrees}° {minutes}′ {seconds}″")
203
+
204
+ sun_astro = location.at(ts.utc(year, month, day, h, m, s)).observe(sun)
205
+ sun_alt, _, _ = sun_astro.apparent().altaz()
206
+ alt_deg = float(np.atleast_1d(sun_alt.degrees)[0]) # <- force scalar
207
+
208
+ # Check if the sun is above the horizon at zuhur time
209
+ if sun_alt.degrees <= 0:
210
+ zuhur = "Zuhur Does Not Occur"
211
+ else:
212
+ zuhur = f"{degrees}:{minutes}:{seconds}"
213
+ altitude_zuhur = alt_deg
214
+
215
+ return zuhur,altitude_zuhur
216
+
217
+ def asar(lat,long,ele,tz,y,m,d):
218
+ from datetime import datetime, timedelta, date
219
+ from skyfield.api import load, wgs84
220
+ from skyfield import almanac
221
+ import numpy as np
222
+ import math
223
+
224
+ # -------- Settings --------
225
+ lat_location = lat
226
+ long_location = long
227
+ ele = ele
228
+ timezone = tz # UTC+8 (Malaysia)
229
+ year = y
230
+ month = m
231
+ day = d
232
+
233
+ the_day = date(year, month, day)
234
+
235
+ _, altitude_zuhur = zuhur(lat,long,ele,tz,y,m,d)
236
+
237
+ # Noon shadow and Asar target altitude
238
+ s0 = 1.0 / math.tan(math.radians(altitude_zuhur))
239
+ h_asar = math.degrees(math.atan(1.0 / (1.0 + s0)))
240
+
241
+ target_alt_deg = h_asar # angle asar
242
+ tolerance_alt = 0.01 # ± degrees around target altitude
243
+
244
+
245
+ # -------- Skyfield setup --------
246
+ ts = load.timescale()
247
+ eph = load('de440s.bsp')
248
+ earth, sun = eph['earth'], eph['sun']
249
+ location = earth + wgs84.latlon(lat_location, long_location, elevation_m=ele)
250
+
251
+ def make_altitude_predicate(target_alt_deg: float, tolerance_alt: float):
252
+ """True when Sun altitude is within [target - tol, target + tol]."""
253
+ lo = target_alt_deg - tolerance_alt
254
+ hi = target_alt_deg + tolerance_alt
255
+
256
+ def in_alt_band(t):
257
+ alt, az, _ = location.at(t).observe(sun).apparent().altaz()
258
+ return (alt.degrees >= lo) & (alt.degrees <= hi)
259
+
260
+ # hints for Skyfield’s search
261
+ in_alt_band.step_days = 0.00001
262
+ in_alt_band.rough_period = 0.5 # ≈12 h between crossings
263
+ return in_alt_band
264
+
265
+ pred = make_altitude_predicate(target_alt_deg, tolerance_alt)
266
+
267
+ def local_from_utc(dt_utc):
268
+ return dt_utc + timedelta(hours=timezone)
269
+
270
+ def sun_altitude_intervals_for_day(d: date):
271
+ """Return [(entry_time_utc, exit_time_utc)] for one local day where altitude is in band."""
272
+ t0 = ts.utc(year, month, day, 12 - timezone, 0, 0)
273
+ t1 = ts.utc(year, month, day, 24 - timezone, 0, 0)
274
+
275
+ times, states = almanac.find_discrete(t0, t1, pred)
276
+ intervals = []
277
+
278
+ # if we start inside True state, prepend opening time
279
+ is_true_at_start = bool(pred(t0)) if np.isscalar(pred(t0)) else bool(pred(t0).any())
280
+ if is_true_at_start:
281
+ times = ts.tt_jd(np.insert(times.tt, 0, t0.tt))
282
+ states = np.insert(states, 0, True)
283
+
284
+ # build [entry, exit) pairs for True segments
285
+ for i in range(len(times) - 1):
286
+ if states[i]:
287
+ intervals.append((times[i], times[i + 1]))
288
+
289
+ # if last state continues to t1
290
+ if len(times) > 0 and states[-1]:
291
+ intervals.append((times[-1], t1))
292
+
293
+ return intervals
294
+
295
+ # -------- Run for one day --------
296
+ intervals = sun_altitude_intervals_for_day(the_day)
297
+
298
+ #print(f"Sun altitude windows for {the_day.isoformat()} (local UTC{timezone:+d}), "
299
+ # f"alt ≈ {target_alt_deg}° ±{tolerance_alt}°")
300
+
301
+ if not intervals:
302
+ print(" — none —")
303
+ else:
304
+ for idx, (entry, exit_) in enumerate(intervals, start=1):
305
+ utc_entry = entry.utc_datetime()
306
+ utc_exit = exit_.utc_datetime()
307
+ loc_entry = local_from_utc(utc_entry)
308
+ loc_exit = local_from_utc(utc_exit)
309
+ duration_min = (utc_exit - utc_entry).total_seconds() / 60.0
310
+
311
+ # optional: altitude & azimuth at entry
312
+ alt_e, az_e, _ = location.at(entry).observe(sun).apparent().altaz()
313
+
314
+ # print(f" #{idx}: {loc_entry.strftime('%H:%M:%S')} → {loc_exit.strftime('%H:%M:%S')}"
315
+ # f" | duration: {duration_min:.2f} min"
316
+ # f" | alt_entry: {alt_e.degrees:.2f}° az: {az_e.degrees:.2f}°")
317
+ z=loc_entry.strftime('%H:%M:%S')
318
+ return z
319
+
320
+ def maghrib(lat,long,ele,tz,y,m,d):
321
+ from datetime import datetime, timedelta, date
322
+ from skyfield.api import load, wgs84
323
+ from skyfield import almanac
324
+ import numpy as np
325
+
326
+ # -------- Settings --------
327
+ lat_location = lat
328
+ long_location = long
329
+ ele = ele
330
+ timezone = tz # UTC+8 (Malaysia)
331
+ year = y
332
+ month = m
333
+ day = d
334
+
335
+ ts = load.timescale()
336
+ eph = load('de440s.bsp')
337
+ planets = load('de440s.bsp')
338
+ earth = planets['earth']
339
+ sun = planets['sun']
340
+ location = earth + wgs84.latlon(lat_location, long_location, elevation_m=ele)
341
+
342
+ t0 = ts.utc(year, month, day-1)
343
+ t1 = ts.utc(year, month, day)
344
+
345
+ from skyfield.units import Angle
346
+ from numpy import arccos
347
+ from skyfield.earthlib import refraction
348
+
349
+ altitude_m = ele
350
+ earth_radius_m = 6378136.6
351
+ side_over_hypotenuse = earth_radius_m / (earth_radius_m + altitude_m)
352
+ h = Angle(radians=-arccos(side_over_hypotenuse))
353
+ solar_radius_degrees = 16 / 60
354
+ r = refraction(0.0, temperature_C=15.0, pressure_mbar=1030.0)
355
+
356
+ t, y = almanac.find_settings(location, sun, t0, t1, horizon_degrees=-r + h.degrees - solar_radius_degrees)
357
+ h, m, s = t.utc.hour, t.utc.minute, t.utc.second
358
+ maghrib_time = float(np.array(h).item() + np.array(m).item()/60 + np.array(s).item()/3600 + timezone)
359
+ maghrib_time %= 24 # Ensure 24-hour clock format
360
+
361
+ maghrib_time = float(maghrib_time)
362
+ degrees = int(maghrib_time)
363
+ decimal_part = maghrib_time - degrees
364
+ minutes_total = decimal_part * 60
365
+ minutes = int(minutes_total)
366
+ seconds = round((minutes_total - minutes) * 60)
367
+ sun_astro = location.at(ts.utc(year, month, day, h, m, s)).observe(sun)
368
+ sun_alt, _, _ = sun_astro.apparent().altaz()
369
+ sun_alt, _, _ = sun_astro.apparent().altaz()
370
+ if sun_alt.degrees >= 0:
371
+ maghrib = "Maghrib Does Not Occur"
372
+ else:
373
+ maghrib = f"{degrees}:{minutes}:{seconds}"
374
+
375
+ return maghrib
376
+
377
+ def isyak(lat,long,ele,tz,y,m,d):
378
+ from datetime import datetime, timedelta, date
379
+ from skyfield.api import load, wgs84
380
+ from skyfield import almanac
381
+ import numpy as np
382
+
383
+ # -------- Settings --------
384
+ lat_location = lat
385
+ long_location = long
386
+ ele = ele
387
+ timezone = tz # UTC+8 (Malaysia)
388
+ year = y
389
+ month = m
390
+ day = d
391
+
392
+ the_day = date(year, month, day)
393
+
394
+ target_alt_deg = -17.99 # e.g., astronomical twilight
395
+ tolerance_alt = 0.01 # ± degrees around target altitude
396
+
397
+
398
+ # -------- Skyfield setup --------
399
+ ts = load.timescale()
400
+ eph = load('de440s.bsp')
401
+ earth, sun = eph['earth'], eph['sun']
402
+ location = earth + wgs84.latlon(lat_location, long_location, elevation_m=ele)
403
+
404
+ def make_altitude_predicate(target_alt_deg: float, tolerance_alt: float):
405
+ """True when Sun altitude is within [target - tol, target + tol]."""
406
+ lo = target_alt_deg - tolerance_alt
407
+ hi = target_alt_deg + tolerance_alt
408
+
409
+ def in_alt_band(t):
410
+ alt, az, _ = location.at(t).observe(sun).apparent().altaz()
411
+ return (alt.degrees >= lo) & (alt.degrees <= hi)
412
+
413
+ # hints for Skyfield’s search
414
+ in_alt_band.step_days = 0.00001
415
+ in_alt_band.rough_period = 0.5 # ≈12 h between crossings
416
+ return in_alt_band
417
+
418
+ pred = make_altitude_predicate(target_alt_deg, tolerance_alt)
419
+
420
+ def local_from_utc(dt_utc):
421
+ return dt_utc + timedelta(hours=timezone)
422
+
423
+ def sun_altitude_intervals_for_day(d: date):
424
+ """Return [(entry_time_utc, exit_time_utc)] for one local day where altitude is in band."""
425
+ t0 = ts.utc(year, month, day, 12 - timezone, 0, 0)
426
+ t1 = ts.utc(year, month, day, 24 - timezone, 0, 0)
427
+
428
+ times, states = almanac.find_discrete(t0, t1, pred)
429
+ intervals = []
430
+
431
+ # if we start inside True state, prepend opening time
432
+ is_true_at_start = bool(pred(t0)) if np.isscalar(pred(t0)) else bool(pred(t0).any())
433
+ if is_true_at_start:
434
+ times = ts.tt_jd(np.insert(times.tt, 0, t0.tt))
435
+ states = np.insert(states, 0, True)
436
+
437
+ # build [entry, exit) pairs for True segments
438
+ for i in range(len(times) - 1):
439
+ if states[i]:
440
+ intervals.append((times[i], times[i + 1]))
441
+
442
+ # if last state continues to t1
443
+ if len(times) > 0 and states[-1]:
444
+ intervals.append((times[-1], t1))
445
+
446
+ return intervals
447
+
448
+ # -------- Run for one day --------
449
+ intervals = sun_altitude_intervals_for_day(the_day)
450
+
451
+ # print(f"Sun altitude windows for {the_day.isoformat()} (local UTC{timezone:+d}), "
452
+ # f"alt ≈ {target_alt_deg}° ±{tolerance_alt}°")
453
+
454
+ if not intervals:
455
+ print(" — none —")
456
+ else:
457
+ for idx, (entry, exit_) in enumerate(intervals, start=1):
458
+ utc_entry = entry.utc_datetime()
459
+ utc_exit = exit_.utc_datetime()
460
+ loc_entry = local_from_utc(utc_entry)
461
+ loc_exit = local_from_utc(utc_exit)
462
+ duration_min = (utc_exit - utc_entry).total_seconds() / 60.0
463
+
464
+ # optional: altitude & azimuth at entry
465
+ alt_e, az_e, _ = location.at(entry).observe(sun).apparent().altaz()
466
+
467
+ # print(f" #{idx}: {loc_entry.strftime('%H:%M:%S')} → {loc_exit.strftime('%H:%M:%S')}"
468
+ # f" | duration: {duration_min:.2f} min"
469
+ # f" | alt_entry: {alt_e.degrees:.2f}° az: {az_e.degrees:.2f}°")
470
+ z=loc_entry.strftime('%H:%M:%S')
471
+ return z
472
+
473
+
474
+
475
+ def singleday(lat, lon, ele, tz, y, m, d, csv_filename="prayer_times.csv"):
476
+
477
+ import csv
478
+ from datetime import date
479
+ """
480
+ Generate prayer times for one date, print as table, and also save as CSV.
481
+ """
482
+ # --- get each prayer time ---
483
+ subuh_time = subuh(lat, lon, ele, tz, y, m, d)
484
+ syuruk_time = syuruk(lat, lon, ele, tz, y, m, d)
485
+ zuhur_time, _ = zuhur(lat, lon, ele, tz, y, m, d)
486
+ asar_time = asar(lat, lon, ele, tz, y, m, d)
487
+ maghrib_time = maghrib(lat, lon, ele, tz, y, m, d)
488
+ isyak_time = isyak(lat, lon, ele, tz, y, m, d)
489
+
490
+ # --- Prepare data row ---
491
+ date_str = date(y, m, d).strftime("%Y-%m-%d")
492
+ header = ["Date", "Subuh", "Syuruk", "Zuhur", "Asar", "Maghrib", "Isyak"]
493
+ row = [date_str, subuh_time, syuruk_time, zuhur_time, asar_time, maghrib_time, isyak_time]
494
+
495
+ # --- Print table to screen ---
496
+ print("\n+------------+----------+----------+----------+----------+----------+----------+")
497
+ print("| Date | Subuh | Syuruk | Zuhur | Asar | Maghrib | Isyak |")
498
+ print("+------------+----------+----------+----------+----------+----------+----------+")
499
+ print(f"| {date_str} | {subuh_time:8} | {syuruk_time:8} | {zuhur_time:8} | "
500
+ f"{asar_time:8} | {maghrib_time:8} | {isyak_time:8} |")
501
+ print("+------------+----------+----------+----------+----------+----------+----------+")
502
+
503
+ # --- Write to CSV file ---
504
+ with open(csv_filename, mode="w", newline="") as f:
505
+ writer = csv.writer(f)
506
+ writer.writerow(header)
507
+ writer.writerow(row)
508
+
509
+ print(f"\n✅ Saved to CSV file: {csv_filename}")
510
+
511
+
512
+
513
+
514
+
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ """qibla
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1UK-U7P-Ab71HAPQJe0c4NNwdZ0TKaGxG
8
+ """
9
+
10
+ import math
11
+
12
+ from dataclasses import dataclass
13
+
14
+ @dataclass(frozen=True)
15
+ class QiblaDirection:
16
+ decimal: float # decimal degrees, 0..360
17
+ degree: str
18
+
19
+ def direction (lat : float, long: float) -> QiblaDirection:
20
+
21
+
22
+ φ_Location = lat
23
+ λ_Location = long
24
+ φ_Kaabah = 21.4225
25
+ λ_Kaabah = 39.8262
26
+ Difference_Longitude = abs(λ_Location-λ_Kaabah )
27
+
28
+
29
+ #Calculation of Qibla Direction
30
+ import math
31
+
32
+ A = math.sin(math.radians(abs(Difference_Longitude)))
33
+ B = math.cos(math.radians(φ_Location))*math.tan(math.radians(φ_Kaabah))
34
+ C = math.sin(math.radians(φ_Location)) * math.cos(math.radians(Difference_Longitude))
35
+ D = A/(B-C)
36
+ θ = math.degrees(math.atan(D))
37
+
38
+ #Determine the Azimuth of the Qibla
39
+ if Difference_Longitude > 180:
40
+ delta_λ = 360 - Difference_Longitude
41
+ else:
42
+ delta_λ = Difference_Longitude
43
+
44
+ if θ > 0:
45
+ if λ_Location > λ_Kaabah:
46
+ quadrant = "UB" # Utara Barat
47
+ elif λ_Location <= λ_Kaabah:
48
+ quadrant = "UT" # Utara Timur
49
+ elif λ_Location < 0:
50
+ if c >= 180:
51
+ quadrant = "UB"
52
+ else:
53
+ quadrant = "UT"
54
+ elif θ < 0:
55
+ if λ_Location > λ_Kaabah:
56
+ quadrant = "SB" # Selatan Barat
57
+ elif λ_Location <= λ_Kaabah:
58
+ quadrant = "ST" # Selatan Timur
59
+ elif λ_Location < 0:
60
+ if c >= 180:
61
+ quadrant = "SB"
62
+ else:
63
+ quadrant = "ST"
64
+
65
+ if quadrant == "UB":
66
+ azimuth_kiblat = 360 - θ
67
+ elif quadrant == "SB":
68
+ azimuth_kiblat = 180 - θ
69
+ elif quadrant == "UT":
70
+ azimuth_kiblat = θ
71
+ elif quadrant == "ST":
72
+ azimuth_kiblat = 180 + θ
73
+
74
+ # To Convert in Degree Form
75
+
76
+ degrees = int(azimuth_kiblat)
77
+ decimal_part = azimuth_kiblat - degrees
78
+ minutes_total = decimal_part = 60
79
+ minutes = int(minutes_total)
80
+ seconds = round((minutes_total - minutes) * 60)
81
+ #print(f'The azimuth of the Qibla for Location with coordinate {φ_Location} Latitude, {λ_Location} Longitude, is {degrees}° {minutes}′ {seconds}″')
82
+ outputdegree = f"{degrees}° {minutes}′ {seconds}″"
83
+
84
+ return QiblaDirection(decimal=azimuth_kiblat, degree=outputdegree)
85
+
86
+
87
+
88
+
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: falakpy
3
+ Version: 0.1.0
4
+ Summary: Small Package Tool for Islamic Astronomy Related Computation and Visualization.
5
+ Author-email: Muhamad Syazwan Faid <syazwaned90@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/msyazwanfaid/falakpy
8
+ Project-URL: Issues, https://github.com/msyazwanfaid/falakpy/issues
9
+ Keywords: Islamic Astronomy,qibla,prayer time,hijri calendar
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: skyfield
@@ -0,0 +1,10 @@
1
+ pyproject.toml
2
+ src/falakpy/__init__.py
3
+ src/falakpy/lunar.py
4
+ src/falakpy/prayertime.py
5
+ src/falakpy/qibla.py
6
+ src/falakpy.egg-info/PKG-INFO
7
+ src/falakpy.egg-info/SOURCES.txt
8
+ src/falakpy.egg-info/dependency_links.txt
9
+ src/falakpy.egg-info/requires.txt
10
+ src/falakpy.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ skyfield
@@ -0,0 +1 @@
1
+ falakpy