@@ -320,3 +320,118 @@ def test_active_patches_count(self):
320320
321321 # Only one patch should be active
322322 assert result ['n_active_patches' ] == 1
323+
324+
325+ import pandas as pd
326+ from voxcity .simulator_gpu .solar .integration .utils import filter_df_to_period
327+
328+
329+ def _year_df ():
330+ idx = pd .date_range ("2000-01-01 00:00:00" , "2000-12-31 23:00:00" , freq = "h" )
331+ return pd .DataFrame ({"DNI" : 1.0 , "DHI" : 1.0 }, index = idx )
332+
333+
334+ def test_filter_df_daily_hour_window_crosses_date_range ():
335+ df = _year_df ()
336+ out = filter_df_to_period (
337+ df , "07-01 00:00:00" , "08-31 23:00:00" , tz = 0.0 ,
338+ daily_start_hour = 6 , daily_end_hour = 11 ,
339+ )
340+ hours = out .index .hour .unique ().tolist ()
341+ assert min (hours ) >= 6 and max (hours ) <= 11
342+ months = out .index .month .unique ().tolist ()
343+ assert set (months ) == {7 , 8 }
344+
345+
346+ def test_filter_df_daily_hour_wraparound ():
347+ df = _year_df ()
348+ out = filter_df_to_period (
349+ df , "07-01 00:00:00" , "07-02 23:00:00" , tz = 0.0 ,
350+ daily_start_hour = 22 , daily_end_hour = 3 ,
351+ )
352+ assert set (out .index .hour .unique ().tolist ()) <= {22 , 23 , 0 , 1 , 2 , 3 }
353+
354+
355+ def test_filter_df_none_daily_hours_unchanged ():
356+ df = _year_df ()
357+ out = filter_df_to_period (df , "07-01 06:00:00" , "07-01 11:00:00" , tz = 0.0 )
358+ # Continuous span: all 6 hours present, no daily narrowing.
359+ assert len (out ) == 6
360+
361+
362+ from unittest .mock import patch
363+
364+
365+ def test_cumulative_global_forwards_daily_hours ():
366+ """Ground cumulative entry point must forward daily hour kwargs to filter_df_to_period."""
367+ import voxcity .simulator_gpu .solar .integration as integ
368+ # Patch filter_df_to_period as resolved inside the caller's own module.
369+ target_module = integ .get_cumulative_global_solar_irradiance .__module__
370+ with patch (f"{ target_module } .filter_df_to_period" ) as mock_filter :
371+ # Stop execution right after the filter call so no heavy work runs.
372+ mock_filter .side_effect = RuntimeError ("stop after filter" )
373+ try :
374+ integ .get_cumulative_global_solar_irradiance (
375+ voxcity = None , df = _year_df (), lon = 0.0 , lat = 0.0 , tz = 0.0 ,
376+ start_time = "07-01 00:00:00" , end_time = "08-31 23:00:00" ,
377+ daily_start_hour = 6 , daily_end_hour = 11 ,
378+ )
379+ except Exception :
380+ pass
381+ assert mock_filter .called , "filter_df_to_period was not reached"
382+ _ , called_kwargs = mock_filter .call_args
383+ assert called_kwargs .get ("daily_start_hour" ) == 6
384+ assert called_kwargs .get ("daily_end_hour" ) == 11
385+
386+
387+ def test_cumulative_volumetric_applies_daily_hours ():
388+ """Volumetric cumulative entry point filters inline; verify the daily window is applied.
389+
390+ volumetric.get_cumulative_volumetric_solar_irradiance does NOT call
391+ filter_df_to_period (it filters inline), so we prove the daily-hour bounds
392+ reach the filtering by inspecting the timestamps passed to the first
393+ downstream consumer (get_solar_positions_astral).
394+ """
395+ import voxcity .simulator_gpu .solar .integration as integ
396+ mod = integ .get_cumulative_volumetric_solar_irradiance .__module__
397+ captured = {}
398+
399+ def fake_solar_positions (index , lon , lat ):
400+ captured ["hours" ] = sorted (set (index .hour .tolist ()))
401+ raise RuntimeError ("stop after filter" )
402+
403+ with patch (f"{ mod } .get_solar_positions_astral" , side_effect = fake_solar_positions ):
404+ try :
405+ integ .get_cumulative_volumetric_solar_irradiance (
406+ voxcity = None , df = _year_df (), lon = 0.0 , lat = 0.0 , tz = 0.0 ,
407+ start_time = "07-01 00:00:00" , end_time = "08-31 23:00:00" ,
408+ daily_start_hour = 6 , daily_end_hour = 11 ,
409+ )
410+ except Exception :
411+ pass
412+ assert captured .get ("hours" ), "get_solar_positions_astral was not reached"
413+ assert set (captured ["hours" ]) == {6 , 7 , 8 , 9 , 10 , 11 }
414+
415+
416+ def test_building_cumulative_forwards_daily_hours ():
417+ """Building-surface cumulative entry point must forward daily hour kwargs
418+ to filter_df_to_period."""
419+ import voxcity .simulator_gpu .solar .integration .building as bld
420+ with patch .object (bld , "filter_df_to_period" ) as mock_filter :
421+ # Stop execution right after the filter call so no heavy work runs.
422+ mock_filter .side_effect = RuntimeError ("stop after filter" )
423+ try :
424+ bld .get_cumulative_building_solar_irradiance (
425+ voxcity = None ,
426+ building_svf_mesh = None ,
427+ weather_df = _year_df (),
428+ lon = 0.0 , lat = 0.0 , tz = 0.0 ,
429+ period_start = "07-01 00:00:00" , period_end = "08-31 23:00:00" ,
430+ daily_start_hour = 6 , daily_end_hour = 11 ,
431+ )
432+ except Exception :
433+ pass
434+ assert mock_filter .called , "filter_df_to_period was not reached"
435+ _ , called_kwargs = mock_filter .call_args
436+ assert called_kwargs .get ("daily_start_hour" ) == 6
437+ assert called_kwargs .get ("daily_end_hour" ) == 11
0 commit comments