def test_date_to_days(self): days = date_to_days(date(2019, 1, 1)) self.assertEqual(days, 1) days = date_to_days(date(19, 1, 1)) self.assertEqual(days, 1) days = date_to_days(date(19, 12, 31)) self.assertEqual(days, 365) days = date_to_days(date(19, 3, 12)) self.assertEqual(days, 71)
def get_calculation_inputs_between( self, start_date: date, end_date: date, brewer_id, settings: Settings, uvr_file: Optional[str] = None) -> List[CalculationInput]: """ Create inputs for all UV Files found for between a start date and an end date for a given brewer id. :param start_date: the dates' lower bound (inclusive) for the measurements :param end_date: the dates' upper bound (inclusive) for the measurements :param brewer_id: the id of the brewer instrument :param settings: the settings to use for the calculation :param uvr_file: the uvr file to use for the calculation or None to use the default :return: the calculation inputs """ if uvr_file is None and settings.uvr_data_source == DataSource.FILES and brewer_id in self._file_dict: uvr_file = self._file_dict[brewer_id].uvr_files[0].file_name input_list = [] for d in date_range(start_date, end_date): year = d.year - 2000 days = date_to_days(d) LOG.debug("Creating input for date %s as days %d and year %d", d.isoformat(), days, year) calculation_input = self.input_from_files(f"{days:03}", f"{year:02}", brewer_id, settings, uvr_file) if calculation_input is not None: input_list.append(calculation_input) return input_list
def cloud_cover(self) -> CloudCover: position = self.uv_file_entries[0].header.position d = self.uv_file_entries[0].header.date days = date_to_days(d) parameter_value = self.parameters.cloud_cover(days) if parameter_value is not None: return ParameterCloudCover(parameter_value) else: return get_cloud_cover(position.latitude, position.longitude, d)
def _get_single_result_content(self, result: Result) -> str: # Initialize the content that will be returned content = "" minutes = result.uv_file_entry.raw_values[0].time days = date_to_days(result.uv_file_entry.header.date) ozone = result.calculation_input.ozone.interpolated_ozone(minutes, result.calculation_input.settings.default_ozone) albedo = result.calculation_input.parameters.interpolated_albedo(days, result.calculation_input.settings.default_albedo) aerosol = result.calculation_input.parameters.interpolated_aerosol(days, result.calculation_input.settings.default_aerosol) cos_cor_to_apply = result.calculation_input.cos_correction_to_apply(minutes) # If the value comes from Darksky, we add the cloud cover in parenthesis after the coscor type cloud_cover_value = "" if cos_cor_to_apply != CosCorrection.NONE and isinstance(result.calculation_input.cloud_cover, DarkskyCloudCover): cloud_cover_value = f"(darksky:{result.calculation_input.cloud_cover.darksky_value(minutes)})" content += f"% Generated with Brewer UV Irradiance Calculation {APP_VERSION} at {datetime.now().replace(microsecond=0)}\n" content += f"% https://github.com/pec0ra/buvic\n" content += ( f"% {result.uv_file_entry.header.place} {result.uv_file_entry.header.position.latitude}N " f"{result.uv_file_entry.header.position.longitude}W\n" ) straylight_correction = correct_straylight(result.calculation_input.brewer_type) if straylight_correction == StraylightCorrection.UNDEFINED: straylight_correction = result.calculation_input.settings.default_straylight_correction second_line_parts = { "type": result.uv_file_entry.header.type, "coscor": f"{cos_cor_to_apply.value}{cloud_cover_value}", "tempcor": f"{round(result.temperature_correction, 3)}", "straylightcor": straylight_correction.value, "o3": f"{round(float(ozone), 3)}DU", "albedo": str(albedo), "alpha": str(aerosol.alpha), "beta": str(aerosol.beta), "uvr_source": result.calculation_input.calibration.source, } # We join the second line parts like <key>=<value> and separate them with a tabulation (\t) content += "% " + ("\t".join("=".join(_) for _ in second_line_parts.items())) + "\n" content += f"% wavelength(nm) spectral_irradiance(W m-2 nm-1) time_hour_UTC\n" for i in range(len(result.spectrum.wavelengths)): content += ( f"{result.spectrum.wavelengths[i]:.1f}\t " f"{result.spectrum.cos_corrected_spectrum[i] / 1000:.9f}\t " # converted to W m-2 nm-1 f"{result.spectrum.measurement_times[i] / 60:.5f}\n" # converted to hours ) return content
def get_qasume_name(self, prefix: str = "", suffix: str = "") -> str: """ Create a name specific to this result. :param prefix: the prefix to add to the file name :param suffix: the suffix to add to the file name :return: the created file name """ bid = self.calculation_input.brewer_id days = date_to_days(self.calculation_input.date) time = minutes_to_time(self.spectrum.measurement_times[0]) file_name = f"{prefix}{days:03}{time.hour:02}{time.minute:02}G.{bid}{suffix}" return path.join(self.get_relative_path(), file_name)
def get_cloud_cover(latitude: float, longitude: float, d: date) -> CloudCover: if DARKSKY_TOKEN is None: LOG.warning( "DARKSKY_TOKEN environment variable is not defined. Functionality will be deactivated and 'clear_sky' will be used as " "default") warn( f"DARKSKY_TOKEN environment variable is not defined. Functionality is deactivated and 'clear_sky' " f"is used as default for cos correction.") return DefaultCloudCover() t = datetime.combine(d, time(0, 0, 0, 0)).isoformat() url_string = f"https://api.darksky.net/forecast/{DARKSKY_TOKEN}/{latitude},{-longitude},{t}?exclude=minutely,currently,daily&units=si" LOG.debug("Retrieving weather data from %s", url_string) try: req = urllib.request.Request(url_string) with urllib.request.urlopen(req) as url: data = json.loads(url.read().decode()) except HTTPError as e: raise Exception( "Error while trying to access darksky. Please check your configuration and your quota." ) from e # Display a warning if madis isn't in the data sources if "madis" not in data["flags"]["sources"]: LOG.warning( "The data used as weather to choose between clear sky or diffuse correction might be imprecise. The data used came from" "unknown sources: %s", " ".join(data["flags"]["sources"]), ) warn( f"The data used as weather to choose between clear sky or diffuse correction might be imprecise. The data" f" used came from unknown sources: {' '.join(data['flags']['sources'])}" ) # Display a warning if the nearest weather station used is too far away nearest_station_distance = data["flags"]["nearest-station"] if nearest_station_distance > 30: LOG.warning( "The data used as weather to choose between clear sky or diffuse correction might be imprecise.\nThe nearest weather " "station found was %f km away from UV measurement.", nearest_station_distance, ) warn( f"The data used as weather to choose between clear sky or diffuse correction might be imprecise.\n" f"The nearest weather station found was {nearest_station_distance} km away from UV measurement." ) i = 0 times = [] values = [] for hour_data in data["hourly"]["data"]: if "cloudCover" not in hour_data: LOG.warning( "No cloud cover data found for hour %d at date %s (%d). Value will be interpolated.", i, d.isoformat(), date_to_days(d)) i += 1 continue times.append(float(i * 60)) values.append(hour_data["cloudCover"]) i += 1 if 24 > len(values) > 0: warn( f"Cloud cover data not found for some of the hours of this day. Interpolated values might be used." ) if len(values) == 0: LOG.warning( "No cloud cover data found for date %s (%d). Default value will be used", d.isoformat(), date_to_days(d)) warn( f"No cloud cover data found for date {d.isoformat()} ({date_to_days(d)}). Default value is used." ) return DefaultCloudCover() return DarkskyCloudCover(times, values)