diff --git a/.gitignore b/.gitignore index ff7726439..aba3fad8d 100755 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ julia_src/licenseserver julia_src/Dockerfile.xpress docker-compose.xpress.yml julia_src/solver_setup.sh +compare_run_*.json diff --git a/.gitmodules b/.gitmodules index e69de29bb..04f5826de 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "EVI-EnLitePy"] + path = EVI-EnLitePy + url = https://github.nrel.gov/AVCI/EVI-EnLitePy.git + branch = server_version \ No newline at end of file diff --git a/.helm/templates/julia-deployment.yaml b/.helm/templates/julia-deployment.yaml index 9f42da480..dea2880a6 100644 --- a/.helm/templates/julia-deployment.yaml +++ b/.helm/templates/julia-deployment.yaml @@ -53,7 +53,7 @@ spec: resources: requests: cpu: {{ .Values.juliaCpuRequest | quote }} - memory: {{ .Values.juliaMemoryRequest | quote }} + memory: "2000Mi" limits: cpu: {{ .Values.juliaCpuLimit | quote }} - memory: {{ .Values.juliaMemoryLimit | quote }} + memory: "2000Mi" diff --git a/.helm/values.production.yaml b/.helm/values.production.yaml index 026003ca6..46abcafb6 100644 --- a/.helm/values.production.yaml +++ b/.helm/values.production.yaml @@ -1,11 +1,11 @@ appEnv: production djangoSettingsModule: reopt_api.production_settings djangoReplicas: 10 +djangoCpuRequest: "1000m" +djangoCpuLimit: "2000m" djangoMemoryRequest: "2000Mi" djangoMemoryLimit: "2000Mi" celeryReplicas: 10 -celeryMemoryRequest: "900Mi" -celeryMemoryLimit: "900Mi" juliaReplicas: 5 juliaCpuRequest: "2000m" juliaCpuLimit: "4000m" diff --git a/.helm/values.staging.yaml b/.helm/values.staging.yaml index 5bc76eb41..d7cfc7107 100644 --- a/.helm/values.staging.yaml +++ b/.helm/values.staging.yaml @@ -1,4 +1,2 @@ appEnv: staging -djangoSettingsModule: reopt_api.staging_settings -juliaMemoryRequest: "8000Mi" -juliaMemoryLimit: "8000Mi" \ No newline at end of file +djangoSettingsModule: reopt_api.staging_settings \ No newline at end of file diff --git a/.helm/values.yaml b/.helm/values.yaml index 7348a0da7..b6dae9237 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -8,18 +8,18 @@ redisDataVolumeStorageClassName: juliaHost: "{{ .Chart.Name }}-julia-service" redisHost: "{{ .Chart.Name }}-redis-service" redisPort: 6379 -djangoReplicas: 2 -djangoCpuRequest: "2000m" -djangoCpuLimit: "4000m" -djangoMemoryRequest: "1600Mi" -djangoMemoryLimit: "1600Mi" -celeryReplicas: 2 +djangoReplicas: 1 +djangoCpuRequest: "500m" +djangoCpuLimit: "2000m" +djangoMemoryRequest: "700Mi" +djangoMemoryLimit: "700Mi" +celeryReplicas: 1 celeryCpuRequest: "100m" -celeryCpuLimit: "2000m" -celeryMemoryRequest: "700Mi" -celeryMemoryLimit: "700Mi" +celeryCpuLimit: "1000m" +celeryMemoryRequest: "1600Mi" +celeryMemoryLimit: "1600Mi" juliaReplicas: 1 -juliaCpuRequest: "1000m" +juliaCpuRequest: "500m" juliaCpuLimit: "2000m" -juliaMemoryRequest: "8000Mi" -juliaMemoryLimit: "8000Mi" +juliaMemoryRequest: "6000Mi" +juliaMemoryLimit: "6000Mi" diff --git a/Dockerfile b/Dockerfile index f013a9acd..c5c064371 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM reopt/py38 +FROM reopt/py312 # Install NREL root certs for machines running on NREL's network. ARG NREL_ROOT_CERT_URL_ROOT="" @@ -9,12 +9,18 @@ ENV SRC_DIR=/opt/reopt/reo/src ENV LD_LIBRARY_PATH="/opt/reopt/reo/src:${LD_LIBRARY_PATH}" # Copy all code -ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONDONTWRITEBYTECODE=1 COPY . /opt/reopt # Install python packages WORKDIR /opt/reopt RUN ["pip", "install", "-r", "requirements.txt"] +# Conditionally install EVI-EnLitePy and pydantic (dependency) if EVI-EnLitePy has been cloned via git submodule +RUN if [ -d "/opt/reopt/EVI-EnLitePy" ] && [ "$(ls -A /opt/reopt/EVI-EnLitePy)" ]; then \ + cd /opt/reopt/EVI-EnLitePy && pip install -e .; \ + pip install pydantic; \ +fi + EXPOSE 8000 ENTRYPOINT ["/bin/bash", "-c"] diff --git a/EVI-EnLitePy b/EVI-EnLitePy new file mode 160000 index 000000000..75e5e6e4c --- /dev/null +++ b/EVI-EnLitePy @@ -0,0 +1 @@ +Subproject commit 75e5e6e4c106e7b28c932bbc524816c199661759 diff --git a/load_builder/urls.py b/load_builder/urls.py index f31cfc794..5e707b5e5 100644 --- a/load_builder/urls.py +++ b/load_builder/urls.py @@ -5,4 +5,5 @@ urlpatterns = [ re_path(r'^load_builder/?$', views.load_builder), + re_path(r'^ensite/?$', views.ensite_view), ] \ No newline at end of file diff --git a/load_builder/views.py b/load_builder/views.py index 58614936e..d412a3012 100644 --- a/load_builder/views.py +++ b/load_builder/views.py @@ -1,4 +1,4 @@ -# REoptĀ®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt_API/blob/master/LICENSE. +# REoptĀ®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt_API/load_builder/views.py import csv import io import json @@ -7,6 +7,315 @@ from django.http import JsonResponse from reo.exceptions import UnexpectedError +try: + from ensitepy.simextension import enlitepyapi +except ImportError: + enlitepyapi = None + +# EnLitePy Configuration Constants +# These constants define default simulation-level parameters exposed to the UI. +# Units & Conventions: +# * UI-facing aggregate power capacities (e.g., EMS/site charge cap) are in kW. +# * Internal model power is Watts -> we convert via (kW * 1000) at ingress. +# * Charging duration inputs are in hours; converted to integer seconds. +# * Guardrails (min/max) are deliberately generous to catch obvious typos +# (e.g., 0.1 kW or 1e9 kW) while not constraining legitimate large sites. +DEFAULT_CHARGER_DEFAULTS = { + "L1": {"count": 0, "power_kW": 2.4, "eff": 0.80, "pci": "LD"}, + "L2": {"count": 0, "power_kW": 13.0, "eff": 0.80, "pci": "LD"}, + "DCFC_LD": {"count": 0, "power_kW": 50.0, "eff": 0.90, "pci": "LD"}, + "DCFC_MDHD": {"count": 0, "power_kW": 250.0, "eff": 0.90, "pci": "MD/HD"}, +} + +DEFAULT_VEHICLE_DEFAULTS = { + "LD": {"weekday": 0, "weekend": 0, "battery_kWh": 80, "initSOC": 0.10, "targetSOC": 0.80, "pChgMax_kW": 120}, + "MD": {"weekday": 0, "weekend": 0, "battery_kWh": 200, "initSOC": 0.20, "targetSOC": 0.80, "pChgMax_kW": 300}, + "HD": {"weekday": 0, "weekend": 0, "battery_kWh": 500, "initSOC": 0.30, "targetSOC": 0.85, "pChgMax_kW": 600}, +} +EMS_DEFAULT_PCHG_CAP_KW = 1000 # Legacy fallback (1 MW) if no chargers defined +MAX_CHG_DURATION_DEFAULT_HR = None # If set (e.g., 6), becomes default max charge duration (hours) unless UI overrides + +SIM_DAYS = 7 +EMS_MIN_PCHG_CAP_KW = 10 # lower bound guardrail (practically non-zero site) +EMS_MAX_PCHG_CAP_KW = 20000 # upper bound guardrail (20 MW campus / depot scale) +EMS_CONFIG = {"type": "basic", "pChgCap": EMS_DEFAULT_PCHG_CAP_KW * 1000, "pap": "FCFS+SMX"} # stored in Watts +BASE_SIM_CONFIG = {"tStart": 0, "tEnd": 7 * 24 * 3600, "dowStart": 0} +RESULTS_CONFIG_BASE = {"timeseriesDt": 3600, "powerMetricsDt": 3600} +WEEK_TO_YEAR_SCALE = 365 / 7 + +# Keys for annual scaling +EQUIPMENT_ANNUAL_SCALARS = {"Energy output [kWh]", "Energy loss [kWh]"} +EV_COUNT_SCALARS = {"Total EVs arrived [#]", "Total unique EVs arrived [#]", "EVs no queue wait [#]", "EVs arrived but not charged [#]", "EVs that did not start/complete charge [#]"} +EV_ENERGY_SCALARS = {"Total energy charged [kWh]"} + +# --------------------------------------------------------------------------- +# EnLitePy Helper Functions +# --------------------------------------------------------------------------- + +def build_arrival_pmf(weekday_pct, weekend_pct, bucket_hours=2): + """Convert UI bucket percentages to API arrivalTimePMF structure.""" + bucket_seconds = bucket_hours * 3600 + + def normalize(side, label): + if len(side) != 12: + raise ValueError(f"{label} arrival array must have 12 entries (got {len(side)})") + if any(x < 0 for x in side): + raise ValueError(f"{label} arrival percentages must be non-negative") + active = [(i, x) for i, x in enumerate(side) if x > 0] + if not active: # fallback deterministic midnight arrival + return {"val": [0], "prob": [1.0], "dur": bucket_seconds} + total = sum(x for _, x in active) + vals = [i * bucket_seconds for i, _ in active] + probs = [x / total for _, x in active] + # Adjust final probability for floating sum errors + diff = 1.0 - sum(probs) + if abs(diff) > 1e-9: + probs[-1] += diff + return {"val": vals, "prob": [round(p, 6) for p in probs], "dur": bucket_seconds} + + return {"1-5": normalize(weekday_pct, "Weekday"), "0,6": normalize(weekend_pct, "Weekend")} + + +def _validate_and_apply_ems_override(ems_override, base_cfg): + """Return new EMS config with validated overrides applied. + + Inputs: + ems_override: dict possibly containing one or both of: + * pChgCap_kW (float/int) -> preferred UI field + * pChgCap (int Watts) -> legacy / fallback + base_cfg: existing EMS config dict (must include 'pChgCap' in Watts) + + Logic: + * Precedence: pChgCap_kW overrides pChgCap if both present. + * Conversion: kW -> W, rounding to nearest integer. + * Validation: Enforces positive and within configured guardrails. + + Returns: + New dict copy containing possibly updated 'pChgCap'. + + Raises: + ValueError with a concise reason for any invalid override. + """ + cfg = dict(base_cfg) + if not isinstance(ems_override, dict): + return cfg + pchgcap_kw = ems_override.get("pChgCap_kW") + pchgcap_w = ems_override.get("pChgCap") if pchgcap_kw is None else None + try: + if pchgcap_kw is not None: + val_kw = float(pchgcap_kw) + if val_kw <= 0: + raise ValueError("pChgCap_kW must be > 0") + if not (EMS_MIN_PCHG_CAP_KW <= val_kw <= EMS_MAX_PCHG_CAP_KW): + raise ValueError(f"pChgCap_kW must be within [{EMS_MIN_PCHG_CAP_KW}, {EMS_MAX_PCHG_CAP_KW}] kW") + cfg["pChgCap"] = int(round(val_kw * 1000)) + elif pchgcap_w is not None: + val_w = float(pchgcap_w) + if val_w <= 0: + raise ValueError("pChgCap must be > 0") + val_kw = val_w / 1000.0 + if not (EMS_MIN_PCHG_CAP_KW <= val_kw <= EMS_MAX_PCHG_CAP_KW): + raise ValueError(f"pChgCap equivalent must be within [{EMS_MIN_PCHG_CAP_KW}, {EMS_MAX_PCHG_CAP_KW}] kW") + cfg["pChgCap"] = int(round(val_w)) + except (TypeError, ValueError) as exc: + raise ValueError(f"Invalid EMS capacity override: {exc}") + return cfg + + +def build_enlitepy_payload(ui_inputs=None): + """Build EnLitePy payload from UI inputs or use defaults. + + Parameters + ---------- + ui_inputs : dict | None + Optional UI schema input. If provided, may include: + - chargers + - vehicles + - arrival + - ems: { pChgCap_kW: |None OR pChgCap: } + If both provided, pChgCap_kW takes precedence. The resulting value + is stored internally in Watts under 'pChgCap'. + - maxChargeDurationHr (float/int, HOURS) + + Notes + ----- + * Duration: `maxChargeDurationHr` (hours) -> multiplied by 3600 -> seconds (`chgDurMax`). + * Power units: UI kW values converted to W where simulation expects watts. + * EMS Capacity Default: If the UI does NOT supply an EMS override, the default + aggregate site charge capacity is computed as the SUM of all configured + charger rated powers (count * power_kW for each charger). If that sum is 0 + (no chargers), we fall back to EMS_DEFAULT_PCHG_CAP_KW (legacy 1 MW). UI can + still explicitly override via ems.pChgCap_kW or ems.pChgCap. + * Max Charge Duration: If `maxChargeDurationHr` omitted and `MAX_CHG_DURATION_DEFAULT_HR` + is set, that value is applied. Only a basic >0 validation is enforced. + * PMFs: Currently deterministic single-point distributions; can be expanded later. + """ + # Apply UI overrides or use defaults + charger_config = ui_inputs.get("chargers", {}) if ui_inputs else {} + vehicle_config = ui_inputs.get("vehicles", {}) if ui_inputs else {} + arrival_config = ui_inputs.get("arrival", {}) if ui_inputs else {} + ems_override = ui_inputs.get("ems", {}) if ui_inputs else {} + max_duration = ui_inputs.get("maxChargeDurationHr") if ui_inputs else None + if max_duration is None and MAX_CHG_DURATION_DEFAULT_HR is not None: + max_duration = MAX_CHG_DURATION_DEFAULT_HR + + # Deep merge with defaults + chargers = {} + for cat, defaults in DEFAULT_CHARGER_DEFAULTS.items(): + chargers[cat] = {**defaults} + if cat in charger_config: + chargers[cat].update(charger_config[cat]) + + vehicles = {} + for vtype, defaults in DEFAULT_VEHICLE_DEFAULTS.items(): + vehicles[vtype] = {**defaults} + if vtype in vehicle_config: + vehicles[vtype].update(vehicle_config[vtype]) + + # Arrival patterns + weekday_pct = arrival_config.get("weekday", [0] * 12) + weekend_pct = arrival_config.get("weekend", [0] * 12) + + # Start with EMS defaults and apply overrides + ems_cfg = _validate_and_apply_ems_override(ems_override, EMS_CONFIG) + + # Dynamic capacity if no explicit override provided (i.e., value unchanged from base) + if ems_cfg.get("pChgCap") == EMS_CONFIG["pChgCap"] and not ems_override: + total_kw = 0.0 + for cat, cfg in chargers.items(): + count = cfg.get("count", 0) or 0 + power_kw = cfg.get("power_kW", 0) or 0 + try: + total_kw += float(count) * float(power_kw) + except (TypeError, ValueError): + pass + if total_kw > 0: + ems_cfg["pChgCap"] = int(round(total_kw * 1000)) + else: + # retain legacy fallback already set + pass + + # Build nodes with grid capacity synchronized to EMS pChgCap + grid_pmax = ems_cfg.get("pChgCap", EMS_DEFAULT_PCHG_CAP_KW * 1000) + nodes = {"grid": {"type": "Grid", "pMax": grid_pmax, "pMin": 0, "eff": 0.99}} + + # Add EVSE nodes + for cat, cfg in chargers.items(): + for i in range(1, cfg["count"] + 1): + nodes[f"EVSE_{cat}_{i}"] = { + "type": "EVSE", + "pMax": int(cfg["power_kW"] * 1000), + "pMin": 0, + "eff": cfg["eff"], + "nEVPort": 1, + "pci": cfg["pci"], + } + + # Build EV types + def expand_counts(weekend, weekday): + return [weekend, weekday, weekday, weekday, weekday, weekday, weekend] + + weekday_weekend = {k: expand_counts(v["weekend"], v["weekday"]) for k, v in vehicles.items()} + arrival_pmf = build_arrival_pmf(weekday_pct, weekend_pct) + + ev_types = {} + for k, v in vehicles.items(): + delta = max(0.0, min(1.0, v["targetSOC"] - v["initSOC"])) + ev_types[k] = { + "type": k, + "eCap": int(v["battery_kWh"] * 1000), + "pChgMax": int(v["pChgMax_kW"] * 1000), + "count": weekday_weekend[k], + "targetFinalSOCPMF": {"val": [round(v["targetSOC"], 4)], "prob": [1.0]}, + "energyDemandPMF": {"val": [round(delta, 4)], "prob": [1.0]}, + "arrivalTimePMF": arrival_pmf, + "departureTimePMF": None, + } + + sim_cfg = dict(BASE_SIM_CONFIG) + sim_cfg["tEnd"] = SIM_DAYS * 24 * 3600 + + duration_max_hr = None + if max_duration is not None: + try: + md = float(max_duration) + if md <= 0: + raise ValueError("maxChargeDurationHr must be > 0") + duration_max_hr = int(round(md * 3600)) + except (TypeError, ValueError) as exc: + raise ValueError(f"Invalid maxChargeDurationHr: {exc}") + + return { + "simConfig": sim_cfg, + "hubConfig": {"nodes": nodes, "ems": ems_cfg}, + "evInfo": {"stochasticModel": 1, "chgDurMax": duration_max_hr, "evTypes": ev_types}, + "resultsConfig": { + **RESULTS_CONFIG_BASE, + "resultFieldOptions": { + "nodes": "Grid", + "portUsage": True, + "queueLength": True, + "nodeStats": True, + "evStats": True, + }, + }, + } + + +def is_ui_input(data): + """Detect if input is UI format vs direct EnLitePy payload.""" + if not isinstance(data, dict): + return False + + # Check for UI-specific keys + ui_keys = {"chargers", "vehicles", "arrival", "maxChargeDurationHr", "ems"} + if any(key in data for key in ui_keys): + return True + + # Check for direct EnLitePy payload keys + enlitepy_keys = {"simConfig", "hubConfig", "evInfo", "resultsConfig"} + if any(key in data for key in enlitepy_keys): + return False + + # Default to UI format for safety + return True + +def ensite_view(request): + """Enhanced EnLitePy endpoint that handles both UI inputs and direct payloads.""" + if enlitepyapi is not None: + try: + if request.method == 'POST': + try: + input_data = json.loads(request.body) + except json.JSONDecodeError as e: + return JsonResponse({"Error": f"Invalid JSON in request body: {e}"}, status=400) + if is_ui_input(input_data): + try: + enlitepy_payload = build_enlitepy_payload(input_data) + except Exception as e: + return JsonResponse({"Error": f"Failed to build EnLitePy payload: {e}"}, status=400) + else: + enlitepy_payload = input_data + try: + results = enlitepyapi.run(enlitepy_payload) + if isinstance(results, dict) and 'logs' not in results: + results['logs'] = [] + # Always normalize and annualize for client convenience + _normalize_and_enrich(results) + # Bump enrichment version to 2 to reflect unit change (power_in_grid_annual now in kW) + results.setdefault('version', 2) + return JsonResponse({"results": results}) + except Exception as e: + return JsonResponse({"Error": f"EnLitePy execution failed: {e}"}, status=500) + else: + return JsonResponse({"Error": "Must POST a JSON body for ensitepy."}, status=400) + except Exception: + exc_type, exc_value, exc_traceback = sys.exc_info() + err = UnexpectedError(exc_type, exc_value, exc_traceback, task='ensite') + return JsonResponse({"Error": err.message}, status=500) + else: + return JsonResponse({"Error": "ensitepy is not installed."}, status=500) + def check_load_builder_inputs(loads_table): required_inputs = ["Power (W)", "Quantity", "% Run Time", "Start Mo.", "Stop Mo.", "Start Hr.", "Stop Hr."] @@ -31,7 +340,7 @@ def validate_load_builder_inputs(loads_table): for numeric_input in numeric_inputs: try: float(load[numeric_input]) - except: + except Exception: return False for month_input in month_inputs: @@ -55,16 +364,16 @@ def load_builder(request): if request.method == 'POST': post = request.body try: - # Try to import JSON, then try to import CSV + # Try to import JSON, then fall back to CSV try: loads_table = json.loads(post) - except: + except Exception: loads_table = post.decode("utf-8") finally: if not isinstance(loads_table, list): csv_reader = csv.DictReader(io.StringIO(loads_table)) loads_table = list(csv_reader) - except: + except Exception: return JsonResponse({"Error": "Invalid JSON or CSV"}) # Validation @@ -80,7 +389,7 @@ def load_builder(request): else: return JsonResponse({"Error": "Must POST a JSON based on the SolarResilient component based load builder downloadable CSV"}) - except Exception as e: + except Exception: exc_type, exc_value, exc_traceback = sys.exc_info() err = UnexpectedError(exc_type, exc_value, exc_traceback, task='load_builder') err.save_to_db() @@ -161,3 +470,106 @@ def translate_months(string): hour_load += load.power_total loads_kw.append(hour_load) return loads_kw + +# (Removed duplicate constant definitions that appeared earlier in file) + +def _replicate_to_8760(series): + if not isinstance(series, list) or len(series) == 0: + return series + if len(series) == 8760: + return series + reps = (8760 // len(series)) + 1 + return (series * reps)[:8760] + +def _annualize_equipment(stats_list): + if not isinstance(stats_list, list): + return [] + out = [] + for row in stats_list: + if not isinstance(row, dict): + continue + copy = row.copy() + for k in EQUIPMENT_ANNUAL_SCALARS: + v = copy.get(k) + if isinstance(v, (int, float)): + copy[k] = round(v * WEEK_TO_YEAR_SCALE, 2) + out.append(copy) + return out + +def _annualize_ev(stats_list): + if not isinstance(stats_list, list): + return [] + out = [] + for row in stats_list: + if not isinstance(row, dict): + continue + copy = row.copy() + for k, v in list(copy.items()): + if k in EV_COUNT_SCALARS or k in EV_ENERGY_SCALARS: + if isinstance(v, (int, float)): + copy[k] = round(v * WEEK_TO_YEAR_SCALE, 2) + if k.endswith('[min]') and isinstance(v, (int, float)): + copy[k.replace('[min]', '[h]')] = round(v / 60.0, 2) + del copy[k] + out.append(copy) + return out + +def _add_grid_util(timeseries_dict, equip_annual): + if not isinstance(timeseries_dict, dict): + return + power = timeseries_dict.get('power_in_grid') + if not isinstance(power, list) or not power: + return + # Locate grid equipment row + grid_row = None + for r in equip_annual: + if isinstance(r, dict) and r.get('Name') in ('grid', 'Grid'): + grid_row = r + break + if not grid_row: + return + cap = grid_row.get('Power capacity [kW]') + if not isinstance(cap, (int, float)) or cap <= 0: + return + sample = [v for v in power[:50] if isinstance(v, (int, float))] + if not sample: + return + max_sample = max(sample) + to_kw = 1000.0 if max_sample > cap * 1.1 else 1.0 + series_kw = [v / to_kw for v in power if isinstance(v, (int, float))] + if not series_kw: + return + grid_row.setdefault('Min capacity utilization [kW]', round(min(series_kw), 2)) + grid_row.setdefault('Average capacity utilization [kW]', round(sum(series_kw) / len(series_kw), 2)) + grid_row.setdefault('Max capacity utilization [kW]', round(max(series_kw), 2)) + + +def _normalize_and_enrich(results_dict): + if not isinstance(results_dict, dict): + return + # Normalize stringified JSON lists + for key in ('equipment_statistics', 'ev_statistics'): + val = results_dict.get(key) + if isinstance(val, str): + try: + parsed = json.loads(val) + if isinstance(parsed, list): + results_dict[key] = parsed + except json.JSONDecodeError: + pass + equipment = results_dict.get('equipment_statistics') or [] + evstats = results_dict.get('ev_statistics') or [] + timeseries = results_dict.get('timeseries') + # Annual replication for timeseries + if isinstance(timeseries, dict) and 'power_in_grid' in timeseries: + annual_series = _replicate_to_8760(timeseries['power_in_grid']) + if isinstance(annual_series, list) and len(annual_series) == 8760: + # Convert from W (simulation native) to kW for annual series exposure + timeseries['power_in_grid_annual'] = [x / 1000 for x in annual_series] + equip_annual = _annualize_equipment(equipment) if equipment else [] + ev_annual = _annualize_ev(evstats) if evstats else [] + if equip_annual: + results_dict['equipment_statistics_annual'] = equip_annual + _add_grid_util(timeseries, equip_annual) + if ev_annual: + results_dict['ev_statistics_annual'] = ev_annual diff --git a/reo/process_results.py b/reo/process_results.py index 6bb08bc7b..759749837 100644 --- a/reo/process_results.py +++ b/reo/process_results.py @@ -496,12 +496,12 @@ def get_nested(self): # preprocessed_BAU_year_one_emissions_tCO2 = self.results_dict.get("preprocessed_BAU_year_one_emissions_tCO2") # year_one_emissions_bau_tCO2_out = self.results_dict.get("year_one_emissions_tCO2_bau") # year_one_emissions_bau_preprocess_pct_diff = (year_one_emissions_bau_tCO2_out-preprocessed_BAU_year_one_emissions_tCO2)/year_one_emissions_bau_tCO2_out - # self.assertAlmostEquals(year_one_emissions_bau_preprocess_pct_diff,0.0,places=2) #(<0.5% error) + # self.assertAlmostEqual(year_one_emissions_bau_preprocess_pct_diff,0.0,places=2) #(<0.5% error) # check pre-processed lifecycle bau CO2 emissions calcs vs lifecycle bau CO2 emissions output # preprocessed_BAU_lifecycle_emissions_tCO2 = self.results_dict.get("preprocessed_BAU_lifecycle_emissions_tCO2") # lifecycle_emissions_bau_tCO2_out = self.results_dict.get("lifecycle_emissions_tCO2_bau") # lifecycle_emissions_bau_preprocess_pct_diff = (lifecycle_emissions_bau_tCO2_out-preprocessed_BAU_lifecycle_emissions_tCO2)/lifecycle_emissions_bau_tCO2_out - # self.assertAlmostEquals(lifecycle_emissions_bau_preprocess_pct_diff,0.0,places=2) #(<0.5% error) + # self.assertAlmostEqual(lifecycle_emissions_bau_preprocess_pct_diff,0.0,places=2) #(<0.5% error) # emissions reductions diff --git a/reo/tests/test_diesel_gen_sizing.py b/reo/tests/test_diesel_gen_sizing.py index 64c4901f8..360c5272e 100644 --- a/reo/tests/test_diesel_gen_sizing.py +++ b/reo/tests/test_diesel_gen_sizing.py @@ -96,4 +96,4 @@ def test_generator_sizing_with_existing_pv(self): list_to_load = [generator_to_load, storage_to_load, pv_to_load] tech_to_load = self.outage_tech_to_load(list_to_load, outage_start, outage_end) for x, y in zip(critical_load[outage_start-1:outage_end], tech_to_load): - self.assertAlmostEquals(x, y, places=3) + self.assertAlmostEqual(x, y, places=3) diff --git a/reo/tests/test_hybrid_loadprofile.py b/reo/tests/test_hybrid_loadprofile.py index fa25dfd25..0e0063a39 100644 --- a/reo/tests/test_hybrid_loadprofile.py +++ b/reo/tests/test_hybrid_loadprofile.py @@ -198,4 +198,4 @@ def test_hybrid_loadprofile(self): # this comparison of the 'resultant_load_profile' and 'manually_calculated_load_profile' is possible because there is no existing-pv in this case. If existing-pv is present, then the resultant_hybrid_annual_kwh would be different when net native load is calculated by subtracting existing-pv's output. - self.assertAlmostEquals(resultant_hybrid_annual_kwh, 500000, places=2) \ No newline at end of file + self.assertAlmostEqual(resultant_hybrid_annual_kwh, 500000, places=2) \ No newline at end of file diff --git a/reo/tests/test_multiple_pvs.py b/reo/tests/test_multiple_pvs.py index fba1804bc..961a34fff 100644 --- a/reo/tests/test_multiple_pvs.py +++ b/reo/tests/test_multiple_pvs.py @@ -270,15 +270,15 @@ def test_multiple_pv(self): roof_west = next(d for d in pvs if d["pv_name"] == "roof_west") roof_east = next(d for d in pvs if d["pv_name"] == "roof_east") - self.assertAlmostEquals(ground_pv['size_kw'], 15, places=2) - self.assertAlmostEquals(roof_west['size_kw'], 7, places=2) - self.assertAlmostEquals(roof_east['size_kw'], 4, places=2) - self.assertAlmostEquals(ground_pv['existing_pv_om_cost_us_dollars'], 782.0, places=2) - self.assertAlmostEquals(roof_west['existing_pv_om_cost_us_dollars'], 782.0, places=2) - self.assertAlmostEquals(roof_east['existing_pv_om_cost_us_dollars'], 0, places=2) - self.assertAlmostEquals(ground_pv['average_yearly_energy_produced_bau_kwh'], 8719.0, places=2) - self.assertAlmostEquals(roof_west['average_yearly_energy_produced_bau_kwh'], 7334.0, places=2) - self.assertAlmostEquals(roof_east['average_yearly_energy_produced_bau_kwh'], 0, places=2) - self.assertAlmostEquals(ground_pv['average_yearly_energy_produced_kwh'], 26157.0, places=2) - self.assertAlmostEquals(roof_west['average_yearly_energy_produced_kwh'], 10268.0, places=2) - self.assertAlmostEquals(roof_east['average_yearly_energy_produced_kwh'], 6390.0, places=2) \ No newline at end of file + self.assertAlmostEqual(ground_pv['size_kw'], 15, places=2) + self.assertAlmostEqual(roof_west['size_kw'], 7, places=2) + self.assertAlmostEqual(roof_east['size_kw'], 4, places=2) + self.assertAlmostEqual(ground_pv['existing_pv_om_cost_us_dollars'], 782.0, places=2) + self.assertAlmostEqual(roof_west['existing_pv_om_cost_us_dollars'], 782.0, places=2) + self.assertAlmostEqual(roof_east['existing_pv_om_cost_us_dollars'], 0, places=2) + self.assertAlmostEqual(ground_pv['average_yearly_energy_produced_bau_kwh'], 8719.0, places=2) + self.assertAlmostEqual(roof_west['average_yearly_energy_produced_bau_kwh'], 7334.0, places=2) + self.assertAlmostEqual(roof_east['average_yearly_energy_produced_bau_kwh'], 0, places=2) + self.assertAlmostEqual(ground_pv['average_yearly_energy_produced_kwh'], 26157.0, places=2) + self.assertAlmostEqual(roof_west['average_yearly_energy_produced_kwh'], 10268.0, places=2) + self.assertAlmostEqual(roof_east['average_yearly_energy_produced_kwh'], 6390.0, places=2) \ No newline at end of file diff --git a/reo/tests/test_re_emissions_constraints.py b/reo/tests/test_re_emissions_constraints.py index 6b44add63..0018c37ee 100644 --- a/reo/tests/test_re_emissions_constraints.py +++ b/reo/tests/test_re_emissions_constraints.py @@ -87,19 +87,19 @@ def test_RE_and_ER(self): # Emissions reductions: ER_pct_out = d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'] if ER_target[i] is not None: - self.assertAlmostEquals(ER_target[i], d['inputs']['Scenario']['Site']['co2_emissions_reduction_min_pct']) - self.assertAlmostEquals(ER_pct_out,ER_target[i],places=3) + self.assertAlmostEqual(ER_target[i], d['inputs']['Scenario']['Site']['co2_emissions_reduction_min_pct']) + self.assertAlmostEqual(ER_pct_out,ER_target[i],places=3) lifecycle_emissions_tCO2_out = d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'] lifecycle_emissions_bau_tCO2_out = d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'] ER_pct_calced_out = (lifecycle_emissions_bau_tCO2_out-lifecycle_emissions_tCO2_out)/lifecycle_emissions_bau_tCO2_out ER_pct_diff = abs(ER_pct_calced_out-ER_pct_out) - self.assertAlmostEquals(ER_pct_diff,0.0,places=2) #within 1% of each other + self.assertAlmostEqual(ER_pct_diff,0.0,places=2) #within 1% of each other # Year 1 emissions - non-BAU case: year_one_emissions_tCO2_out = d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'] yr1_fuel_emissions_tCO2_out = d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'] or 0.0 yr1_grid_emissions_tCO2_out = d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'] or 0.0 yr1_total_emissions_calced_tCO2 = yr1_fuel_emissions_tCO2_out + yr1_grid_emissions_tCO2_out - self.assertAlmostEquals(year_one_emissions_tCO2_out,yr1_total_emissions_calced_tCO2,places=-1) + self.assertAlmostEqual(year_one_emissions_tCO2_out,yr1_total_emissions_calced_tCO2,places=-1) # Breakeven cost of emissions reductions yr1_cost_ER_usd_per_tCO2_out = d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'] self.assertTrue(yr1_cost_ER_usd_per_tCO2_out is None or yr1_cost_ER_usd_per_tCO2_out>=0.0) @@ -107,113 +107,113 @@ def test_RE_and_ER(self): ### test expected values for each scenario: if i ==0: # Scenario 1 # system sizes - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['PV']['size_kw'],46.2923,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Wind']['size_kw'],10.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Storage']['size_kw'],2.4303,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Storage']['size_kwh'],6.062,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Generator']['size_kw'],22.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['PV']['size_kw'],46.2923,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Wind']['size_kw'],10.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Storage']['size_kw'],2.4303,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Storage']['size_kwh'],6.062,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Generator']['size_kw'],22.0,places=0) # NPV npv = d['outputs']['Scenario']['Site']['Financial']['npv_us_dollars'] npv_diff_pct = (-101278.0 - npv)/-101278.0 - self.assertAlmostEquals(npv_diff_pct,0.0,places=3) + self.assertAlmostEqual(npv_diff_pct,0.0,places=3) # Renewable energy - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct'],0.8,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh'],75140.37,places=-3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct_bau'],0.146395,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct'],0.8,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct_bau'],0.146395,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct'],0.8,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh'],75140.37,places=-3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct_bau'],0.146395,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct'],0.8,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct_bau'],0.146395,places=3) # CO2 emissions - totals, from grid, from fuelburn, ER, $/tCO2 breakeven - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'],0.660022,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'],301.237,places=1) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'],12.79,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2_bau'],40.54,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'],7.65,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2'],8713.36,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2_bau'],25856.9,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'],244.06,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'],717.86,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2'],152.97,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'],5.14,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2_bau'],40.54,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2'],91.09,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2_bau'],717.86,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'],0.660022,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'],301.237,places=1) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'],12.79,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2_bau'],40.54,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'],7.65,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2'],8713.36,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2_bau'],25856.9,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'],244.06,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'],717.86,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2'],152.97,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'],5.14,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2_bau'],40.54,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2'],91.09,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2_bau'],717.86,places=0) if i ==1: # Scenario 2 # system sizes - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['PV']['size_kw'],59.1891,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Wind']['size_kw'],23.0474,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Storage']['size_kw'],12.532,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Storage']['size_kwh'],80.817,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Generator']['size_kw'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['PV']['size_kw'],59.1891,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Wind']['size_kw'],23.0474,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Storage']['size_kw'],12.532,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Storage']['size_kwh'],80.817,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Generator']['size_kw'],0.0,places=0) # NPV npv = d['outputs']['Scenario']['Site']['Financial']['npv_us_dollars'] npv_diff_pct = (-198934.0 - npv)/-198934.0 - self.assertAlmostEquals(npv_diff_pct,0.0,places=3) + self.assertAlmostEqual(npv_diff_pct,0.0,places=3) # Renewable energy - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct'],0.78984,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh'],78984.01,places=-1) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct_bau'],0.135426,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh_bau'],13542.62,places=-1) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct'],0.78984,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct_bau'],0.135426,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct'],0.78984,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh'],78984.01,places=-1) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct_bau'],0.135426,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh_bau'],13542.62,places=-1) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct'],0.78984,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct_bau'],0.135426,places=3) # CO2 emissions - totals, from grid, from fuelburn, ER, $/tCO2 breakeven - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'],0.8,places=3) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'],342.947,places=1) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'],11.59,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2_bau'],57.97,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2'],7395.84,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2_bau'],36979.2,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'],205.33,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'],1026.65,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'],11.59,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2_bau'],57.97,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2'],205.33,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2_bau'],1026.65,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'],0.8,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'],342.947,places=1) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'],11.59,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2_bau'],57.97,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2'],7395.84,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2_bau'],36979.2,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'],205.33,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'],1026.65,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2_bau'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'],11.59,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2_bau'],57.97,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2'],205.33,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2_bau'],1026.65,places=0) if i ==2: # Scenario 3 # system sizes - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['PV']['size_kw'],20.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Wind']['size_kw'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Storage']['size_kw'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Storage']['size_kwh'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['Generator']['size_kw'],0.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['CHP']['size_kw'],200.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['SteamTurbine']['size_kw'],1254.938,places=-2) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['NewBoiler']['size_mmbtu_per_hr'],28.04,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['AbsorptionChiller']['size_ton'],400.0,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['HotTES']['size_gal'],50000,places=0) - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ColdTES']['size_gal'],30000,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['PV']['size_kw'],20.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Wind']['size_kw'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Storage']['size_kw'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Storage']['size_kwh'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['Generator']['size_kw'],0.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['CHP']['size_kw'],200.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['SteamTurbine']['size_kw'],1254.938,places=-2) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['NewBoiler']['size_mmbtu_per_hr'],28.04,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['AbsorptionChiller']['size_ton'],400.0,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['HotTES']['size_gal'],50000,places=0) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ColdTES']['size_gal'],30000,places=0) # NPV npv = d['outputs']['Scenario']['Site']['Financial']['npv_us_dollars'] npv_diff_pct = (11210580.0 - npv)/11210580.0 - self.assertAlmostEquals(npv_diff_pct,0.0,places=1) + self.assertAlmostEqual(npv_diff_pct,0.0,places=1) # Renewable energy - self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct'],0.733551,places=2) - #self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh'],5540064.64,places=-3) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct_bau'],0.001534,places=3) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh_bau'],13454.22,places=-2) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct'],0.572143,places=2) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct_bau'],0.000852,places=3) + self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct'],0.733551,places=2) + #self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh'],5540064.64,places=-3) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_pct_bau'],0.001534,places=3) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_renewable_electricity_kwh_bau'],13454.22,places=-2) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct'],0.572143,places=2) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['annual_total_renewable_energy_pct_bau'],0.000852,places=3) # # CO2 emissions - totals, from grid, from fuelburn, ER, $/tCO2 breakeven - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'],1.276628,places=2) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'],0.0,places=3) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'],-1717.17,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2_bau'],5931.29,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'],33.89,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2_bau'],1585.32,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2'],-1212513.34,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2_bau'],4350968.52,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'],-36837.04,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'],133157.72,places=1) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2'],844.66,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2_bau'],39633.01,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'],-1751.02,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2_bau'],4345.97,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2'],-37681.7,places=0) - # self.assertAlmostEquals(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2_bau'],93524.7,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_reduction_CO2_pct'],1.276628,places=2) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['breakeven_cost_of_emissions_reduction_us_dollars_per_tCO2'],0.0,places=3) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2'],-1717.17,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_tCO2_bau'],5931.29,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2'],33.89,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['year_one_emissions_from_fuelburn_tCO2_bau'],1585.32,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2'],-1212513.34,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_cost_CO2_bau'],4350968.52,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2'],-36837.04,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_tCO2_bau'],133157.72,places=1) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2'],844.66,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['lifecycle_emissions_from_fuelburn_tCO2_bau'],39633.01,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2'],-1751.02,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['year_one_emissions_tCO2_bau'],4345.97,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2'],-37681.7,places=0) + # self.assertAlmostEqual(d['outputs']['Scenario']['Site']['ElectricTariff']['lifecycle_emissions_tCO2_bau'],93524.7,places=0) diff --git a/reo/tests/test_validator.py b/reo/tests/test_validator.py index 287d58b25..b715b0e54 100644 --- a/reo/tests/test_validator.py +++ b/reo/tests/test_validator.py @@ -73,16 +73,16 @@ def test_different_load_profiles(self): validator = self.get_validator(post) if length in good_lengths: - self.assertEquals(validator.isValid, True) + self.assertEqual(validator.isValid, True) # check downsampling if length > 8760: - self.assertEquals( + self.assertEqual( len(validator.input_dict['Scenario']['Site']['LoadProfile']['loads_kw']), 8760 ) elif length in bad_lengths: - self.assertEquals(validator.isValid, False) + self.assertEqual(validator.isValid, False) assert(any('Invalid length for loads_kw' in e for e in validator.errors['input_errors'])) def test_different_critical_load_profiles(self): @@ -103,16 +103,16 @@ def test_different_critical_load_profiles(self): validator = self.get_validator(post) if length in good_lengths: - self.assertEquals(validator.isValid, True) + self.assertEqual(validator.isValid, True) # check downsampling if length > 8760: - self.assertEquals( + self.assertEqual( len(validator.input_dict['Scenario']['Site']['LoadProfile']['critical_loads_kw']), 8760 ) elif length in bad_lengths: - self.assertEquals(validator.isValid, False) + self.assertEqual(validator.isValid, False) assert(any('Invalid length for critical_loads_kw' in e for e in validator.errors['input_errors'])) def test_warnings_for_mismatch_of_time_steps_per_hour_and_resolution_of_time_of_export_rate(self): @@ -126,7 +126,7 @@ def test_warnings_for_mismatch_of_time_steps_per_hour_and_resolution_of_time_of_ for rate in rates: post["Scenario"]["Site"]["ElectricTariff"][rate] = [1.1] * 8760 * resolution validator = self.get_validator(post) - self.assertEquals(validator.isValid, True) + self.assertEqual(validator.isValid, True) up_or_down = "Upsampled" if resolution > time_steps_per_hour: up_or_down = "Downsampled" @@ -140,7 +140,7 @@ def test_coincident_peak_inputs(self): post["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_active_timesteps"] = 5000 post["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_charge_us_dollars_per_kw"] = [10,5,8] validator = self.get_validator(post) - self.assertEquals(validator.isValid, False) + self.assertEqual(validator.isValid, False) self.assertEqual(validator.input_dict["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_active_timesteps"], [[5000]]) self.assertEqual(validator.input_dict["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_charge_us_dollars_per_kw"], [10,5,8]) assert(any("The number of rates in coincident_peak_load_charge_us_dollars_per_kw must match the number of timestep sets in coincident_peak_load_active_timesteps" in e for e in validator.errors['input_errors'])) @@ -148,6 +148,6 @@ def test_coincident_peak_inputs(self): post["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_active_timesteps"] = [1,100,6000,7000] post["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_charge_us_dollars_per_kw"] = 10.5 validator = self.get_validator(post) - self.assertEquals(validator.isValid, True) + self.assertEqual(validator.isValid, True) self.assertEqual(validator.input_dict["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_active_timesteps"], [[1,100,6000,7000]]) self.assertEqual(validator.input_dict["Scenario"]["Site"]["ElectricTariff"]["coincident_peak_load_charge_us_dollars_per_kw"], [10.5]) diff --git a/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py b/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py index 5a02b6559..9e3a0101d 100644 --- a/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py +++ b/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py @@ -14,11 +14,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='financialinputs', name='max_initial_capital_costs_before_incentives', - field=models.FloatField(blank=True, help_text='Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + field=models.FloatField(blank=True, help_text='Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [\\$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), ), migrations.AddField( model_name='financialinputs', name='min_initial_capital_costs_before_incentives', - field=models.FloatField(blank=True, help_text='Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + field=models.FloatField(blank=True, help_text='Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [\\$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), ), ] diff --git a/reoptjl/migrations/0105_alter_chpoutputs_initial_capital_costs_and_more.py b/reoptjl/migrations/0105_alter_chpoutputs_initial_capital_costs_and_more.py new file mode 100644 index 000000000..7674344bc --- /dev/null +++ b/reoptjl/migrations/0105_alter_chpoutputs_initial_capital_costs_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.24 on 2025-09-30 13:31 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0104_cstinputs_can_waste_heat'), + ] + + operations = [ + migrations.AlterField( + model_name='chpoutputs', + name='initial_capital_costs', + field=models.FloatField(blank=True, help_text='Initial capital costs of the CHP system, before incentives [$]', null=True), + ), + migrations.AlterField( + model_name='chpoutputs', + name='lifecycle_fuel_cost_after_tax', + field=models.FloatField(blank=True, help_text='Present value of cost of fuel consumed by the CHP system, after tax [$]', null=True), + ), + migrations.AlterField( + model_name='chpoutputs', + name='year_one_fuel_cost_after_tax', + field=models.FloatField(blank=True, help_text='Cost of fuel consumed by the CHP system in year one, after tax [$]', null=True), + ), + migrations.AlterField( + model_name='chpoutputs', + name='year_one_fuel_cost_before_tax', + field=models.FloatField(blank=True, help_text='Cost of fuel consumed by the CHP system in year one [$]', null=True), + ), + migrations.AlterField( + model_name='chpoutputs', + name='year_one_standby_cost_after_tax', + field=models.FloatField(blank=True, help_text='CHP standby charges in year one, after tax [$]', null=True), + ), + migrations.AlterField( + model_name='chpoutputs', + name='year_one_standby_cost_before_tax', + field=models.FloatField(blank=True, help_text='CHP standby charges in year one [$]', null=True), + ), + migrations.AlterField( + model_name='financialinputs', + name='max_initial_capital_costs_before_incentives', + field=models.FloatField(blank=True, help_text='Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='min_initial_capital_costs_before_incentives', + field=models.FloatField(blank=True, help_text='Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 38272057f..450625647 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -793,7 +793,7 @@ class FinancialInputs(BaseModel, models.Model): ], blank=True, null=True, - help_text=("Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].") + help_text=("Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [$].") ) max_initial_capital_costs_before_incentives = models.FloatField( validators=[ @@ -802,7 +802,7 @@ class FinancialInputs(BaseModel, models.Model): ], blank=True, null=True, - help_text=("Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].") + help_text=("Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [$].") ) CO2_cost_per_tonne = models.FloatField( validators=[ @@ -4763,23 +4763,23 @@ class CHPOutputs(BaseModel, models.Model): ) year_one_fuel_cost_before_tax = models.FloatField( null=True, blank=True, - help_text="Cost of fuel consumed by the CHP system in year one [\$]" + help_text="Cost of fuel consumed by the CHP system in year one [$]" ) year_one_fuel_cost_after_tax = models.FloatField( null=True, blank=True, - help_text="Cost of fuel consumed by the CHP system in year one, after tax [\$]" + help_text="Cost of fuel consumed by the CHP system in year one, after tax [$]" ) lifecycle_fuel_cost_after_tax = models.FloatField( null=True, blank=True, - help_text="Present value of cost of fuel consumed by the CHP system, after tax [\$]" + help_text="Present value of cost of fuel consumed by the CHP system, after tax [$]" ) year_one_standby_cost_before_tax = models.FloatField( null=True, blank=True, - help_text="CHP standby charges in year one [\$]" + help_text="CHP standby charges in year one [$]" ) year_one_standby_cost_after_tax = models.FloatField( null=True, blank=True, - help_text="CHP standby charges in year one, after tax [\$]" + help_text="CHP standby charges in year one, after tax [$]" ) lifecycle_standby_cost_after_tax = models.FloatField( null=True, blank=True, @@ -4803,7 +4803,7 @@ class CHPOutputs(BaseModel, models.Model): ) initial_capital_costs = models.FloatField( null=True, blank=True, - help_text="Initial capital costs of the CHP system, before incentives [\$]" + help_text="Initial capital costs of the CHP system, before incentives [$]" ) def clean(): diff --git a/reoptjl/test/test_http_endpoints.py b/reoptjl/test/test_http_endpoints.py index 14771a2c3..8bad4f1e0 100644 --- a/reoptjl/test/test_http_endpoints.py +++ b/reoptjl/test/test_http_endpoints.py @@ -192,9 +192,9 @@ def test_avert_emissions_profile_endpoint(self): resp = self.api_client.get(f'/v3/avert_emissions_profile', data=inputs) self.assertHttpOK(resp) view_response = json.loads(resp.content) - self.assertEquals(view_response["avert_meters_to_region"], 0.0) - self.assertEquals(view_response["avert_region"], "Northwest") - self.assertEquals(len(view_response["emissions_factor_series_lb_NOx_per_kwh"]), 8760) + self.assertEqual(view_response["avert_meters_to_region"], 0.0) + self.assertEqual(view_response["avert_region"], "Northwest") + self.assertEqual(len(view_response["emissions_factor_series_lb_NOx_per_kwh"]), 8760) #case 2: location off shore of NJ (works for AVERT, not Cambium) inputs = { "latitude": 39.034417, @@ -205,8 +205,8 @@ def test_avert_emissions_profile_endpoint(self): self.assertHttpOK(resp) view_response = json.loads(resp.content) self.assertAlmostEqual(view_response["avert_meters_to_region"], 760.62, delta=1.0) - self.assertEquals(view_response["avert_region"], "Mid-Atlantic") - self.assertEquals(len(view_response["emissions_factor_series_lb_NOx_per_kwh"]), 8760) + self.assertEqual(view_response["avert_region"], "Mid-Atlantic") + self.assertEqual(len(view_response["emissions_factor_series_lb_NOx_per_kwh"]), 8760) #case 3: Honolulu, HI (works for AVERT but not Cambium) inputs = { "latitude": 21.3099, @@ -216,9 +216,9 @@ def test_avert_emissions_profile_endpoint(self): resp = self.api_client.get(f'/v3/avert_emissions_profile', data=inputs) self.assertHttpOK(resp) view_response = json.loads(resp.content) - self.assertEquals(view_response["avert_meters_to_region"], 0.0) - self.assertEquals(view_response["avert_region"], "Hawaii (Oahu)") - self.assertEquals(len(view_response["emissions_factor_series_lb_NOx_per_kwh"]), 8760) + self.assertEqual(view_response["avert_meters_to_region"], 0.0) + self.assertEqual(view_response["avert_region"], "Hawaii (Oahu)") + self.assertEqual(len(view_response["emissions_factor_series_lb_NOx_per_kwh"]), 8760) #case 4: location well outside of US (does not work) inputs = { "latitude": 0.0, @@ -248,9 +248,9 @@ def test_cambium_profile_endpoint(self): resp = self.api_client.get(f'/v3/cambium_profile', data=inputs) self.assertHttpOK(resp) view_response = json.loads(resp.content) - self.assertEquals(view_response["metric_col"], "lrmer_co2e") - self.assertEquals(view_response["location"], "Northern Grid West") - self.assertEquals(len(view_response["data_series"]), 8760) + self.assertEqual(view_response["metric_col"], "lrmer_co2e") + self.assertEqual(view_response["location"], "Northern Grid West") + self.assertEqual(len(view_response["data_series"]), 8760) #case 2: location off shore of NJ (works for AVERT, not Cambium) inputs["latitude"] = 39.034417 inputs["longitude"] = -74.759292 diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 97502460d..ed309b049 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -182,11 +182,11 @@ def test_chp_defaults_from_julia(self): for key in view_response["default_inputs"].keys(): if post["CHP"].get(key) is None: # Check that default got assigned consistent with /chp_defaults if key == "max_kw": - self.assertEquals(inputs_chp[key], view_response["chp_max_size_kw"]) + self.assertEqual(inputs_chp[key], view_response["chp_max_size_kw"]) else: - self.assertEquals(inputs_chp[key], view_response["default_inputs"][key]) + self.assertEqual(inputs_chp[key], view_response["default_inputs"][key]) else: # Make sure we didn't overwrite user-input - self.assertEquals(inputs_chp[key], post["CHP"][key]) + self.assertEqual(inputs_chp[key], post["CHP"][key]) def test_peak_load_outage_times(self): """ @@ -208,7 +208,7 @@ def test_peak_load_outage_times(self): resp = self.api_client.post(f'/v3/peak_load_outage_times', data=outage_inputs) self.assertHttpOK(resp) resp = json.loads(resp.content) - self.assertEquals(resp["outage_start_time_steps"], expected_time_steps) + self.assertEqual(resp["outage_start_time_steps"], expected_time_steps) outage_inputs["seasonal_peaks"] = False outage_inputs["start_not_center_on_peaks"] = True @@ -216,7 +216,7 @@ def test_peak_load_outage_times(self): resp = self.api_client.post(f'/v3/peak_load_outage_times', data=outage_inputs) self.assertHttpOK(resp) resp = json.loads(resp.content) - self.assertEquals(resp["outage_start_time_steps"], expected_time_steps) + self.assertEqual(resp["outage_start_time_steps"], expected_time_steps) def test_superset_input_fields(self): """ @@ -274,9 +274,9 @@ def test_steamturbine_defaults_from_julia(self): # skip this key because its NaN in REoptInputs but is populated in /chp_defaults response. if key != "inlet_steam_temperature_degF": if post["SteamTurbine"].get(key) is None: # Check that default got assigned consistent with /chp_defaults - self.assertEquals(inputs_steamturbine[key], view_response["default_inputs"][key]) + self.assertEqual(inputs_steamturbine[key], view_response["default_inputs"][key]) else: # Make sure we didn't overwrite user-input - self.assertEquals(inputs_steamturbine[key], post["SteamTurbine"][key]) + self.assertEqual(inputs_steamturbine[key], post["SteamTurbine"][key]) def test_hybridghp(self): post_file = os.path.join('reoptjl', 'test', 'posts', 'hybrid_ghp.json') @@ -325,8 +325,8 @@ def test_ashp_defaults_update_from_julia(self): resp = self.api_client.get(f'/stable/job/{run_uuid}/results') r = json.loads(resp.content) - self.assertEquals(r["inputs"]["ASHPSpaceHeater"]["om_cost_per_ton"], 0.0) - self.assertEquals(r["inputs"]["ASHPSpaceHeater"]["sizing_factor"], 1.1) + self.assertEqual(r["inputs"]["ASHPSpaceHeater"]["om_cost_per_ton"], 0.0) + self.assertEqual(r["inputs"]["ASHPSpaceHeater"]["sizing_factor"], 1.1) def test_pv_cost_defaults_update_from_julia(self): # Test that the inputs_with_defaults_set_in_julia feature worked for PV @@ -340,5 +340,5 @@ def test_pv_cost_defaults_update_from_julia(self): resp = self.api_client.get(f'/stable/job/{run_uuid}/results') r = json.loads(resp.content) - self.assertEquals(r["inputs"]["PV"]["size_class"], 2) + self.assertEqual(r["inputs"]["PV"]["size_class"], 2) self.assertAlmostEqual(r["inputs"]["PV"]["installed_cost_per_kw"], 2914.6, delta=0.05 * 2914.6) \ No newline at end of file diff --git a/reoptjl/test/test_validator.py b/reoptjl/test/test_validator.py index 2e9d5c24b..ad86d28dd 100644 --- a/reoptjl/test/test_validator.py +++ b/reoptjl/test/test_validator.py @@ -36,15 +36,15 @@ def test_elec_load_profile_length_validation_and_resampling(self): validator.cross_clean() if length in good_lengths: - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) if length > 8760: # check downsampling - self.assertEquals(len(validator.models["ElectricLoad"].loads_kw), 8760) - self.assertEquals(len(validator.models["ElectricLoad"].critical_loads_kw), 8760) + self.assertEqual(len(validator.models["ElectricLoad"].loads_kw), 8760) + self.assertEqual(len(validator.models["ElectricLoad"].critical_loads_kw), 8760) assert("resampled inputs" in validator.messages) elif length in bad_lengths: - self.assertEquals(validator.is_valid, False) + self.assertEqual(validator.is_valid, False) assert('Invalid length' in validator.validation_errors['ElectricLoad']['loads_kw']) assert('Invalid length' in validator.validation_errors['ElectricLoad']['critical_loads_kw']) @@ -59,9 +59,9 @@ def test_elec_load_profile_length_validation_and_resampling(self): validator.clean() validator.clean_fields() validator.cross_clean() - self.assertEquals(validator.is_valid, True) - self.assertEquals(len(validator.models["ElectricLoad"].loads_kw), time_steps_per_hour*8760) - self.assertEquals(len(validator.models["ElectricLoad"].critical_loads_kw), time_steps_per_hour*8760) + self.assertEqual(validator.is_valid, True) + self.assertEqual(len(validator.models["ElectricLoad"].loads_kw), time_steps_per_hour*8760) + self.assertEqual(len(validator.models["ElectricLoad"].critical_loads_kw), time_steps_per_hour*8760) def test_bad_blended_profile_inputs(self): post = copy.deepcopy(self.post) @@ -98,7 +98,7 @@ def test_off_grid_defaults_overrides(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) self.assertAlmostEqual(validator.models["Wind"].operating_reserve_required_fraction, 0.5) self.assertAlmostEqual(validator.models["PV"].operating_reserve_required_fraction, 0.25) @@ -137,7 +137,7 @@ def test_off_grid_defaults_overrides(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) self.assertAlmostEqual(validator.models["PV"].operating_reserve_required_fraction, 0.35) self.assertAlmostEqual(validator.models["Wind"].operating_reserve_required_fraction, 0.35) @@ -166,7 +166,7 @@ def existingboiler_boiler_validation(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) self.assertAlmostEqual(validator.models["ExistingBoiler"].emissions_factor_lb_CO2_per_mmbtu, 117, places=-1) self.assertAlmostEqual(len(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 8760) @@ -189,7 +189,7 @@ def existingboiler_boiler_validation(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) self.assertAlmostEqual(len(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 8760) self.assertEqual(sum(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 9432.0) @@ -264,7 +264,7 @@ def test_multiple_outages_validation(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) # test mismatched length post = copy.deepcopy(outage_post) @@ -301,8 +301,8 @@ def test_multiple_outages_validation(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.models["ElectricUtility"].outage_probabilities, [0.5, 0.5]) - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.models["ElectricUtility"].outage_probabilities, [0.5, 0.5]) + self.assertEqual(validator.is_valid, True) def test_pv_tilt_defaults(self): post = copy.deepcopy(self.post) @@ -314,7 +314,7 @@ def test_pv_tilt_defaults(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.models["PV"].tilt, 20) + self.assertEqual(validator.models["PV"].tilt, 20) post["APIMeta"]["run_uuid"] = uuid.uuid4() post["PV"]["array_type"] = 2 @@ -322,7 +322,7 @@ def test_pv_tilt_defaults(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertAlmostEquals(validator.models["PV"].tilt, 0) + self.assertAlmostEqual(validator.models["PV"].tilt, 0) def boiler_validation(self): """ @@ -337,7 +337,7 @@ def boiler_validation(self): validator.clean_fields() validator.clean() validator.cross_clean() - self.assertEquals(validator.is_valid, True) + self.assertEqual(validator.is_valid, True) # Update with Boiler test fields # self.assertAlmostEqual(validator.models["ExistingBoiler"].emissions_factor_lb_CO2_per_mmbtu, 117, places=-1) diff --git a/reoptjl/validators.py b/reoptjl/validators.py index 306814dd8..a55301486 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -651,9 +651,9 @@ def validate_time_series(series: list, time_steps_per_hour: int) -> Tuple[list, if time_steps_per_hour < time_steps_per_hour_in_series: resampling_msg = f"Downsampled to match time_steps_per_hour via average." - index = pd.date_range('1/1/2000', periods=n, freq=f'{int(60/time_steps_per_hour_in_series)}T') + index = pd.date_range('1/1/2000', periods=n, freq=f'{int(60/time_steps_per_hour_in_series)}min') s = pd.Series(series, index=index) - s = s.resample(f'{int(60/time_steps_per_hour)}T').mean() + s = s.resample(f'{int(60/time_steps_per_hour)}min').mean() return s.tolist(), resampling_msg, "" # time_steps_per_hour > time_steps_per_hour_in_series diff --git a/requirements.txt b/requirements.txt index e6b1af133..42c96bbbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,119 +1,162 @@ -# python 3.8 +# python 3.12 adal==1.2.7 -amqp==5.0.9 -asgiref==3.4.1 -attrs==21.4.0 -autopep8==1.6.0 -backports.zoneinfo==0.2.1 -bandit==1.7.1 -billiard==3.6.4.0 -cachetools==4.2.4 -celery==5.2.3 -certifi==2022.12.7 -cffi==1.15.0 -chardet==4.0.0 -charset-normalizer==2.0.10 -click==8.0.3 -click-didyoumean==0.3.0 -click-plugins==1.1.1 -click-repl==0.2.0 +amqp==5.3.1 +asgiref==3.9.1 +asttokens==3.0.0 +attrs==25.3.0 +autopep8==2.3.2 +azure-core==1.35.0 +backcall==0.2.0 +bandit==1.8.6 +beautifulsoup4==4.13.5 +billiard==4.2.1 +bleach==6.2.0 +blosc2==3.7.2 +cachetools==5.5.2 +celery==5.5.3 +certifi==2025.8.3 +cffi==2.0.0 +chardet==5.2.0 +charset-normalizer==3.4.3 +click==8.2.1 +click-didyoumean==0.3.1 +click-plugins==1.1.1.2 +click-repl==0.3.0 cligj==0.7.2 -colorama==0.4.4 -CoolProp==6.4.1 -cryptography==36.0.1 -Cython==0.29.26 -decorator==5.1.1 +colorama==0.4.6 +coolprop==7.0.0 +cryptography==45.0.7 +Cython==3.1.3 +decorator==5.2.1 deepdish==0.3.7 -Deprecated==1.2.13 -Django==4.0.7 -django-celery-results==2.4.0 -django-extensions==3.1.5 -django-picklefield==3.0.1 -django-tastypie==0.14.4 +defusedxml==0.7.1 +Deprecated==1.2.18 +Django==4.2.24 +django-extensions==4.1 +django-picklefield==3.3 +django-tastypie==0.15.1 +django_celery_results==2.6.0 docopt==0.6.2 -et-xmlfile==1.1.0 -Fiona==1.8.20 -future==0.18.3 -geopandas==0.10.2 -gitdb==4.0.9 -GitPython==3.1.30 -google-api-core==2.4.0 -google-api-python-client==2.36.0 -google-auth==1.35.0 -google-auth-httplib2==0.1.0 -google-auth-oauthlib==0.4.6 -googleapis-common-protos==1.54.0 -greenlet==1.1.2 -gunicorn==20.1.0 -h5py==3.6.0 -h5pyd==0.9.2 -httplib2==0.20.2 -idna==3.3 -importlib-metadata==4.10.1 +et_xmlfile==2.0.0 +executing==2.2.1 +fastjsonschema==2.21.2 +fiona==1.10.1 +future==1.0.0 +geopandas==1.1.1 +gitdb==4.0.12 +GitPython==3.1.45 +google-api-core==2.25.1 +google-api-python-client==2.181.0 +google-auth==2.40.3 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.2 +googleapis-common-protos==1.70.0 +greenlet==3.2.4 +gunicorn==23.0.0 +h5py==3.14.0 +h5pyd==0.18.0 +httplib2==0.30.0 +idna==3.10 +importlib_metadata==8.7.0 +ipython==8.12.3 ipython-genutils==0.2.0 -isodate==0.6.1 +isodate==0.7.2 jdcal==1.4.1 -kombu==5.2.3 -lxml==4.9.1 -more-itertools==8.12.0 -msrest==0.6.21 -msrestazure==0.6.4 -munch==2.5.0 -numexpr==2.8.1 -numpy==1.22.1 +jedi==0.19.2 +Jinja2==3.1.6 +jsonschema==4.25.1 +jsonschema-specifications==2025.9.1 +jupyter_client==8.6.3 +jupyter_core==5.8.1 +jupyterlab_pygments==0.3.0 +kombu==5.5.4 +lxml==6.0.1 +markdown-it-py==4.0.0 +MarkupSafe==3.0.2 +matplotlib-inline==0.1.7 +mdurl==0.1.2 +mistune==3.1.4 +more-itertools==10.8.0 +msgpack==1.1.1 +msrest==0.7.1 +msrestazure==0.6.4.post1 +munch==4.0.0 +nbclient==0.10.2 +nbconvert==7.16.6 +nbformat==5.10.4 +ndindex==1.10.0 +numexpr==2.11.0 +numpy==1.26.0 numpy-financial==1.0.0 -oauthlib==3.2.2 -openpyxl==3.0.9 -packaging==21.3 -pandas==1.3.5 -pathlib2==2.3.6 -pbr==5.8.0 -pexpect==4.8.0 -pipreqs==0.4.11 -prompt-toolkit==3.0.24 -protobuf==3.19.5 -psutil==5.9.0 -psycopg2==2.9.3 +oauthlib==3.3.1 +openpyxl==3.1.5 +packaging==25.0 +pandas==2.3.2 +pandocfilters==1.5.1 +parso==0.8.5 +pathlib2==2.3.7.post1 +pbr==7.0.1 +pexpect==4.9.0 +pickleshare==0.7.5 +pipreqs==0.5.0 +platformdirs==4.4.0 +prompt_toolkit==3.0.52 +proto-plus==1.26.1 +protobuf==6.32.0 +psutil==7.0.0 +psycopg2==2.9.10 ptyprocess==0.7.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycodestyle==2.8.0 -pycparser==2.21 -Pygments==2.11.2 -PyJWT==2.4.0 -pyparsing==3.0.6 -pyproj==3.3.0 -python-dateutil==2.8.2 -python-mimeparse==1.6.0 -pytz==2021.3 +pure_eval==0.2.3 +py-cpuinfo==9.0.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pycodestyle==2.14.0 +pycparser==2.23 +Pygments==2.19.2 +PyJWT==2.10.1 +pyogrio==0.11.1 +pyparsing==3.2.3 +pyproj==3.7.2 +python-dateutil==2.9.0.post0 +python-mimeparse==2.0.0 +pytz==2025.2 pytz-deprecation-shim==0.1.0.post0 -PyYAML==6.0 -redis==4.1.1 -requests==2.27.1 -requests-oauthlib==1.3.0 -requests-unixsocket==0.3.0 -rollbar==0.16.2 -rsa==4.8 -scipy==1.7.3 -Shapely==1.8.0 -six==1.16.0 -smmap==5.0.0 -SQLAlchemy==1.4.30 -sqlparse==0.4.2 -stevedore==3.5.0 -tables==3.7.0 +PyYAML==6.0.2 +pyzmq==27.1.0 +redis==6.4.0 +referencing==0.36.2 +requests==2.32.5 +requests-oauthlib==2.0.0 +requests-unixsocket==0.4.1 +rich==14.1.0 +rollbar==1.3.0 +rpds-py==0.27.1 +rsa==4.9.1 +scipy==1.16.1 +setuptools==80.9.0 +shapely==2.1.1 +six==1.17.0 +smmap==5.0.2 +soupsieve==2.8 +SQLAlchemy==2.0.43 +sqlparse==0.5.3 +stack-data==0.6.3 +stevedore==5.5.0 +tables==3.10.2 +tinycss2==1.4.0 toml==0.10.2 -traitlets==5.1.1 -typing_extensions==4.0.1 -tzdata==2021.5 -tzlocal==4.1 -uritemplate==4.1.1 -urllib3==1.26.8 -vine==5.0.0 -wcwidth==0.2.5 -wrapt==1.13.3 -xlsxwriter==3.1.9 -xlrd==2.0.1 +tornado==6.5.2 +traitlets==5.14.3 +typing_extensions==4.15.0 +tzdata==2025.2 +tzlocal==5.3.1 +uritemplate==4.2.0 +urllib3==2.5.0 +vine==5.1.0 +wcwidth==0.2.13 +webencodings==0.5.1 +wrapt==1.17.3 +xlrd==2.0.2 +xlsxwriter==3.2.5 yarg==0.1.9 -zipp==3.7.0 - +zipp==3.23.0 diff --git a/resilience_stats/tests/test_resilience_stats.py b/resilience_stats/tests/test_resilience_stats.py index 0023ff6f0..dae44b7ef 100644 --- a/resilience_stats/tests/test_resilience_stats.py +++ b/resilience_stats/tests/test_resilience_stats.py @@ -293,14 +293,14 @@ def test_outage_sim_no_diesel(self): self.assertAlmostEqual(expected['resilience_hours_avg'], resp['resilience_hours_avg'], places=3) self.assertAlmostEqual(expected['outage_durations'], resp['outage_durations'], places=3) for x, y in zip(expected['probs_of_surviving'], resp['probs_of_surviving']): - self.assertAlmostEquals(x, y, places=3) + self.assertAlmostEqual(x, y, places=3) for e, r in zip([0, 11], [0, 11]): for x, y in zip(expected['probs_of_surviving_by_month'][e], resp['probs_of_surviving_by_month'][r]): - self.assertAlmostEquals(x, y, places=3) + self.assertAlmostEqual(x, y, places=3) for e, r in zip([0, 23], [0, 23]): for x, y in zip(expected['probs_of_surviving_by_hour_of_the_day'][e], resp['probs_of_surviving_by_hour_of_the_day'][r]): - self.assertAlmostEquals(x, y, places=3) + self.assertAlmostEqual(x, y, places=3) def test_outage_sim(self): """ @@ -334,7 +334,7 @@ def test_outage_sim(self): self.assertAlmostEqual(expected['resilience_hours_avg'], resp['resilience_hours_avg'], places=4) self.assertListEqual(expected['outage_durations'], resp['outage_durations']) for x, y in zip(expected['probs_of_surviving'], resp['probs_of_surviving']): - self.assertAlmostEquals(x, y, places=4) + self.assertAlmostEqual(x, y, places=4) def test_no_resilience(self): inputs = self.inputs @@ -361,7 +361,7 @@ def test_flexible_timesteps(self): self.assertAlmostEqual(1, resp2['resilience_hours_avg'] / resp1['resilience_hours_avg'], places=1) for x, y in zip(resp1['probs_of_surviving'], resp2['probs_of_surviving']): - self.assertAlmostEquals(x, y, places=1) + self.assertAlmostEqual(x, y, places=1) def test_resil_endpoint(self): post = json.load(open(os.path.join('resilience_stats', 'tests', 'POST_nested.json'), 'r')) @@ -413,4 +413,4 @@ def test_outage_sim_chp(self): self.assertAlmostEqual(expected['resilience_hours_avg'], resp['resilience_hours_avg'], places=4) self.assertListEqual(expected['outage_durations'], resp['outage_durations']) for x, y in zip(expected['probs_of_surviving'], resp['probs_of_surviving']): - self.assertAlmostEquals(x, y, places=4) + self.assertAlmostEqual(x, y, places=4)